Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file main/tg_cli.c
6 : * @brief tg-cli — batch read+write Telegram CLI entry point.
7 : *
8 : * Thin wrapper over tg-domain-read + tg-domain-write (ADR-0005). Exposes
9 : * every read command (identical to tg-cli-ro) plus all write commands.
10 : * For the interactive REPL/TUI use tg-tui(1).
11 : */
12 :
13 : #include "app/bootstrap.h"
14 : #include "app/auth_flow.h"
15 : #include "app/credentials.h"
16 : #include "app/config_wizard.h"
17 : #include "app/dc_config.h"
18 : #include "app/session_store.h"
19 : #include "infrastructure/auth_logout.h"
20 : #include "infrastructure/updates_state_store.h"
21 : #include "infrastructure/media_index.h"
22 : #include "logger.h"
23 : #include "arg_parse.h"
24 : #include "json_util.h"
25 : #include "platform/path.h"
26 :
27 : #include "domain/read/self.h"
28 : #include "domain/read/dialogs.h"
29 : #include "domain/read/history.h"
30 : #include "domain/read/updates.h"
31 : #include "domain/read/user_info.h"
32 : #include "domain/read/search.h"
33 : #include "domain/read/contacts.h"
34 : #include "domain/read/media.h"
35 : #include "domain/write/send.h"
36 : #include "domain/write/read_history.h"
37 : #include "domain/write/edit.h"
38 : #include "domain/write/delete.h"
39 : #include "domain/write/forward.h"
40 : #include "domain/write/upload.h"
41 : #include "fs_util.h"
42 :
43 : #include <errno.h>
44 : #include <signal.h>
45 : #include <stdio.h>
46 : #include <stdlib.h>
47 : #include <string.h>
48 : #include <unistd.h>
49 :
50 : /* SEC-01: set once at startup; 1 when stdout is a real terminal. */
51 : static int g_stdout_is_tty = 0;
52 :
53 : /**
54 : * @brief Sanitize @p src into @p dst for terminal display (SEC-01).
55 : */
56 0 : static void tty_sanitize(char *dst, size_t cap, const char *src) {
57 0 : if (!dst || cap == 0) return;
58 0 : if (!src) { dst[0] = '\0'; return; }
59 0 : if (!g_stdout_is_tty) {
60 0 : size_t i = 0;
61 0 : while (src[i] && i + 1 < cap) { dst[i] = src[i]; i++; }
62 0 : dst[i] = '\0';
63 0 : return;
64 : }
65 0 : size_t i = 0;
66 0 : while (*src && i + 1 < cap) {
67 0 : unsigned char c = (unsigned char)*src++;
68 0 : if ((c < 0x20 && c != 0x09 && c != 0x0A) || c == 0x7F || c == 0x9B)
69 0 : dst[i++] = '.';
70 : else
71 0 : dst[i++] = (char)c;
72 : }
73 0 : dst[i] = '\0';
74 : }
75 :
76 : typedef struct {
77 : const char *phone;
78 : const char *code;
79 : const char *password;
80 : } BatchCreds;
81 :
82 0 : static int cb_get_phone(void *u, char *out, size_t cap) {
83 0 : const BatchCreds *c = (const BatchCreds *)u;
84 0 : if (!c->phone) {
85 0 : fprintf(stderr, "tg-cli: --phone <number> required in batch mode\n");
86 0 : return -1;
87 : }
88 0 : snprintf(out, cap, "%s", c->phone);
89 0 : return 0;
90 : }
91 0 : static int cb_get_code(void *u, char *out, size_t cap) {
92 0 : const BatchCreds *c = (const BatchCreds *)u;
93 0 : if (!c->code) {
94 0 : fprintf(stderr, "tg-cli: --code <digits> required in batch mode\n");
95 0 : return -1;
96 : }
97 0 : snprintf(out, cap, "%s", c->code);
98 0 : return 0;
99 : }
100 0 : static int cb_get_password(void *u, char *out, size_t cap) {
101 0 : const BatchCreds *c = (const BatchCreds *)u;
102 0 : if (!c->password) return -1;
103 0 : snprintf(out, cap, "%s", c->password);
104 0 : return 0;
105 : }
106 :
107 0 : static int session_bringup(const ArgResult *args, ApiConfig *cfg,
108 : MtProtoSession *s, Transport *t) {
109 0 : if (credentials_load(cfg) != 0) return 1;
110 : static BatchCreds creds;
111 0 : creds.phone = args->phone;
112 0 : creds.code = args->code;
113 0 : creds.password = args->password;
114 :
115 : static AuthFlowCallbacks cb;
116 0 : cb.get_phone = cb_get_phone;
117 0 : cb.get_code = cb_get_code;
118 0 : cb.get_password = cb_get_password;
119 0 : cb.user = &creds;
120 :
121 0 : transport_init(t);
122 0 : mtproto_session_init(s);
123 0 : AuthFlowResult res = {0};
124 0 : if (auth_flow_login(cfg, &cb, t, s, &res) != 0) {
125 0 : fprintf(stderr, "tg-cli: login failed (see logs)\n");
126 0 : transport_close(t);
127 0 : return 1;
128 : }
129 0 : return 0;
130 : }
131 :
132 : /* ---- Read-command helpers (matching tg_cli_ro) ---- */
133 :
134 0 : static const char *peer_kind_name(DialogPeerKind k) {
135 0 : switch (k) {
136 0 : case DIALOG_PEER_USER: return "user";
137 0 : case DIALOG_PEER_CHAT: return "chat";
138 0 : case DIALOG_PEER_CHANNEL: return "channel";
139 0 : default: return "unknown";
140 : }
141 : }
142 :
143 0 : static const char *resolved_kind_name(ResolvedKind k) {
144 0 : switch (k) {
145 0 : case RESOLVED_KIND_USER: return "user";
146 0 : case RESOLVED_KIND_CHAT: return "chat";
147 0 : case RESOLVED_KIND_CHANNEL: return "channel";
148 0 : default: return "unknown";
149 : }
150 : }
151 :
152 : static volatile sig_atomic_t g_stop = 0;
153 0 : static void on_sigint(int sig) { (void)sig; g_stop = 1; }
154 :
155 : #define WATCH_BACKOFF_CAP_S 300
156 : #define WATCH_BACKOFF_INIT_S 5
157 : #define WATCH_PEERS_MAX 64
158 :
159 0 : static int watch_peers_resolve(const ApiConfig *cfg, MtProtoSession *s,
160 : Transport *t, const char *spec,
161 : int64_t *ids, int cap) {
162 0 : if (!spec || !*spec) return 0;
163 : char buf[4096];
164 0 : size_t slen = strlen(spec);
165 0 : if (slen >= sizeof(buf)) {
166 0 : fprintf(stderr, "watch: --peers value too long\n");
167 0 : return -1;
168 : }
169 0 : memcpy(buf, spec, slen + 1);
170 0 : int n = 0;
171 0 : char *tok = strtok(buf, ",");
172 0 : while (tok && n < cap) {
173 0 : while (*tok == ' ' || *tok == '\t') tok++;
174 0 : char *end = tok + strlen(tok);
175 0 : while (end > tok && (end[-1] == ' ' || end[-1] == '\t')) *--end = '\0';
176 0 : if (!*tok) { tok = strtok(NULL, ","); continue; }
177 0 : if (strcmp(tok, "self") == 0) { ids[n++] = 0; tok = strtok(NULL, ","); continue; }
178 0 : char *numend = NULL;
179 0 : long long numid = strtoll(tok, &numend, 10);
180 0 : if (numend && *numend == '\0') { ids[n++] = (int64_t)numid; tok = strtok(NULL, ","); continue; }
181 0 : ResolvedPeer rp = {0};
182 0 : if (domain_resolve_username(cfg, s, t, tok, &rp) != 0) {
183 0 : fprintf(stderr, "watch: --peers: cannot resolve '%s'\n", tok);
184 0 : return -1;
185 : }
186 0 : ids[n++] = rp.id;
187 0 : tok = strtok(NULL, ",");
188 : }
189 0 : return n;
190 : }
191 :
192 0 : static int watch_peer_allowed(const int64_t *ids, int n, int64_t peer_id) {
193 0 : if (n == 0) return 1;
194 0 : for (int i = 0; i < n; i++) if (ids[i] == peer_id) return 1;
195 0 : return 0;
196 : }
197 :
198 : static int resolve_peer_arg(const ApiConfig *cfg, MtProtoSession *s,
199 : Transport *t, const char *peer_arg,
200 : HistoryPeer *out);
201 :
202 0 : static int cmd_me(const ArgResult *args) {
203 : ApiConfig cfg; MtProtoSession s; Transport t;
204 0 : int brc = session_bringup(args, &cfg, &s, &t);
205 0 : if (brc != 0) return brc;
206 0 : SelfInfo me = {0};
207 0 : int rc = domain_get_self(&cfg, &s, &t, &me);
208 0 : transport_close(&t);
209 0 : if (rc != 0) { fprintf(stderr, "tg-cli me: failed\n"); return 1; }
210 0 : if (args->json) {
211 0 : printf("{\"id\":%lld,\"username\":\"%s\",\"first_name\":\"%s\","
212 : "\"last_name\":\"%s\",\"phone\":\"%s\",\"premium\":%s,\"bot\":%s}\n",
213 0 : (long long)me.id, me.username, me.first_name, me.last_name, me.phone,
214 0 : me.is_premium ? "true" : "false", me.is_bot ? "true" : "false");
215 : } else {
216 : char s1[128], s2[128], s3[128];
217 0 : tty_sanitize(s1, sizeof(s1), me.username);
218 0 : tty_sanitize(s2, sizeof(s2), me.first_name);
219 0 : tty_sanitize(s3, sizeof(s3), me.last_name);
220 0 : printf("id: %lld\n", (long long)me.id);
221 0 : if (me.username[0]) printf("username: @%s\n", s1);
222 0 : if (me.first_name[0] || me.last_name[0])
223 0 : printf("name: %s%s%s\n", s2, me.last_name[0] ? " " : "", s3);
224 0 : if (me.phone[0]) printf("phone: +%s\n", me.phone);
225 0 : printf("premium: %s\n", me.is_premium ? "yes" : "no");
226 0 : if (me.is_bot) printf("bot: yes\n");
227 : }
228 0 : return 0;
229 : }
230 :
231 0 : static int cmd_dialogs(const ArgResult *args) {
232 : ApiConfig cfg; MtProtoSession s; Transport t;
233 0 : int brc = session_bringup(args, &cfg, &s, &t);
234 0 : if (brc != 0) return brc;
235 0 : int limit = args->limit > 0 ? args->limit : 20;
236 0 : if (limit > 100) limit = 100;
237 0 : DialogEntry *entries = calloc((size_t)limit, sizeof(DialogEntry));
238 0 : if (!entries) { transport_close(&t); return 1; }
239 0 : int count = 0;
240 0 : int rc = domain_get_dialogs(&cfg, &s, &t, limit, args->archived, entries, &count, NULL);
241 0 : transport_close(&t);
242 0 : if (rc != 0) {
243 0 : fprintf(stderr, "tg-cli dialogs: failed (see logs)\n");
244 0 : free(entries); return 1;
245 : }
246 0 : if (args->json) {
247 0 : printf("[");
248 0 : for (int i = 0; i < count; i++) {
249 0 : if (i) printf(",");
250 0 : printf("{\"type\":\"%s\",\"id\":%lld,\"title\":\"%s\","
251 : "\"username\":\"%s\",\"top\":%d,\"unread\":%d}",
252 0 : peer_kind_name(entries[i].kind),
253 0 : (long long)entries[i].peer_id,
254 0 : entries[i].title, entries[i].username,
255 0 : entries[i].top_message_id, entries[i].unread_count);
256 : }
257 0 : printf("]\n");
258 : } else {
259 0 : printf("%-8s %6s %-32s %s\n", "type", "unread", "title", "@username / id");
260 0 : for (int i = 0; i < count; i++) {
261 : char stitle[128], susername[64];
262 0 : tty_sanitize(stitle, sizeof(stitle), entries[i].title);
263 0 : tty_sanitize(susername, sizeof(susername), entries[i].username);
264 0 : const char *title = entries[i].title[0] ? stitle : "(no title)";
265 0 : if (entries[i].username[0])
266 0 : printf("%-8s %6d %-32s @%s\n",
267 0 : peer_kind_name(entries[i].kind), entries[i].unread_count,
268 : title, susername);
269 : else
270 0 : printf("%-8s %6d %-32s %lld\n",
271 0 : peer_kind_name(entries[i].kind), entries[i].unread_count,
272 0 : title, (long long)entries[i].peer_id);
273 : }
274 : }
275 0 : free(entries);
276 0 : return 0;
277 : }
278 :
279 0 : static int cmd_history(const ArgResult *args) {
280 : ApiConfig cfg; MtProtoSession s; Transport t;
281 0 : int brc = session_bringup(args, &cfg, &s, &t);
282 0 : if (brc != 0) return brc;
283 0 : HistoryPeer peer = {0};
284 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &peer) != 0) {
285 0 : transport_close(&t); return 1;
286 : }
287 0 : int limit = args->limit > 0 ? args->limit : 20;
288 0 : if (limit > 100) limit = 100;
289 0 : int offset = args->offset > 0 ? args->offset : 0;
290 0 : HistoryEntry *entries = calloc((size_t)limit, sizeof(HistoryEntry));
291 0 : if (!entries) { transport_close(&t); return 1; }
292 0 : int count = 0;
293 0 : int rc = domain_get_history(&cfg, &s, &t, &peer, offset, limit, entries, &count);
294 0 : transport_close(&t);
295 0 : if (rc != 0) {
296 0 : fprintf(stderr, "tg-cli history: failed (see logs)\n");
297 0 : free(entries); return 1;
298 : }
299 : static const char *media_label[] = {
300 : [MEDIA_NONE] = "", [MEDIA_EMPTY] = "", [MEDIA_UNSUPPORTED] = "unsup",
301 : [MEDIA_PHOTO] = "photo", [MEDIA_DOCUMENT] = "document", [MEDIA_GEO] = "geo",
302 : [MEDIA_CONTACT] = "contact", [MEDIA_VENUE] = "venue",
303 : [MEDIA_GEO_LIVE] = "geo_live", [MEDIA_DICE] = "dice",
304 : [MEDIA_WEBPAGE] = "webpage", [MEDIA_POLL] = "poll",
305 : [MEDIA_INVOICE] = "invoice", [MEDIA_STORY] = "story",
306 : [MEDIA_GIVEAWAY] = "giveaway", [MEDIA_GAME] = "game",
307 : [MEDIA_PAID] = "paid", [MEDIA_OTHER] = "other",
308 : };
309 0 : if (args->json) {
310 0 : printf("[");
311 0 : int first = 1;
312 0 : for (int i = 0; i < count; i++) {
313 0 : if (args->no_media && entries[i].media != MEDIA_NONE
314 0 : && entries[i].media != MEDIA_EMPTY
315 0 : && entries[i].text[0] == '\0') continue;
316 0 : if (!first) printf(",");
317 0 : first = 0;
318 0 : const char *ml = args->no_media ? "" : media_label[entries[i].media];
319 0 : long long mid = args->no_media ? 0LL : (long long)entries[i].media_id;
320 0 : char cached_path[2048] = {0};
321 0 : int has_cache = 0;
322 0 : if (!args->no_media && entries[i].media_id != 0
323 0 : && (entries[i].media == MEDIA_PHOTO || entries[i].media == MEDIA_DOCUMENT))
324 0 : has_cache = (media_index_get(entries[i].media_id, cached_path, sizeof(cached_path)) == 1);
325 0 : printf("{\"id\":%d,\"out\":%s,\"date\":%lld,\"text\":\"%s\","
326 : "\"complex\":%s,\"media\":\"%s\",\"media_id\":%lld"
327 : ",\"media_path\":\"%s\"}",
328 0 : entries[i].id, entries[i].out ? "true" : "false",
329 0 : (long long)entries[i].date, entries[i].text,
330 0 : entries[i].complex ? "true" : "false",
331 : ml, mid, has_cache ? cached_path : "");
332 : }
333 0 : printf("]\n");
334 : } else {
335 0 : int printed = 0;
336 0 : for (int i = 0; i < count; i++) {
337 0 : const char *ml = media_label[entries[i].media];
338 0 : char cached_path[2048] = {0};
339 0 : int has_cache = 0;
340 0 : if (entries[i].media_id != 0
341 0 : && (entries[i].media == MEDIA_PHOTO || entries[i].media == MEDIA_DOCUMENT))
342 0 : has_cache = (media_index_get(entries[i].media_id, cached_path, sizeof(cached_path)) == 1);
343 : char stext[HISTORY_TEXT_MAX];
344 0 : tty_sanitize(stext, sizeof(stext), entries[i].text);
345 0 : if (entries[i].complex) {
346 0 : printf("[%d] %s %lld (complex \xe2\x80\x94 text not parsed)\n",
347 0 : entries[i].id, entries[i].out ? ">" : "<", (long long)entries[i].date);
348 0 : printed++;
349 0 : } else if (ml[0] && args->no_media) {
350 0 : if (entries[i].text[0] == '\0') continue;
351 0 : printf("[%d] %s %lld %s\n",
352 0 : entries[i].id, entries[i].out ? ">" : "<",
353 0 : (long long)entries[i].date, stext);
354 0 : printed++;
355 0 : } else if (ml[0]) {
356 0 : if (has_cache)
357 0 : printf("[%d] %s %lld [%s: %s] %s\n",
358 0 : entries[i].id, entries[i].out ? ">" : "<",
359 0 : (long long)entries[i].date, ml, cached_path, stext);
360 : else
361 0 : printf("[%d] %s %lld [%s] %s\n",
362 0 : entries[i].id, entries[i].out ? ">" : "<",
363 0 : (long long)entries[i].date, ml, stext);
364 0 : printed++;
365 : } else {
366 0 : printf("[%d] %s %lld %s\n",
367 0 : entries[i].id, entries[i].out ? ">" : "<",
368 0 : (long long)entries[i].date, stext);
369 0 : printed++;
370 : }
371 : }
372 0 : if (printed == 0) printf("(no messages)\n");
373 : }
374 0 : free(entries);
375 0 : return 0;
376 : }
377 :
378 0 : static int cmd_search(const ArgResult *args) {
379 0 : if (!args->query) { fprintf(stderr, "tg-cli search: <query> required\n"); return 1; }
380 : ApiConfig cfg; MtProtoSession s; Transport t;
381 0 : int brc = session_bringup(args, &cfg, &s, &t);
382 0 : if (brc != 0) return brc;
383 0 : int limit = args->limit > 0 ? args->limit : 20;
384 0 : if (limit > 100) limit = 100;
385 0 : HistoryEntry *entries = calloc((size_t)limit, sizeof(HistoryEntry));
386 0 : if (!entries) { transport_close(&t); return 1; }
387 0 : int count = 0, rc;
388 0 : if (args->peer) {
389 0 : HistoryPeer peer = {0};
390 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &peer) != 0) {
391 0 : transport_close(&t); free(entries); return 1;
392 : }
393 0 : rc = domain_search_peer(&cfg, &s, &t, &peer, args->query, limit, entries, &count);
394 : } else {
395 0 : rc = domain_search_global(&cfg, &s, &t, args->query, limit, entries, &count);
396 : }
397 0 : transport_close(&t);
398 0 : if (rc != 0) {
399 0 : fprintf(stderr, "tg-cli search: failed (see logs)\n");
400 0 : free(entries); return 1;
401 : }
402 0 : if (args->json) {
403 : char esc[HISTORY_TEXT_MAX * 6 + 1];
404 0 : printf("[");
405 0 : for (int i = 0; i < count; i++) {
406 0 : if (i) printf(",");
407 0 : json_escape_str(esc, sizeof(esc), entries[i].text);
408 0 : printf("{\"id\":%d,\"out\":%s,\"date\":%lld,\"text\":\"%s\","
409 : "\"complex\":%s}",
410 0 : entries[i].id, entries[i].out ? "true" : "false",
411 0 : (long long)entries[i].date, esc,
412 0 : entries[i].complex ? "true" : "false");
413 : }
414 0 : printf("]\n");
415 : } else {
416 0 : printf("%-8s %-4s %-20s %s\n", "id", "out", "date", "text");
417 0 : for (int i = 0; i < count; i++) {
418 : char stext[HISTORY_TEXT_MAX];
419 0 : tty_sanitize(stext, sizeof(stext), entries[i].text);
420 0 : printf("%-8d %-4s %-20lld %s\n",
421 0 : entries[i].id, entries[i].out ? "yes" : "no",
422 0 : (long long)entries[i].date,
423 0 : entries[i].complex ? "(complex \xe2\x80\x94 text not parsed)" : stext);
424 : }
425 0 : if (count == 0) printf("(no matches)\n");
426 : }
427 0 : free(entries);
428 0 : return 0;
429 : }
430 :
431 0 : static int cmd_contacts(const ArgResult *args) {
432 : ApiConfig cfg; MtProtoSession s; Transport t;
433 0 : int brc = session_bringup(args, &cfg, &s, &t);
434 0 : if (brc != 0) return brc;
435 0 : ContactEntry *entries = calloc(CONTACTS_MAX, sizeof(ContactEntry));
436 0 : if (!entries) { transport_close(&t); return 1; }
437 0 : int count = 0;
438 0 : int rc = domain_get_contacts(&cfg, &s, &t, entries, CONTACTS_MAX, &count);
439 0 : transport_close(&t);
440 0 : if (rc != 0) {
441 0 : fprintf(stderr, "tg-cli contacts: failed (see logs)\n");
442 0 : free(entries); return 1;
443 : }
444 0 : if (args->json) {
445 0 : printf("[");
446 0 : for (int i = 0; i < count; i++) {
447 0 : if (i) printf(",");
448 0 : printf("{\"user_id\":%lld,\"mutual\":%s}",
449 0 : (long long)entries[i].user_id, entries[i].mutual ? "true" : "false");
450 : }
451 0 : printf("]\n");
452 : } else {
453 0 : printf("%-18s %s\n", "user_id", "mutual");
454 0 : for (int i = 0; i < count; i++)
455 0 : printf("%-18lld %s\n",
456 0 : (long long)entries[i].user_id, entries[i].mutual ? "yes" : "no");
457 0 : if (count == 0) printf("(no contacts)\n");
458 : }
459 0 : free(entries);
460 0 : return 0;
461 : }
462 :
463 0 : static int cmd_user_info(const ArgResult *args) {
464 0 : if (!args->peer) { fprintf(stderr, "tg-cli user-info: <peer> required\n"); return 1; }
465 : ApiConfig cfg; MtProtoSession s; Transport t;
466 0 : int brc = session_bringup(args, &cfg, &s, &t);
467 0 : if (brc != 0) return brc;
468 0 : ResolvedPeer r = {0};
469 0 : int rc = domain_resolve_username(&cfg, &s, &t, args->peer, &r);
470 0 : transport_close(&t);
471 0 : if (rc != 0) { fprintf(stderr, "tg-cli user-info: resolve failed\n"); return 1; }
472 0 : if (args->json) {
473 0 : printf("{\"type\":\"%s\",\"id\":%lld,\"username\":\"%s\","
474 : "\"access_hash\":\"%s\"}\n",
475 0 : resolved_kind_name(r.kind), (long long)r.id,
476 0 : r.username, r.have_hash ? "present" : "none");
477 : } else {
478 : char su[64];
479 0 : tty_sanitize(su, sizeof(su), r.username);
480 0 : printf("type: %s\n", resolved_kind_name(r.kind));
481 0 : printf("id: %lld\n", (long long)r.id);
482 0 : if (r.username[0]) printf("username: @%s\n", su);
483 0 : printf("access_hash: %s\n", r.have_hash ? "present" : "none");
484 : }
485 0 : return 0;
486 : }
487 :
488 0 : static int cmd_watch(const ArgResult *args) {
489 : ApiConfig cfg; MtProtoSession s; Transport t;
490 0 : int brc = session_bringup(args, &cfg, &s, &t);
491 0 : if (brc != 0) return brc;
492 0 : signal(SIGINT, on_sigint);
493 0 : signal(SIGPIPE, SIG_IGN);
494 : int64_t peer_filter[WATCH_PEERS_MAX];
495 0 : int peer_filter_n = 0;
496 0 : if (args->watch_peers) {
497 0 : peer_filter_n = watch_peers_resolve(&cfg, &s, &t, args->watch_peers,
498 : peer_filter, WATCH_PEERS_MAX);
499 0 : if (peer_filter_n < 0) { transport_close(&t); return 1; }
500 0 : if (!args->quiet)
501 0 : fprintf(stderr, "watch: filtering to %d peer(s)\n", peer_filter_n);
502 : }
503 0 : UpdatesState state = {0};
504 0 : int loaded = updates_state_load(&state);
505 0 : if (loaded != 0) {
506 0 : if (!args->quiet) fprintf(stderr, "watch: no persisted state, fetching from server\n");
507 0 : if (domain_updates_state(&cfg, &s, &t, &state) != 0) {
508 0 : fprintf(stderr, "tg-cli watch: getState failed\n");
509 0 : transport_close(&t); return 1;
510 : }
511 0 : updates_state_save(&state);
512 : }
513 0 : int interval = args->watch_interval > 0 ? args->watch_interval : 30;
514 0 : if (!args->quiet)
515 0 : fprintf(stderr, "watch: seeded pts=%d qts=%d date=%lld, "
516 : "polling every %ds (SIGINT to quit)\n",
517 0 : state.pts, state.qts, (long long)state.date, interval);
518 0 : int backoff = 0;
519 0 : while (!g_stop) {
520 0 : UpdatesDifference diff = {0};
521 0 : if (domain_updates_difference(&cfg, &s, &t, &state, &diff) != 0) {
522 0 : if (backoff == 0) backoff = WATCH_BACKOFF_INIT_S;
523 0 : else if (backoff < WATCH_BACKOFF_CAP_S)
524 0 : backoff = (backoff * 2 < WATCH_BACKOFF_CAP_S) ? backoff * 2 : WATCH_BACKOFF_CAP_S;
525 0 : fprintf(stderr, "watch: getDifference failed, retrying in %ds\n", backoff);
526 0 : for (int i = 0; i < backoff && !g_stop; i++) sleep(1);
527 0 : continue;
528 : }
529 0 : backoff = 0;
530 0 : state = diff.next_state;
531 0 : updates_state_save(&state);
532 0 : if (args->json) {
533 : char esc[HISTORY_TEXT_MAX * 6 + 1];
534 0 : for (int i = 0; i < diff.new_messages_count; i++) {
535 0 : if (!watch_peer_allowed(peer_filter, peer_filter_n, diff.new_messages[i].peer_id)) continue;
536 0 : json_escape_str(esc, sizeof(esc), diff.new_messages[i].text);
537 0 : if (printf("{\"peer_id\":%lld,\"msg_id\":%d,\"date\":%lld,\"text\":\"%s\"}\n",
538 0 : (long long)diff.new_messages[i].peer_id, diff.new_messages[i].id,
539 0 : (long long)diff.new_messages[i].date, esc) < 0
540 0 : || fflush(stdout) != 0) {
541 0 : if (errno == EPIPE) { g_stop = 1; break; }
542 : }
543 : }
544 : } else {
545 0 : int printed = 0;
546 0 : for (int i = 0; i < diff.new_messages_count; i++) {
547 0 : if (!watch_peer_allowed(peer_filter, peer_filter_n, diff.new_messages[i].peer_id)) continue;
548 : char stext[HISTORY_TEXT_MAX];
549 0 : tty_sanitize(stext, sizeof(stext), diff.new_messages[i].text);
550 0 : if (printf("[%d] %lld %s\n", diff.new_messages[i].id,
551 0 : (long long)diff.new_messages[i].date,
552 0 : diff.new_messages[i].complex ? "(complex \xe2\x80\x94 text not parsed)" : stext) < 0) {
553 0 : if (errno == EPIPE) { g_stop = 1; break; }
554 : }
555 0 : printed++;
556 : }
557 0 : if (!g_stop && printed == 0 && !args->quiet) {
558 0 : if (printf("(no new messages; pts=%d date=%lld)\n",
559 0 : state.pts, (long long)state.date) < 0 && errno == EPIPE)
560 0 : g_stop = 1;
561 : }
562 : }
563 0 : if (!g_stop && fflush(stdout) != 0 && errno == EPIPE) g_stop = 1;
564 0 : for (int i = 0; i < interval && !g_stop; i++) sleep(1);
565 : }
566 0 : transport_close(&t);
567 0 : return 0;
568 : }
569 :
570 0 : static int cmd_download(const ArgResult *args, const AppContext *ctx) {
571 0 : if (!args->peer || args->msg_id <= 0) {
572 0 : fprintf(stderr, "tg-cli download: <peer> and positive <msg_id> required\n");
573 0 : return 1;
574 : }
575 : ApiConfig cfg; MtProtoSession s; Transport t;
576 0 : int brc = session_bringup(args, &cfg, &s, &t);
577 0 : if (brc != 0) return brc;
578 0 : HistoryPeer peer = {0};
579 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &peer) != 0) {
580 0 : transport_close(&t); return 1;
581 : }
582 0 : HistoryEntry entry = {0};
583 0 : int count = 0;
584 0 : int rc = domain_get_history(&cfg, &s, &t, &peer, args->msg_id + 1, 1, &entry, &count);
585 0 : if (rc != 0 || count == 0 || entry.id != args->msg_id) {
586 0 : fprintf(stderr, "tg-cli download: message %d not found\n", args->msg_id);
587 0 : transport_close(&t); return 1;
588 : }
589 0 : if (entry.media != MEDIA_PHOTO && entry.media != MEDIA_DOCUMENT) {
590 0 : fprintf(stderr, "tg-cli download: message %d has no downloadable media\n", args->msg_id);
591 0 : transport_close(&t); return 1;
592 : }
593 0 : if (entry.media_info.access_hash == 0 || entry.media_info.file_reference_len == 0) {
594 0 : fprintf(stderr, "tg-cli download: missing access_hash or file_reference\n");
595 0 : transport_close(&t); return 1;
596 : }
597 : char path_buf[2048];
598 0 : const char *out_path = args->out_path;
599 0 : if (!out_path) {
600 0 : const char *cache = ctx->cache_dir ? ctx->cache_dir : "/tmp";
601 : char dir_buf[1536];
602 0 : snprintf(dir_buf, sizeof(dir_buf), "%s/downloads", cache);
603 0 : fs_mkdir_p(dir_buf, 0700);
604 0 : if (entry.media == MEDIA_DOCUMENT) {
605 0 : const char *fn = entry.media_info.document_filename;
606 0 : if (fn[0]) snprintf(path_buf, sizeof(path_buf), "%s/%s", dir_buf, fn);
607 0 : else snprintf(path_buf, sizeof(path_buf), "%s/doc-%lld",
608 0 : dir_buf, (long long)entry.media_info.document_id);
609 : } else {
610 0 : snprintf(path_buf, sizeof(path_buf), "%s/photo-%lld.jpg",
611 0 : dir_buf, (long long)entry.media_info.photo_id);
612 : }
613 0 : out_path = path_buf;
614 : }
615 0 : rc = domain_download_media_cross_dc(&cfg, &s, &t, &entry.media_info, out_path);
616 0 : transport_close(&t);
617 0 : if (rc != 0) { fprintf(stderr, "tg-cli download: failed (see logs)\n"); return 1; }
618 0 : int64_t media_id = (entry.media == MEDIA_DOCUMENT)
619 0 : ? entry.media_info.document_id : entry.media_info.photo_id;
620 0 : media_index_put(media_id, out_path);
621 0 : if (args->json)
622 0 : printf("{\"saved\":\"%s\",\"kind\":\"%s\",\"id\":%lld}\n",
623 0 : out_path, (entry.media == MEDIA_DOCUMENT) ? "document" : "photo",
624 : (long long)media_id);
625 0 : else if (!args->quiet)
626 0 : printf("saved: %s\n", out_path);
627 0 : return 0;
628 : }
629 :
630 : /* ---- Peer resolution (forward declaration above; definition here) ---- */
631 0 : static int resolve_peer_arg(const ApiConfig *cfg, MtProtoSession *s,
632 : Transport *t, const char *peer_arg,
633 : HistoryPeer *out) {
634 0 : if (!peer_arg || strcmp(peer_arg, "self") == 0) {
635 0 : out->kind = HISTORY_PEER_SELF;
636 0 : return 0;
637 : }
638 0 : ResolvedPeer rp = {0};
639 0 : if (domain_resolve_username(cfg, s, t, peer_arg, &rp) != 0) return -1;
640 0 : switch (rp.kind) {
641 0 : case RESOLVED_KIND_USER: out->kind = HISTORY_PEER_USER; break;
642 0 : case RESOLVED_KIND_CHANNEL: out->kind = HISTORY_PEER_CHANNEL; break;
643 0 : case RESOLVED_KIND_CHAT: out->kind = HISTORY_PEER_CHAT; break;
644 0 : default: return -1;
645 : }
646 0 : out->peer_id = rp.id;
647 0 : out->access_hash = rp.access_hash;
648 0 : return 0;
649 : }
650 :
651 0 : static int cmd_edit(const ArgResult *args) {
652 0 : if (!args->peer || args->msg_id <= 0 || !args->message) {
653 0 : fprintf(stderr, "tg-cli edit: <peer> <msg_id> <text> required\n");
654 0 : return 1;
655 : }
656 : ApiConfig cfg; MtProtoSession s; Transport t;
657 0 : int brc = session_bringup(args, &cfg, &s, &t);
658 0 : if (brc != 0) return brc;
659 :
660 0 : HistoryPeer peer = {0};
661 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &peer) != 0) {
662 0 : transport_close(&t); return 1;
663 : }
664 0 : RpcError err = {0};
665 0 : int rc = domain_edit_message(&cfg, &s, &t, &peer, args->msg_id,
666 0 : args->message, &err);
667 0 : transport_close(&t);
668 0 : if (rc != 0) {
669 0 : fprintf(stderr, "tg-cli edit: failed (%d: %s)\n",
670 : err.error_code, err.error_msg);
671 0 : return 1;
672 : }
673 0 : if (args->json) printf("{\"edited\":%d}\n", args->msg_id);
674 0 : else if (!args->quiet) printf("edited %d\n", args->msg_id);
675 0 : return 0;
676 : }
677 :
678 0 : static int cmd_delete(const ArgResult *args) {
679 0 : if (!args->peer || args->msg_id <= 0) {
680 0 : fprintf(stderr, "tg-cli delete: <peer> <msg_id> required\n");
681 0 : return 1;
682 : }
683 : ApiConfig cfg; MtProtoSession s; Transport t;
684 0 : int brc = session_bringup(args, &cfg, &s, &t);
685 0 : if (brc != 0) return brc;
686 :
687 0 : HistoryPeer peer = {0};
688 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &peer) != 0) {
689 0 : transport_close(&t); return 1;
690 : }
691 0 : RpcError err = {0};
692 0 : int32_t ids[1] = { args->msg_id };
693 0 : int rc = domain_delete_messages(&cfg, &s, &t, &peer, ids, 1,
694 0 : args->revoke, &err);
695 0 : transport_close(&t);
696 0 : if (rc != 0) {
697 0 : fprintf(stderr, "tg-cli delete: failed (%d: %s)\n",
698 : err.error_code, err.error_msg);
699 0 : return 1;
700 : }
701 0 : if (args->json) printf("{\"deleted\":%d,\"revoke\":%s}\n",
702 0 : args->msg_id, args->revoke ? "true" : "false");
703 0 : else if (!args->quiet) printf("deleted %d\n", args->msg_id);
704 0 : return 0;
705 : }
706 :
707 0 : static int cmd_send_file(const ArgResult *args) {
708 0 : if (!args->peer || !args->out_path) {
709 0 : fprintf(stderr, "tg-cli send-file: <peer> <path> required\n");
710 0 : return 1;
711 : }
712 : ApiConfig cfg; MtProtoSession s; Transport t;
713 0 : int brc = session_bringup(args, &cfg, &s, &t);
714 0 : if (brc != 0) return brc;
715 :
716 0 : HistoryPeer peer = {0};
717 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &peer) != 0) {
718 0 : transport_close(&t); return 1;
719 : }
720 0 : RpcError err = {0};
721 0 : int as_photo = domain_path_is_image(args->out_path);
722 0 : int rc = as_photo
723 0 : ? domain_send_photo(&cfg, &s, &t, &peer, args->out_path,
724 0 : args->message, &err)
725 0 : : domain_send_file (&cfg, &s, &t, &peer, args->out_path,
726 0 : args->message, NULL, &err);
727 0 : transport_close(&t);
728 0 : if (rc != 0) {
729 0 : fprintf(stderr, "tg-cli send-file: failed (%d: %s)\n",
730 : err.error_code, err.error_msg);
731 0 : return 1;
732 : }
733 0 : if (args->json) printf("{\"uploaded\":\"%s\",\"kind\":\"%s\"}\n",
734 0 : args->out_path, as_photo ? "photo" : "document");
735 0 : else if (!args->quiet) printf("uploaded %s as %s\n",
736 0 : args->out_path,
737 : as_photo ? "photo" : "document");
738 0 : return 0;
739 : }
740 :
741 0 : static int cmd_forward(const ArgResult *args) {
742 0 : if (!args->peer || !args->peer2 || args->msg_id <= 0) {
743 0 : fprintf(stderr,
744 : "tg-cli forward: <from_peer> <to_peer> <msg_id> required\n");
745 0 : return 1;
746 : }
747 : ApiConfig cfg; MtProtoSession s; Transport t;
748 0 : int brc = session_bringup(args, &cfg, &s, &t);
749 0 : if (brc != 0) return brc;
750 :
751 0 : HistoryPeer from = {0}, to = {0};
752 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &from) != 0
753 0 : || resolve_peer_arg(&cfg, &s, &t, args->peer2, &to) != 0) {
754 0 : transport_close(&t); return 1;
755 : }
756 0 : RpcError err = {0};
757 0 : int32_t ids[1] = { args->msg_id };
758 0 : int rc = domain_forward_messages(&cfg, &s, &t, &from, &to, ids, 1, &err);
759 0 : transport_close(&t);
760 0 : if (rc != 0) {
761 0 : fprintf(stderr, "tg-cli forward: failed (%d: %s)\n",
762 : err.error_code, err.error_msg);
763 0 : return 1;
764 : }
765 0 : if (args->json) printf("{\"forwarded\":%d}\n", args->msg_id);
766 0 : else if (!args->quiet) printf("forwarded %d\n", args->msg_id);
767 0 : return 0;
768 : }
769 :
770 0 : static int cmd_read(const ArgResult *args) {
771 0 : if (!args->peer) {
772 0 : fprintf(stderr, "tg-cli read: <peer> required\n");
773 0 : return 1;
774 : }
775 : ApiConfig cfg; MtProtoSession s; Transport t;
776 0 : int brc = session_bringup(args, &cfg, &s, &t);
777 0 : if (brc != 0) return brc;
778 :
779 0 : HistoryPeer peer = {0};
780 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &peer) != 0) {
781 0 : fprintf(stderr, "tg-cli read: failed to resolve peer '%s'\n",
782 0 : args->peer);
783 0 : transport_close(&t);
784 0 : return 1;
785 : }
786 0 : RpcError err = {0};
787 0 : int rc = domain_mark_read(&cfg, &s, &t, &peer, args->msg_id, &err);
788 0 : transport_close(&t);
789 0 : if (rc != 0) {
790 0 : fprintf(stderr, "tg-cli read: failed (%d: %s)\n",
791 : err.error_code, err.error_msg);
792 0 : return 1;
793 : }
794 0 : if (args->json) printf("{\"read\":true}\n");
795 0 : else if (!args->quiet) printf("marked as read\n");
796 0 : return 0;
797 : }
798 :
799 0 : static int cmd_send(const ArgResult *args) {
800 0 : if (!args->peer) {
801 0 : fprintf(stderr, "tg-cli send: <peer> required\n");
802 0 : return 1;
803 : }
804 0 : const char *msg = args->message;
805 : char stdin_buf[4096];
806 :
807 : /* If no inline message and stdin is a pipe, read it (P8-03 done here). */
808 0 : if ((!msg || !*msg)) {
809 0 : if (isatty(0)) {
810 0 : fprintf(stderr, "tg-cli send: <message> required "
811 : "(or pipe it on stdin)\n");
812 0 : return 1;
813 : }
814 0 : size_t n = fread(stdin_buf, 1, sizeof(stdin_buf) - 1, stdin);
815 0 : if (n == 0) {
816 0 : fprintf(stderr, "tg-cli send: empty stdin\n");
817 0 : return 1;
818 : }
819 0 : stdin_buf[n] = '\0';
820 : /* Strip one trailing newline for convenience. */
821 0 : if (n > 0 && stdin_buf[n - 1] == '\n') stdin_buf[n - 1] = '\0';
822 0 : msg = stdin_buf;
823 : }
824 :
825 : ApiConfig cfg; MtProtoSession s; Transport t;
826 0 : int brc = session_bringup(args, &cfg, &s, &t);
827 0 : if (brc != 0) return brc;
828 :
829 0 : HistoryPeer peer = {0};
830 0 : if (resolve_peer_arg(&cfg, &s, &t, args->peer, &peer) != 0) {
831 0 : fprintf(stderr, "tg-cli send: failed to resolve peer '%s'\n",
832 0 : args->peer);
833 0 : transport_close(&t);
834 0 : return 1;
835 : }
836 :
837 0 : int32_t new_id = 0;
838 0 : RpcError err = {0};
839 0 : int rc = domain_send_message_reply(&cfg, &s, &t, &peer, msg,
840 0 : args->reply_to, &new_id, &err);
841 0 : transport_close(&t);
842 0 : if (rc != 0) {
843 0 : fprintf(stderr, "tg-cli send: failed (%d: %s)\n",
844 : err.error_code, err.error_msg);
845 0 : return 1;
846 : }
847 0 : if (args->json) {
848 0 : printf("{\"sent\":true,\"message_id\":%d}\n", new_id);
849 0 : } else if (!args->quiet) {
850 0 : if (new_id > 0) printf("sent, id=%d\n", new_id);
851 0 : else printf("sent\n");
852 : }
853 0 : return 0;
854 : }
855 :
856 0 : static void print_usage(void) {
857 0 : puts(
858 : "Usage: tg-cli [GLOBAL FLAGS] <subcommand> [ARGS]\n"
859 : "\n"
860 : "Batch-mode Telegram CLI — read and write. Always non-interactive.\n"
861 : "For the interactive REPL/TUI use tg-tui(1).\n"
862 : "\n"
863 : "Read subcommands:\n"
864 : " me (or self) Show own profile (US-05)\n"
865 : " dialogs [--limit N] [--archived] List dialogs (US-04)\n"
866 : " history <peer> [--limit N] [--offset N] [--no-media] Fetch history (US-06)\n"
867 : " search [<peer>] <query> [--limit N] Search messages (US-10)\n"
868 : " contacts List contacts (US-09)\n"
869 : " user-info <peer> User/channel info (US-09)\n"
870 : " watch [--peers X,Y] [--interval N] Watch updates (US-07)\n"
871 : " download <peer> <msg_id> [--out PATH] Download photo/document (US-08)\n"
872 : "\n"
873 : "Write subcommands:\n"
874 : " send <peer> [--reply N] <message> Send a text message (US-12)\n"
875 : " send <peer> --stdin Read message body from stdin\n"
876 : " read <peer> [--max-id N] Mark peer's history as read (US-12)\n"
877 : " edit <peer> <msg_id> <text> Edit a message (US-13)\n"
878 : " delete <peer> <msg_id> [--revoke] Delete a message (US-13)\n"
879 : " forward <from> <to> <msg_id> Forward a message (US-13)\n"
880 : " send-file|upload <peer> <path> [--caption T] Upload a file (US-14)\n"
881 : "\n"
882 : "Session:\n"
883 : " login [--api-id N --api-hash HEX] [--force] First-run config wizard\n"
884 : " Interactive when stdin is a TTY. From a script, pass both\n"
885 : " flags; otherwise the command exits with 1 (never blocks on input).\n"
886 : " --logout Clear persisted session and exit\n"
887 : "\n"
888 : "Global flags:\n"
889 : " --config <path> Use non-default config file\n"
890 : " --json Emit JSON output where supported\n"
891 : " --quiet Suppress informational output\n"
892 : " --help, -h Show this help and exit\n"
893 : " --version, -v Show version and exit\n"
894 : "\n"
895 : "Login flags (for session authentication):\n"
896 : " --phone <number> E.g. +15551234567\n"
897 : " --code <digits> SMS/app code\n"
898 : " --password <pass> 2FA password\n"
899 : "\n"
900 : "Credentials:\n"
901 : " TG_CLI_API_ID / TG_CLI_API_HASH env vars, or\n"
902 : " api_id= / api_hash= in ~/.config/tg-cli/config.ini\n"
903 : " (run 'tg-cli login' to set up config.ini interactively)\n"
904 : "\n"
905 : "Examples:\n"
906 : " tg-cli login --api-id 12345 --api-hash deadbeef... # batch setup\n"
907 : " tg-cli me\n"
908 : " tg-cli dialogs --limit 50\n"
909 : " tg-cli history @friend --limit 20\n"
910 : " tg-cli send @friend 'Hello!'\n"
911 : " echo 'Hi' | tg-cli send @friend\n"
912 : "\n"
913 : "Note: date fields in output are Unix epoch seconds; use 'date -d @$ts' to format.\n"
914 : "See man tg-cli(1) for the full reference.\n"
915 : );
916 0 : }
917 :
918 1 : int main(int argc, char **argv) {
919 1 : platform_normalize_argv(&argc, &argv);
920 1 : g_stdout_is_tty = isatty(STDOUT_FILENO);
921 : AppContext ctx;
922 1 : if (app_bootstrap(&ctx, "tg-cli") != 0) {
923 0 : fprintf(stderr, "tg-cli: bootstrap failed\n");
924 0 : return 1;
925 : }
926 :
927 : /* Drop the session-scoped resolver cache when the user logs out so a
928 : * subsequent login does not see stale @peer → id mappings. */
929 1 : auth_logout_set_cache_flush_cb(resolve_cache_flush);
930 :
931 : /* --logout: invalidate the session server-side, then wipe the local file. */
932 2 : for (int i = 1; i < argc; i++) {
933 1 : if (strcmp(argv[i], "--logout") == 0) {
934 : ApiConfig cfg;
935 : MtProtoSession s;
936 : Transport t;
937 0 : if (credentials_load(&cfg) == 0) {
938 0 : transport_init(&t);
939 0 : mtproto_session_init(&s);
940 0 : int loaded_dc = 0;
941 0 : if (session_store_load(&s, &loaded_dc) == 0) {
942 0 : const DcEndpoint *ep = dc_lookup(loaded_dc);
943 0 : if (ep && transport_connect(&t, ep->host, ep->port) == 0) {
944 0 : t.dc_id = loaded_dc;
945 0 : auth_logout(&cfg, &s, &t);
946 0 : transport_close(&t);
947 : } else {
948 0 : logger_log(LOG_WARN,
949 : "tg-cli: logout: cannot connect to DC%d, clearing local session",
950 : loaded_dc);
951 0 : session_store_clear();
952 : }
953 : } else {
954 0 : session_store_clear();
955 : }
956 : } else {
957 0 : session_store_clear();
958 : }
959 0 : fprintf(stderr, "tg-cli: persisted session cleared.\n");
960 0 : app_shutdown(&ctx);
961 0 : return 0;
962 : }
963 : }
964 :
965 : ArgResult args;
966 1 : int rc = arg_parse(argc, argv, &args);
967 1 : int exit_code = 0;
968 :
969 1 : switch (rc) {
970 0 : case ARG_HELP: print_usage(); break;
971 0 : case ARG_VERSION: arg_print_version(); break;
972 0 : case ARG_ERROR: exit_code = 1; break;
973 1 : case ARG_OK:
974 1 : switch (args.command) {
975 : /* Read commands */
976 0 : case CMD_ME: exit_code = cmd_me(&args); break;
977 0 : case CMD_DIALOGS: exit_code = cmd_dialogs(&args); break;
978 0 : case CMD_HISTORY: exit_code = cmd_history(&args); break;
979 0 : case CMD_SEARCH: exit_code = cmd_search(&args); break;
980 0 : case CMD_CONTACTS: exit_code = cmd_contacts(&args); break;
981 0 : case CMD_USER_INFO: exit_code = cmd_user_info(&args); break;
982 0 : case CMD_WATCH: exit_code = cmd_watch(&args); break;
983 0 : case CMD_DOWNLOAD: exit_code = cmd_download(&args, &ctx); break;
984 : /* Write commands */
985 0 : case CMD_SEND: exit_code = cmd_send(&args); break;
986 0 : case CMD_READ: exit_code = cmd_read(&args); break;
987 0 : case CMD_EDIT: exit_code = cmd_edit(&args); break;
988 0 : case CMD_DELETE: exit_code = cmd_delete(&args); break;
989 0 : case CMD_FORWARD: exit_code = cmd_forward(&args); break;
990 0 : case CMD_SEND_FILE: exit_code = cmd_send_file(&args); break;
991 : /* Session */
992 1 : case CMD_LOGIN:
993 1 : if (args.api_id_str || args.api_hash_str) {
994 0 : exit_code = config_wizard_run_batch(args.api_id_str,
995 : args.api_hash_str,
996 0 : args.force) != 0 ? 1 : 0;
997 : } else {
998 1 : exit_code = config_wizard_run_interactive() != 0 ? 1 : 0;
999 : }
1000 : /* Do NOT call session_bringup for login — config may not exist yet. */
1001 1 : app_shutdown(&ctx);
1002 1 : return exit_code;
1003 0 : case CMD_NONE:
1004 : default:
1005 0 : print_usage(); break;
1006 : }
1007 0 : break;
1008 0 : default:
1009 0 : exit_code = 1; break;
1010 : }
1011 :
1012 0 : app_shutdown(&ctx);
1013 0 : return exit_code;
1014 : }
|