Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file main/tg_tui.c
6 : * @brief tg-tui — interactive Telegram client entry point.
7 : *
8 : * V1 is an interactive command shell (readline-backed). Since P5-03/06
9 : * landed, the TUI also links tg-domain-write and exposes send / reply /
10 : * edit / delete / forward / read commands. A full curses-style TUI with
11 : * panes and live redraw is tracked under US-11 v2.
12 : */
13 :
14 : #include "app/bootstrap.h"
15 : #include "app/auth_flow.h"
16 : #include "app/config_wizard.h"
17 : #include "app/credentials.h"
18 : #include "app/dc_config.h"
19 : #include "app/session_store.h"
20 : #include "infrastructure/auth_logout.h"
21 : #include "logger.h"
22 :
23 : #include "readline.h"
24 : #include "platform/terminal.h"
25 : #include "arg_parse.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/contacts.h"
33 : #include "domain/read/search.h"
34 : #include "domain/read/media.h"
35 :
36 : #include "infrastructure/media_index.h"
37 :
38 : #include "platform/path.h"
39 : #include "fs_util.h"
40 :
41 : #include "domain/write/send.h"
42 : #include "domain/write/edit.h"
43 : #include "domain/write/delete.h"
44 : #include "domain/write/forward.h"
45 : #include "domain/write/read_history.h"
46 : #include "domain/write/upload.h"
47 :
48 : #include "tui/app.h"
49 :
50 : #include <stdio.h>
51 : #include <stdlib.h>
52 : #include <string.h>
53 : #include <time.h>
54 :
55 0 : static void fmt_date(char *buf, size_t cap, int64_t ts) {
56 0 : time_t t = (time_t)ts;
57 : struct tm tm;
58 0 : localtime_r(&t, &tm);
59 0 : strftime(buf, cap, "%Y-%m-%d %H:%M", &tm);
60 0 : }
61 :
62 : /* ---- Credential callbacks: batch values fall back to interactive prompts ---- */
63 :
64 : typedef struct {
65 : LineHistory *hist;
66 : const char *phone; /**< from --phone (may be NULL → interactive) */
67 : const char *code; /**< from --code (may be NULL → interactive) */
68 : const char *password; /**< from --password (may be NULL → interactive) */
69 : } PromptCtx;
70 :
71 0 : static int tui_read_line(const char *label, char *out, size_t cap,
72 : LineHistory *hist) {
73 : char prompt[64];
74 0 : snprintf(prompt, sizeof(prompt), "%s: ", label);
75 0 : int n = rl_readline(prompt, out, cap, hist);
76 0 : return (n < 0) ? -1 : 0;
77 : }
78 :
79 0 : static int cb_get_phone(void *u, char *out, size_t cap) {
80 0 : PromptCtx *c = (PromptCtx *)u;
81 0 : if (c->phone) { snprintf(out, cap, "%s", c->phone); return 0; }
82 0 : return tui_read_line("phone (+...)", out, cap, c->hist);
83 : }
84 0 : static int cb_get_code(void *u, char *out, size_t cap) {
85 0 : PromptCtx *c = (PromptCtx *)u;
86 0 : if (c->code) { snprintf(out, cap, "%s", c->code); return 0; }
87 0 : return tui_read_line("code", out, cap, c->hist);
88 : }
89 0 : static int cb_get_password(void *u, char *out, size_t cap) {
90 0 : PromptCtx *c = (PromptCtx *)u;
91 0 : if (c->password) { snprintf(out, cap, "%s", c->password); return 0; }
92 0 : return tui_read_line("2FA password", out, cap, c->hist);
93 : }
94 :
95 : /* ---- Forward label helpers ---- */
96 :
97 : static const char *kind_label(DialogPeerKind k);
98 : static const char *resolved_label(ResolvedKind k);
99 :
100 : /* ---- Commands dispatched from the interactive loop ---- */
101 :
102 0 : static void do_me(const ApiConfig *cfg, MtProtoSession *s, Transport *t) {
103 0 : SelfInfo me = {0};
104 0 : if (domain_get_self(cfg, s, t, &me) != 0) {
105 0 : puts("me: request failed");
106 0 : return;
107 : }
108 0 : printf("id=%lld @%s %s %s +%s premium=%s\n",
109 0 : (long long)me.id,
110 0 : me.username[0] ? me.username : "(none)",
111 : me.first_name, me.last_name,
112 0 : me.phone[0] ? me.phone : "(hidden)",
113 0 : me.is_premium ? "yes" : "no");
114 : }
115 :
116 0 : static void do_dialogs(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
117 : int limit) {
118 0 : if (limit <= 0 || limit > 100) limit = 20;
119 0 : DialogEntry entries[100] = {0};
120 0 : int count = 0;
121 0 : if (domain_get_dialogs(cfg, s, t, limit, 0, entries, &count, NULL) != 0) {
122 0 : puts("dialogs: request failed");
123 0 : return;
124 : }
125 0 : printf("%-8s %6s %-32s %s\n", "type", "unread", "title", "@username / id");
126 0 : for (int i = 0; i < count; i++) {
127 0 : const char *title = entries[i].title[0] ? entries[i].title : "(no title)";
128 0 : if (entries[i].username[0]) {
129 0 : printf("%-8s %6d %-32s @%s\n",
130 : kind_label(entries[i].kind),
131 0 : entries[i].unread_count, title, entries[i].username);
132 : } else {
133 0 : printf("%-8s %6d %-32s %lld\n",
134 : kind_label(entries[i].kind),
135 : entries[i].unread_count, title,
136 0 : (long long)entries[i].peer_id);
137 : }
138 : }
139 : }
140 :
141 0 : static const char *kind_label(DialogPeerKind k) {
142 0 : switch (k) {
143 0 : case DIALOG_PEER_USER: return "user";
144 0 : case DIALOG_PEER_CHAT: return "chat";
145 0 : case DIALOG_PEER_CHANNEL: return "channel";
146 0 : default: return "unknown";
147 : }
148 : }
149 :
150 0 : static const char *resolved_label(ResolvedKind k) {
151 0 : switch (k) {
152 0 : case RESOLVED_KIND_USER: return "user";
153 0 : case RESOLVED_KIND_CHAT: return "chat";
154 0 : case RESOLVED_KIND_CHANNEL: return "channel";
155 0 : default: return "unknown";
156 : }
157 : }
158 :
159 0 : static int is_numeric_id(const char *s) {
160 0 : if (!s || !*s) return 0;
161 0 : const char *p = (*s == '-') ? s + 1 : s;
162 0 : if (!*p) return 0;
163 0 : for (; *p; p++) if (*p < '0' || *p > '9') return 0;
164 0 : return 1;
165 : }
166 :
167 0 : static int resolve_numeric_peer(const ApiConfig *cfg, MtProtoSession *s,
168 : Transport *t, int64_t peer_id,
169 : HistoryPeer *out) {
170 0 : DialogEntry de = {0};
171 0 : if (domain_dialogs_find_by_id(peer_id, &de) != 0) {
172 : DialogEntry tmp[200];
173 0 : int tc = 0;
174 0 : domain_get_dialogs(cfg, s, t, 200, 0, tmp, &tc, NULL);
175 0 : if (domain_dialogs_find_by_id(peer_id, &de) != 0) return -1;
176 : }
177 0 : switch (de.kind) {
178 0 : case DIALOG_PEER_USER: out->kind = HISTORY_PEER_USER; break;
179 0 : case DIALOG_PEER_CHAT: out->kind = HISTORY_PEER_CHAT; break;
180 0 : case DIALOG_PEER_CHANNEL: out->kind = HISTORY_PEER_CHANNEL; break;
181 0 : default: return -1;
182 : }
183 0 : out->peer_id = de.peer_id;
184 0 : out->access_hash = de.access_hash;
185 0 : return 0;
186 : }
187 :
188 0 : static int resolve_history_peer(const ApiConfig *cfg, MtProtoSession *s,
189 : Transport *t, const char *arg,
190 : HistoryPeer *out) {
191 0 : if (!arg || !*arg || !strcmp(arg, "self")) {
192 0 : out->kind = HISTORY_PEER_SELF; out->peer_id = 0; out->access_hash = 0;
193 0 : return 0;
194 : }
195 0 : if (is_numeric_id(arg)) {
196 0 : int64_t pid = (int64_t)strtoll(arg, NULL, 10);
197 0 : return resolve_numeric_peer(cfg, s, t, pid, out);
198 : }
199 0 : ResolvedPeer rp = {0};
200 0 : if (domain_resolve_username(cfg, s, t, arg, &rp) != 0) return -1;
201 0 : switch (rp.kind) {
202 0 : case RESOLVED_KIND_USER: out->kind = HISTORY_PEER_USER; break;
203 0 : case RESOLVED_KIND_CHANNEL: out->kind = HISTORY_PEER_CHANNEL; break;
204 0 : case RESOLVED_KIND_CHAT: out->kind = HISTORY_PEER_CHAT; break;
205 0 : default: return -1;
206 : }
207 0 : out->peer_id = rp.id;
208 0 : out->access_hash = rp.access_hash;
209 0 : if ((out->kind == HISTORY_PEER_USER || out->kind == HISTORY_PEER_CHANNEL)
210 0 : && !rp.have_hash) return -1;
211 0 : return 0;
212 : }
213 :
214 0 : static void do_history_any(const ApiConfig *cfg, MtProtoSession *s,
215 : Transport *t, const char *arg, int limit) {
216 0 : if (limit <= 0 || limit > 100) limit = 20;
217 0 : HistoryPeer peer = {0};
218 0 : if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
219 0 : printf("history: could not resolve '%s'\n", arg ? arg : "self");
220 0 : return;
221 : }
222 0 : HistoryEntry entries[100] = {0};
223 0 : int count = 0;
224 0 : if (domain_get_history(cfg, s, t, &peer, 0, limit, entries, &count) != 0) {
225 0 : puts("history: request failed");
226 0 : return;
227 : }
228 0 : for (int i = 0; i < count; i++) {
229 0 : char dstr[20]; fmt_date(dstr, sizeof(dstr), entries[i].date);
230 0 : printf("[%d] %s %s %s\n",
231 0 : entries[i].id, entries[i].out ? ">" : "<",
232 0 : dstr, entries[i].text);
233 : }
234 0 : if (count == 0) puts("(no messages)");
235 : }
236 :
237 0 : static void do_contacts(const ApiConfig *cfg, MtProtoSession *s, Transport *t) {
238 0 : ContactEntry entries[64] = {0};
239 0 : int count = 0;
240 0 : if (domain_get_contacts(cfg, s, t, entries, 64, &count) != 0) {
241 0 : puts("contacts: request failed");
242 0 : return;
243 : }
244 0 : printf("%-12s %-30s %-20s %s\n",
245 : "user_id", "name", "username", "mutual");
246 0 : for (int i = 0; i < count; i++) {
247 0 : printf("%-12lld %-30s %-20s %s\n",
248 0 : (long long)entries[i].user_id,
249 0 : entries[i].name,
250 0 : entries[i].username,
251 0 : entries[i].mutual ? "yes" : "no");
252 : }
253 0 : if (count == 0) puts("(no contacts)");
254 : }
255 :
256 0 : static void do_info(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
257 : const char *peer) {
258 0 : if (!peer || !*peer) { puts("usage: info <@name|numeric_id>"); return; }
259 0 : ResolvedPeer r = {0};
260 : int rc;
261 0 : if (is_numeric_id(peer)) {
262 0 : int64_t pid = (int64_t)strtoll(peer, NULL, 10);
263 0 : HistoryPeer hp = {0};
264 0 : rc = resolve_numeric_peer(cfg, s, t, pid, &hp);
265 0 : if (rc == 0) {
266 0 : r.id = hp.peer_id;
267 0 : r.access_hash = hp.access_hash;
268 0 : r.have_hash = (hp.access_hash != 0) ? 1 : 0;
269 : /* map kind */
270 0 : switch (hp.kind) {
271 0 : case HISTORY_PEER_USER: r.kind = RESOLVED_KIND_USER; break;
272 0 : case HISTORY_PEER_CHAT: r.kind = RESOLVED_KIND_CHAT; break;
273 0 : case HISTORY_PEER_CHANNEL: r.kind = RESOLVED_KIND_CHANNEL; break;
274 0 : default: r.kind = RESOLVED_KIND_UNKNOWN; break;
275 : }
276 : /* Fill title/username from dialogs cache */
277 0 : DialogEntry de = {0};
278 0 : if (domain_dialogs_find_by_id(pid, &de) == 0) {
279 0 : snprintf(r.title, sizeof(r.title), "%s", de.title);
280 0 : snprintf(r.username, sizeof(r.username), "%s", de.username);
281 : }
282 : }
283 : } else {
284 0 : rc = domain_resolve_username(cfg, s, t, peer, &r);
285 : }
286 0 : if (rc != 0) { puts("info: resolve failed"); return; }
287 0 : printf("type: %s\nid: %lld\n",
288 0 : resolved_label(r.kind), (long long)r.id);
289 0 : if (r.title[0]) printf("name: %s\n", r.title);
290 0 : if (r.username[0]) printf("username: @%s\n", r.username);
291 0 : printf("access_hash: %s\n", r.have_hash ? "present" : "none");
292 : }
293 :
294 : /* Returns 1 if the token looks like a peer specifier:
295 : * @username, a numeric id, or the literal "self". */
296 0 : static int is_peer_token(const char *tok) {
297 0 : if (!tok || !*tok) return 0;
298 0 : if (*tok == '@') return tok[1] != '\0';
299 0 : if (!strcmp(tok, "self")) return 1;
300 : /* Pure numeric (possibly negative) → peer id */
301 0 : const char *p = tok;
302 0 : if (*p == '-') p++;
303 0 : if (!*p) return 0;
304 0 : while (*p >= '0' && *p <= '9') p++;
305 0 : return (*p == '\0');
306 : }
307 :
308 0 : static void do_search(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
309 : char *arg) {
310 0 : if (!arg || !*arg) {
311 0 : puts("usage: search [<peer>] <query>\n"
312 : " <peer> @username, numeric id, or 'self' (omit for global search)");
313 0 : return;
314 : }
315 :
316 : /* Split arg on the first whitespace to check for an optional peer token. */
317 0 : char peer_buf[128] = "";
318 0 : const char *query = arg;
319 0 : char *space = arg;
320 0 : while (*space && *space != ' ' && *space != '\t') space++;
321 0 : if (*space) {
322 : /* There is a second token — check if the first is a peer. */
323 0 : size_t pn = (size_t)(space - arg);
324 0 : char first[128] = "";
325 0 : if (pn < sizeof(first)) { memcpy(first, arg, pn); first[pn] = '\0'; }
326 0 : if (is_peer_token(first)) {
327 0 : memcpy(peer_buf, first, pn + 1);
328 0 : query = space;
329 0 : while (*query == ' ' || *query == '\t') query++;
330 : }
331 : }
332 :
333 0 : if (!*query) {
334 0 : puts("usage: search [<peer>] <query>");
335 0 : return;
336 : }
337 :
338 0 : HistoryEntry e[20] = {0};
339 0 : int n = 0;
340 :
341 0 : if (peer_buf[0]) {
342 : /* Per-peer search */
343 0 : HistoryPeer peer = {0};
344 0 : if (resolve_history_peer(cfg, s, t, peer_buf, &peer) != 0) {
345 0 : printf("search: cannot resolve '%s'\n", peer_buf);
346 0 : return;
347 : }
348 0 : if (domain_search_peer(cfg, s, t, &peer, query, 20, e, &n) != 0) {
349 0 : puts("search: request failed");
350 0 : return;
351 : }
352 : } else {
353 : /* Global search */
354 0 : if (domain_search_global(cfg, s, t, query, 20, e, &n) != 0) {
355 0 : puts("search: request failed");
356 0 : return;
357 : }
358 : }
359 :
360 0 : for (int i = 0; i < n; i++) {
361 0 : char dstr[20]; fmt_date(dstr, sizeof(dstr), e[i].date);
362 0 : printf("%d %s %s\n", e[i].id, dstr, e[i].text);
363 : }
364 0 : if (n == 0) puts("(no matches)");
365 : }
366 :
367 : /* ---- Write commands ---- */
368 :
369 : /* Split "arg" on the first whitespace. Returns pointer to the remainder
370 : * (may be empty). The first token is NUL-terminated in place. */
371 0 : static char *split_rest(char *arg) {
372 0 : while (*arg && *arg != ' ' && *arg != '\t') arg++;
373 0 : if (!*arg) return arg;
374 0 : *arg++ = '\0';
375 0 : while (*arg == ' ' || *arg == '\t') arg++;
376 0 : return arg;
377 : }
378 :
379 0 : static void do_send(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
380 : char *arg) {
381 0 : if (!arg || !*arg) { puts("usage: send <peer> <text>"); return; }
382 0 : char *text = split_rest(arg);
383 0 : if (!*text) { puts("usage: send <peer> <text>"); return; }
384 :
385 0 : HistoryPeer peer = {0};
386 0 : if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
387 0 : printf("send: cannot resolve '%s'\n", arg);
388 0 : return;
389 : }
390 0 : int32_t new_id = 0;
391 0 : RpcError err = {0};
392 0 : if (domain_send_message(cfg, s, t, &peer, text, &new_id, &err) != 0) {
393 0 : printf("send: failed (%d: %s)\n", err.error_code, err.error_msg);
394 0 : return;
395 : }
396 0 : if (new_id > 0) printf("sent, id=%d\n", new_id);
397 0 : else puts("sent");
398 : }
399 :
400 0 : static void do_reply(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
401 : char *arg) {
402 0 : if (!arg || !*arg) { puts("usage: reply <peer> <msg_id> <text>"); return; }
403 0 : char *rest = split_rest(arg);
404 0 : if (!*rest) { puts("usage: reply <peer> <msg_id> <text>"); return; }
405 0 : char *text = split_rest(rest);
406 0 : if (!*text) { puts("usage: reply <peer> <msg_id> <text>"); return; }
407 0 : int32_t mid = atoi(rest);
408 0 : if (mid <= 0) { puts("reply: <msg_id> must be positive"); return; }
409 :
410 0 : HistoryPeer peer = {0};
411 0 : if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
412 0 : printf("reply: cannot resolve '%s'\n", arg); return;
413 : }
414 0 : int32_t new_id = 0; RpcError err = {0};
415 0 : if (domain_send_message_reply(cfg, s, t, &peer, text, mid,
416 : &new_id, &err) != 0) {
417 0 : printf("reply: failed (%d: %s)\n", err.error_code, err.error_msg);
418 0 : return;
419 : }
420 0 : if (new_id > 0) printf("sent, id=%d (reply to %d)\n", new_id, mid);
421 0 : else printf("sent (reply to %d)\n", mid);
422 : }
423 :
424 0 : static void do_edit(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
425 : char *arg) {
426 0 : if (!arg || !*arg) { puts("usage: edit <peer> <msg_id> <text>"); return; }
427 0 : char *rest = split_rest(arg);
428 0 : if (!*rest) { puts("usage: edit <peer> <msg_id> <text>"); return; }
429 0 : char *text = split_rest(rest);
430 0 : if (!*text) { puts("usage: edit <peer> <msg_id> <text>"); return; }
431 0 : int32_t mid = atoi(rest);
432 0 : if (mid <= 0) { puts("edit: <msg_id> must be positive"); return; }
433 :
434 0 : HistoryPeer peer = {0};
435 0 : if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
436 0 : printf("edit: cannot resolve '%s'\n", arg); return;
437 : }
438 0 : RpcError err = {0};
439 0 : if (domain_edit_message(cfg, s, t, &peer, mid, text, &err) != 0) {
440 0 : printf("edit: failed (%d: %s)\n", err.error_code, err.error_msg);
441 0 : return;
442 : }
443 0 : printf("edited %d\n", mid);
444 : }
445 :
446 0 : static void do_delete(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
447 : char *arg) {
448 0 : if (!arg || !*arg) { puts("usage: delete <peer> <msg_id> [revoke]"); return; }
449 0 : char *rest = split_rest(arg);
450 0 : if (!*rest) { puts("usage: delete <peer> <msg_id> [revoke]"); return; }
451 0 : int revoke = 0;
452 0 : char *id_tok = rest;
453 0 : char *extra = split_rest(rest);
454 0 : if (*extra && !strcmp(extra, "revoke")) revoke = 1;
455 0 : int32_t mid = atoi(id_tok);
456 0 : if (mid <= 0) { puts("delete: <msg_id> must be positive"); return; }
457 :
458 0 : HistoryPeer peer = {0};
459 0 : if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
460 0 : printf("delete: cannot resolve '%s'\n", arg); return;
461 : }
462 0 : int32_t ids[1] = { mid };
463 0 : RpcError err = {0};
464 0 : if (domain_delete_messages(cfg, s, t, &peer, ids, 1, revoke, &err) != 0) {
465 0 : printf("delete: failed (%d: %s)\n", err.error_code, err.error_msg);
466 0 : return;
467 : }
468 0 : printf("deleted %d%s\n", mid, revoke ? " (revoke)" : "");
469 : }
470 :
471 0 : static void do_forward(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
472 : char *arg) {
473 0 : if (!arg || !*arg) { puts("usage: forward <from> <to> <msg_id>"); return; }
474 0 : char *rest = split_rest(arg);
475 0 : if (!*rest) { puts("usage: forward <from> <to> <msg_id>"); return; }
476 0 : char *id_tok = split_rest(rest);
477 0 : if (!*id_tok) { puts("usage: forward <from> <to> <msg_id>"); return; }
478 0 : int32_t mid = atoi(id_tok);
479 0 : if (mid <= 0) { puts("forward: <msg_id> must be positive"); return; }
480 :
481 0 : HistoryPeer from = {0}, to = {0};
482 0 : if (resolve_history_peer(cfg, s, t, arg, &from) != 0
483 0 : || resolve_history_peer(cfg, s, t, rest, &to) != 0) {
484 0 : puts("forward: cannot resolve peers"); return;
485 : }
486 0 : int32_t ids[1] = { mid };
487 0 : RpcError err = {0};
488 0 : if (domain_forward_messages(cfg, s, t, &from, &to, ids, 1, &err) != 0) {
489 0 : printf("forward: failed (%d: %s)\n", err.error_code, err.error_msg);
490 0 : return;
491 : }
492 0 : printf("forwarded %d\n", mid);
493 : }
494 :
495 0 : static void do_upload(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
496 : char *arg) {
497 0 : if (!arg || !*arg) { puts("usage: upload <peer> <path> [caption]"); return; }
498 0 : char *path = split_rest(arg);
499 0 : if (!*path) { puts("usage: upload <peer> <path> [caption]"); return; }
500 0 : char *caption = split_rest(path);
501 0 : HistoryPeer peer = {0};
502 0 : if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
503 0 : printf("upload: cannot resolve '%s'\n", arg); return;
504 : }
505 0 : RpcError err = {0};
506 0 : int as_photo = domain_path_is_image(path);
507 0 : int rc = as_photo
508 0 : ? domain_send_photo(cfg, s, t, &peer, path,
509 0 : *caption ? caption : NULL, &err)
510 0 : : domain_send_file (cfg, s, t, &peer, path,
511 0 : *caption ? caption : NULL, NULL, &err);
512 0 : if (rc != 0) {
513 0 : printf("upload: failed (%d: %s)\n", err.error_code, err.error_msg);
514 0 : return;
515 : }
516 0 : printf("uploaded %s as %s\n", path, as_photo ? "photo" : "document");
517 : }
518 :
519 0 : static void do_download(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
520 : char *arg) {
521 : /* Syntax: download <peer> <msg_id> [out] */
522 0 : if (!arg || !*arg) {
523 0 : puts("usage: download <peer> <msg_id> [out]");
524 0 : return;
525 : }
526 0 : char *rest = split_rest(arg);
527 0 : if (!*rest) { puts("usage: download <peer> <msg_id> [out]"); return; }
528 0 : char *out_arg = split_rest(rest);
529 :
530 0 : int32_t mid = atoi(rest);
531 0 : if (mid <= 0) { puts("download: <msg_id> must be positive"); return; }
532 :
533 0 : HistoryPeer peer = {0};
534 0 : if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
535 0 : printf("download: cannot resolve '%s'\n", arg);
536 0 : return;
537 : }
538 :
539 : /* Fetch the single message using offset_id = msg_id + 1, limit = 1. */
540 0 : HistoryEntry entry = {0};
541 0 : int count = 0;
542 0 : if (domain_get_history(cfg, s, t, &peer, mid + 1, 1, &entry, &count) != 0
543 0 : || count == 0 || entry.id != mid) {
544 0 : printf("download: message %d not found in this peer\n", mid);
545 0 : return;
546 : }
547 0 : if (entry.media != MEDIA_PHOTO && entry.media != MEDIA_DOCUMENT) {
548 0 : printf("download: message %d has no downloadable photo/document "
549 0 : "(media kind=%d)\n", mid, (int)entry.media);
550 0 : return;
551 : }
552 0 : if (entry.media_info.access_hash == 0
553 0 : || entry.media_info.file_reference_len == 0) {
554 0 : puts("download: missing access_hash or file_reference");
555 0 : return;
556 : }
557 :
558 : /* Compose output path: explicit [out] > default cache path. */
559 : char path_buf[2048];
560 0 : const char *out_path = (*out_arg) ? out_arg : NULL;
561 0 : if (!out_path) {
562 0 : const char *cache = platform_cache_dir();
563 0 : if (!cache) cache = "/tmp";
564 : char dir_buf[1536];
565 0 : snprintf(dir_buf, sizeof(dir_buf), "%s/tg-cli/downloads", cache);
566 0 : fs_mkdir_p(dir_buf, 0700);
567 0 : if (entry.media == MEDIA_DOCUMENT) {
568 0 : const char *fn = entry.media_info.document_filename;
569 0 : if (fn[0]) {
570 0 : snprintf(path_buf, sizeof(path_buf), "%s/%s", dir_buf, fn);
571 : } else {
572 0 : snprintf(path_buf, sizeof(path_buf), "%s/doc-%lld",
573 0 : dir_buf, (long long)entry.media_info.document_id);
574 : }
575 : } else {
576 0 : snprintf(path_buf, sizeof(path_buf), "%s/photo-%lld.jpg",
577 0 : dir_buf, (long long)entry.media_info.photo_id);
578 : }
579 0 : out_path = path_buf;
580 : }
581 :
582 0 : if (domain_download_media_cross_dc(cfg, s, t, &entry.media_info,
583 : out_path) != 0) {
584 0 : puts("download: failed (see logs)");
585 0 : return;
586 : }
587 :
588 : /* Record in the media index so `history` can show inline paths. */
589 0 : int64_t media_id = (entry.media == MEDIA_DOCUMENT)
590 : ? entry.media_info.document_id
591 0 : : entry.media_info.photo_id;
592 0 : if (media_index_put(media_id, out_path) != 0) {
593 0 : puts("download: warning: failed to update media index");
594 : }
595 :
596 0 : printf("saved: %s\n", out_path);
597 : }
598 :
599 0 : static void do_read(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
600 : const char *arg) {
601 0 : if (!arg || !*arg) { puts("usage: read <peer>"); return; }
602 0 : HistoryPeer peer = {0};
603 0 : if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
604 0 : printf("read: cannot resolve '%s'\n", arg); return;
605 : }
606 0 : RpcError err = {0};
607 0 : if (domain_mark_read(cfg, s, t, &peer, 0, &err) != 0) {
608 0 : printf("read: failed (%d: %s)\n", err.error_code, err.error_msg);
609 0 : return;
610 : }
611 0 : puts("marked as read");
612 : }
613 :
614 0 : static void do_poll(const ApiConfig *cfg, MtProtoSession *s, Transport *t) {
615 0 : UpdatesState st = {0};
616 0 : if (domain_updates_state(cfg, s, t, &st) != 0) {
617 0 : puts("poll: getState failed");
618 0 : return;
619 : }
620 0 : UpdatesDifference diff = {0};
621 0 : if (domain_updates_difference(cfg, s, t, &st, &diff) != 0) {
622 0 : puts("poll: getDifference failed");
623 0 : return;
624 : }
625 0 : printf("pts=%d new_messages=%d empty=%d\n",
626 : diff.next_state.pts, diff.new_messages_count, diff.is_empty);
627 : }
628 :
629 : /* ---- Help ---- */
630 :
631 0 : static void print_help(void) {
632 0 : puts(
633 : "Commands (accept '/' prefix too):\n"
634 : "\n"
635 : "Read commands:\n"
636 : " me, self Show own profile\n"
637 : " dialogs, list [N] List up to N dialogs (default 20)\n"
638 : " history [<peer>] [N] Saved Messages by default, or <peer>\n"
639 : " contacts List my contacts\n"
640 : " info <@peer> Resolve peer info\n"
641 : " search [<peer>] <query> Message search: per-peer or global (top 20)\n"
642 : " poll One-shot updates.getDifference\n"
643 : " read <peer> Mark peer's history as read\n"
644 : " download <peer> <msg_id> [out] Download photo or document from message\n"
645 : "\n"
646 : "Write commands:\n"
647 : " send <peer> <text> Send a text message\n"
648 : " reply <peer> <msg_id> <text> Send as a reply to msg_id\n"
649 : " edit <peer> <msg_id> <text> Edit a previously sent message\n"
650 : " delete, del <peer> <msg_id> [revoke] Delete a message\n"
651 : " forward, fwd <from> <to> <msg_id> Forward one message\n"
652 : " upload <peer> <path> [caption] Upload a file (document)\n"
653 : "\n"
654 : "Session:\n"
655 : " help, ? Show this help\n"
656 : " quit, exit, :q Leave the TUI (Ctrl-D also exits)\n"
657 : "\n"
658 : "Launch flags (pass on the command line, not inside the REPL):\n"
659 : " --help, -h Show this help and exit\n"
660 : " --version, -v Show version and exit\n"
661 : " --tui Curses-style three-pane UI instead of REPL\n"
662 : " --phone <number> Pre-fill login phone (E.164)\n"
663 : " --code <digits> Pre-fill SMS/app code\n"
664 : " --password <pass> Pre-fill 2FA password\n"
665 : " --logout Clear persisted session and exit\n"
666 : " login [--api-id N --api-hash HEX] [--force] First-run config wizard\n"
667 : " Interactive when stdin is a TTY. From a script, pass both\n"
668 : " flags; otherwise the command exits with 1 (never blocks on input).\n"
669 : "\n"
670 : "See man tg-tui(1) for the full reference.\n"
671 : );
672 0 : }
673 :
674 : /* ---- REPL ---- */
675 :
676 0 : static int repl(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
677 : LineHistory *hist) {
678 : char line[512];
679 0 : for (;;) {
680 0 : int n = rl_readline("tg> ", line, sizeof(line), hist);
681 0 : if (n < 0) return 0; /* EOF / Ctrl-C */
682 0 : if (n == 0) continue;
683 :
684 0 : rl_history_add(hist, line);
685 :
686 : /* Parse the very small command language in-place. */
687 0 : char *cmd = line;
688 0 : while (*cmd == ' ' || *cmd == '\t') cmd++;
689 0 : if (*cmd == '\0') continue;
690 :
691 0 : char *arg = cmd;
692 0 : while (*arg && *arg != ' ' && *arg != '\t') arg++;
693 0 : if (*arg) { *arg++ = '\0'; while (*arg == ' ') arg++; }
694 :
695 : /* Allow an optional leading '/' for IRC-style commands. */
696 0 : if (*cmd == '/') cmd++;
697 :
698 0 : if (!strcmp(cmd, "quit") || !strcmp(cmd, "exit") ||
699 0 : !strcmp(cmd, ":q")) return 0;
700 0 : if (!strcmp(cmd, "help") || !strcmp(cmd, "?")) { print_help(); continue; }
701 0 : if (!strcmp(cmd, "me") || !strcmp(cmd, "self")) { do_me(cfg, s, t); continue; }
702 0 : if (!strcmp(cmd, "dialogs") || !strcmp(cmd, "list")) {
703 0 : do_dialogs(cfg, s, t, atoi(arg));
704 0 : continue;
705 : }
706 0 : if (!strcmp(cmd, "history")) {
707 : /* Accept "history <peer> <N>" or "history <N>" or "history". */
708 0 : char peer[128] = "";
709 0 : int lim = 0;
710 0 : if (*arg) {
711 0 : char *space = strpbrk(arg, " \t");
712 0 : if (space) {
713 0 : size_t pn = (size_t)(space - arg);
714 0 : if (pn >= sizeof(peer)) pn = sizeof(peer) - 1;
715 0 : memcpy(peer, arg, pn);
716 0 : peer[pn] = '\0';
717 0 : while (*space == ' ' || *space == '\t') space++;
718 0 : lim = atoi(space);
719 : } else {
720 0 : if (*arg >= '0' && *arg <= '9') {
721 0 : lim = atoi(arg);
722 : } else {
723 0 : size_t an = strlen(arg);
724 0 : if (an >= sizeof(peer)) an = sizeof(peer) - 1;
725 0 : memcpy(peer, arg, an);
726 0 : peer[an] = '\0';
727 : }
728 : }
729 : }
730 0 : do_history_any(cfg, s, t, peer[0] ? peer : NULL, lim);
731 0 : continue;
732 : }
733 0 : if (!strcmp(cmd, "contacts")) { do_contacts(cfg, s, t); continue; }
734 0 : if (!strcmp(cmd, "info")) { do_info(cfg, s, t, arg); continue; }
735 0 : if (!strcmp(cmd, "search")) { do_search(cfg, s, t, arg); continue; } /* arg is mutable */
736 0 : if (!strcmp(cmd, "poll")) { do_poll(cfg, s, t); continue; }
737 0 : if (!strcmp(cmd, "send")) { do_send(cfg, s, t, arg); continue; }
738 0 : if (!strcmp(cmd, "reply")) { do_reply(cfg, s, t, arg); continue; }
739 0 : if (!strcmp(cmd, "edit")) { do_edit(cfg, s, t, arg); continue; }
740 0 : if (!strcmp(cmd, "delete") || !strcmp(cmd, "del")) {
741 0 : do_delete(cfg, s, t, arg); continue;
742 : }
743 0 : if (!strcmp(cmd, "forward") || !strcmp(cmd, "fwd")) {
744 0 : do_forward(cfg, s, t, arg); continue;
745 : }
746 0 : if (!strcmp(cmd, "read")) { do_read(cfg, s, t, arg); continue; }
747 0 : if (!strcmp(cmd, "upload")) { do_upload(cfg, s, t, arg); continue; }
748 0 : if (!strcmp(cmd, "download")) { do_download(cfg, s, t, arg); continue; }
749 :
750 0 : printf("unknown command: %s (try 'help')\n", cmd);
751 : }
752 : }
753 :
754 :
755 : /* Map a DialogEntry to a HistoryPeer. Legacy groups don't need an
756 : * access_hash; users/channels do, and since TUI-08 the DialogEntry
757 : * carries it when the server sent one. */
758 0 : static int dialog_to_history_peer(const DialogEntry *d, HistoryPeer *out) {
759 0 : memset(out, 0, sizeof(*out));
760 0 : switch (d->kind) {
761 0 : case DIALOG_PEER_CHAT:
762 0 : out->kind = HISTORY_PEER_CHAT;
763 0 : out->peer_id = d->peer_id;
764 0 : return 0;
765 0 : case DIALOG_PEER_USER:
766 0 : if (!d->have_access_hash) return -1;
767 0 : out->kind = HISTORY_PEER_USER;
768 0 : out->peer_id = d->peer_id;
769 0 : out->access_hash = d->access_hash;
770 0 : return 0;
771 0 : case DIALOG_PEER_CHANNEL:
772 0 : if (!d->have_access_hash) return -1;
773 0 : out->kind = HISTORY_PEER_CHANNEL;
774 0 : out->peer_id = d->peer_id;
775 0 : out->access_hash = d->access_hash;
776 0 : return 0;
777 0 : default:
778 0 : return -1;
779 : }
780 : }
781 :
782 : /* Curses-style TUI loop (US-11 v2). Returns 0 on clean exit. */
783 0 : static int run_tui_loop(const ApiConfig *cfg,
784 : MtProtoSession *s, Transport *t) {
785 0 : int rows = terminal_rows(); if (rows < 3) rows = 24;
786 0 : int cols = terminal_cols(); if (cols < 40) cols = 80;
787 :
788 : TuiApp app;
789 0 : if (tui_app_init(&app, rows, cols) != 0) {
790 0 : fprintf(stderr, "tg-tui: cannot initialize TUI (size %dx%d)\n",
791 : rows, cols);
792 0 : return 1;
793 : }
794 :
795 : /* Prime the dialog list. Failure is non-fatal; the user sees the
796 : * empty-placeholder and can still quit. */
797 0 : if (dialog_pane_refresh(&app.dialogs, cfg, s, t) != 0) {
798 0 : status_row_set_message(&app.status, "dialogs: load failed");
799 : }
800 : /* dialog_pane_set_entries resets the list_view but forgets the
801 : * viewport height set in tui_app_init; restore it. */
802 0 : app.dialogs.lv.rows_visible = app.layout.dialogs.rows;
803 :
804 0 : RAII_TERM_RAW TermRawState *raw = terminal_raw_enter();
805 0 : if (!raw) {
806 0 : fprintf(stderr, "tg-tui: cannot enter raw mode\n");
807 0 : tui_app_free(&app);
808 0 : return 1;
809 : }
810 : /* Ensure SIGTERM / SIGHUP / SIGINT restore the terminal before exiting,
811 : * even if the default handler bypasses our RAII cleanup. */
812 0 : terminal_install_cleanup_handlers(raw);
813 0 : screen_cursor_visible(&app.screen, 0);
814 :
815 0 : terminal_enable_resize_notifications();
816 :
817 : /* Prime updates polling. If getState fails (eg. offline or server hiccup)
818 : * we silently skip live polling — the pane UI still works. */
819 0 : UpdatesState upd_state = {0};
820 0 : int upd_active = (domain_updates_state(cfg, s, t, &upd_state) == 0);
821 :
822 0 : tui_app_paint(&app);
823 0 : screen_flip(&app.screen);
824 :
825 : /* Poll cadence for updates.getDifference. 5 seconds is low enough to
826 : * feel live and high enough not to rate-limit ourselves. */
827 0 : const int POLL_INTERVAL_MS = 5000;
828 :
829 0 : int rc = 0;
830 0 : for (;;) {
831 0 : int ready = terminal_wait_key(POLL_INTERVAL_MS);
832 :
833 : /* SIGWINCH interrupts poll(): pick up the new size and repaint. */
834 0 : if (terminal_consume_resize()) {
835 0 : int nr = terminal_rows(); if (nr < 3) nr = app.rows;
836 0 : int nc = terminal_cols(); if (nc < 40) nc = app.cols;
837 0 : if (nr != app.rows || nc != app.cols) {
838 0 : if (tui_app_resize(&app, nr, nc) == 0) {
839 0 : screen_cursor_visible(&app.screen, 0);
840 0 : tui_app_paint(&app);
841 0 : screen_flip(&app.screen);
842 : }
843 : }
844 0 : if (ready < 0) continue; /* poll returned -1 (EINTR) */
845 : }
846 :
847 0 : if (ready == 0) {
848 : /* No keystroke within the poll window: consult the server
849 : * for any changes since we last asked. If anything came in,
850 : * refresh the dialog pane (titles / unread counts) and, if a
851 : * dialog is currently open, its history too. */
852 0 : if (upd_active) {
853 0 : UpdatesDifference diff = {0};
854 0 : if (domain_updates_difference(cfg, s, t,
855 : &upd_state, &diff) == 0) {
856 0 : upd_state = diff.next_state;
857 0 : int changed = !diff.is_empty
858 0 : && (diff.new_messages_count > 0
859 0 : || diff.other_updates_count > 0);
860 0 : if (changed) {
861 0 : dialog_pane_refresh(&app.dialogs, cfg, s, t);
862 0 : app.dialogs.lv.rows_visible = app.layout.dialogs.rows;
863 0 : if (app.history.peer_loaded) {
864 0 : history_pane_load(&app.history, cfg, s, t,
865 : &app.history.peer);
866 0 : app.history.lv.rows_visible = app.layout.history.rows;
867 : }
868 0 : tui_app_paint(&app);
869 0 : screen_flip(&app.screen);
870 : }
871 : }
872 : }
873 0 : continue;
874 : }
875 :
876 0 : if (ready < 0) continue; /* interrupted before any key — retry */
877 :
878 0 : TermKey key = terminal_read_key();
879 : TuiEvent ev;
880 0 : if (key == TERM_KEY_IGNORE) {
881 0 : ev = tui_app_handle_char(&app, terminal_last_printable());
882 : } else {
883 0 : ev = tui_app_handle_key(&app, key);
884 : }
885 :
886 0 : if (ev == TUI_EVENT_QUIT) break;
887 :
888 0 : if (ev == TUI_EVENT_OPEN_DIALOG) {
889 0 : const DialogEntry *d = dialog_pane_selected(&app.dialogs);
890 0 : if (d) {
891 : HistoryPeer peer;
892 0 : if (dialog_to_history_peer(d, &peer) == 0) {
893 0 : status_row_set_message(&app.status, "loading…");
894 0 : tui_app_paint(&app);
895 0 : screen_flip(&app.screen);
896 0 : if (history_pane_load(&app.history, cfg, s, t, &peer) == 0) {
897 : /* Restore the viewport height lost to the
898 : * history_pane_set_entries reset. */
899 0 : app.history.lv.rows_visible = app.layout.history.rows;
900 0 : status_row_set_message(&app.status, NULL);
901 : } else {
902 0 : status_row_set_message(&app.status, "history: load failed");
903 : }
904 : } else {
905 0 : status_row_set_message(&app.status,
906 : "cannot open (access_hash missing)");
907 : }
908 : }
909 : }
910 :
911 0 : if (ev != TUI_EVENT_NONE) {
912 0 : tui_app_paint(&app);
913 0 : screen_flip(&app.screen);
914 : }
915 : }
916 :
917 : /* Reset the terminal for the shell prompt. */
918 0 : screen_cursor_visible(&app.screen, 1);
919 0 : screen_cursor(&app.screen, app.rows, 1);
920 0 : fputs("\r\n", stdout);
921 0 : fflush(stdout);
922 0 : tui_app_free(&app);
923 0 : return rc;
924 : }
925 :
926 : /* Scan argv for --tui flag (used after full arg parse to decide mode). */
927 0 : static int has_tui_flag(int argc, char **argv) {
928 0 : for (int i = 1; i < argc; i++) {
929 0 : if (strcmp(argv[i], "--tui") == 0) return 1;
930 : }
931 0 : return 0;
932 : }
933 :
934 5 : int main(int argc, char **argv) {
935 5 : platform_normalize_argv(&argc, &argv);
936 : AppContext ctx;
937 5 : if (app_bootstrap(&ctx, "tg-tui") != 0) {
938 5 : fprintf(stderr, "tg-tui: bootstrap failed\n");
939 5 : return 1;
940 : }
941 :
942 : /* Drop the session-scoped resolver cache when the user logs out so a
943 : * subsequent login does not see stale @peer → id mappings. */
944 0 : auth_logout_set_cache_flush_cb(resolve_cache_flush);
945 :
946 : /* --logout: invalidate session server-side, then wipe the local file. */
947 0 : for (int i = 1; i < argc; i++) {
948 0 : if (strcmp(argv[i], "--logout") == 0) {
949 : ApiConfig cfg;
950 : MtProtoSession s;
951 : Transport t;
952 0 : if (credentials_load(&cfg) == 0) {
953 0 : transport_init(&t);
954 0 : mtproto_session_init(&s);
955 0 : int loaded_dc = 0;
956 0 : if (session_store_load(&s, &loaded_dc) == 0) {
957 0 : const DcEndpoint *ep = dc_lookup(loaded_dc);
958 0 : if (ep && transport_connect(&t, ep->host, ep->port) == 0) {
959 0 : t.dc_id = loaded_dc;
960 0 : auth_logout(&cfg, &s, &t);
961 0 : transport_close(&t);
962 : } else {
963 0 : logger_log(LOG_WARN,
964 : "tg-tui: logout: cannot connect to DC%d, clearing local session",
965 : loaded_dc);
966 0 : session_store_clear();
967 : }
968 : } else {
969 0 : session_store_clear();
970 : }
971 : } else {
972 0 : session_store_clear();
973 : }
974 0 : fprintf(stderr, "tg-tui: persisted session cleared.\n");
975 0 : app_shutdown(&ctx);
976 0 : return 0;
977 : }
978 : }
979 :
980 : /* Handle `login` subcommand before credentials are loaded.
981 : * Syntax: tg-tui login [--api-id N --api-hash HEX [--force]] */
982 0 : for (int i = 1; i < argc; i++) {
983 0 : if (strcmp(argv[i], "login") == 0) {
984 : /* Parse any --api-id / --api-hash / --force flags after "login". */
985 0 : const char *api_id_str = NULL;
986 0 : const char *api_hash_str = NULL;
987 0 : int force = 0;
988 0 : for (int j = i + 1; j < argc; j++) {
989 0 : if (strcmp(argv[j], "--api-id") == 0 && j + 1 < argc)
990 0 : { api_id_str = argv[++j]; }
991 0 : else if (strcmp(argv[j], "--api-hash") == 0 && j + 1 < argc)
992 0 : { api_hash_str = argv[++j]; }
993 0 : else if (strcmp(argv[j], "--force") == 0)
994 0 : { force = 1; }
995 : }
996 : int wrc;
997 0 : if (api_id_str || api_hash_str)
998 0 : wrc = config_wizard_run_batch(api_id_str, api_hash_str, force);
999 : else
1000 0 : wrc = config_wizard_run_interactive(force);
1001 0 : app_shutdown(&ctx);
1002 0 : return wrc != 0 ? 1 : 0;
1003 : }
1004 : }
1005 :
1006 : /* Handle --help / --version before anything else. */
1007 0 : for (int i = 1; i < argc; i++) {
1008 0 : if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
1009 0 : print_help();
1010 0 : app_shutdown(&ctx);
1011 0 : return 0;
1012 : }
1013 0 : if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) {
1014 0 : arg_print_version();
1015 0 : app_shutdown(&ctx);
1016 0 : return 0;
1017 : }
1018 : }
1019 :
1020 : /* Parse --phone / --code / --password / --tui flags. */
1021 0 : const char *opt_phone = NULL;
1022 0 : const char *opt_code = NULL;
1023 0 : const char *opt_password = NULL;
1024 0 : int tui_mode = has_tui_flag(argc, argv);
1025 :
1026 0 : for (int i = 1; i < argc; i++) {
1027 0 : if (strcmp(argv[i], "--phone") == 0 && i + 1 < argc) {
1028 0 : opt_phone = argv[++i];
1029 0 : } else if (strcmp(argv[i], "--code") == 0 && i + 1 < argc) {
1030 0 : opt_code = argv[++i];
1031 0 : } else if (strcmp(argv[i], "--password") == 0 && i + 1 < argc) {
1032 0 : opt_password = argv[++i];
1033 : }
1034 : }
1035 :
1036 : ApiConfig cfg;
1037 0 : if (credentials_load(&cfg) != 0) {
1038 0 : app_shutdown(&ctx);
1039 0 : return 1;
1040 : }
1041 :
1042 : LineHistory hist;
1043 0 : rl_history_init(&hist);
1044 :
1045 0 : PromptCtx pctx = {
1046 : .hist = &hist,
1047 : .phone = opt_phone,
1048 : .code = opt_code,
1049 : .password = opt_password,
1050 : };
1051 0 : AuthFlowCallbacks cb = {
1052 : .get_phone = cb_get_phone,
1053 : .get_code = cb_get_code,
1054 : .get_password = cb_get_password,
1055 : .user = &pctx,
1056 : };
1057 :
1058 : MtProtoSession s;
1059 : Transport t;
1060 0 : transport_init(&t);
1061 0 : mtproto_session_init(&s);
1062 :
1063 0 : if (!tui_mode) {
1064 0 : fputs("tg-tui — interactive Telegram client. "
1065 : "Type 'help' for commands (or run with --tui for pane view).\n",
1066 : stdout);
1067 : }
1068 :
1069 0 : if (auth_flow_login(&cfg, &cb, &t, &s, NULL) != 0) {
1070 0 : fprintf(stderr, "tg-tui: login failed\n");
1071 0 : transport_close(&t);
1072 0 : app_shutdown(&ctx);
1073 0 : return 1;
1074 : }
1075 :
1076 0 : int rc = tui_mode ? run_tui_loop(&cfg, &s, &t)
1077 0 : : repl(&cfg, &s, &t, &hist);
1078 0 : transport_close(&t);
1079 0 : app_shutdown(&ctx);
1080 0 : return rc;
1081 : }
|