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