LCOV - code coverage report
Current view: top level - src/main - tg_tui.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 0.8 % 623 5
Test Date: 2026-05-06 13:17:06 Functions: 3.0 % 33 1

            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              : }
        

Generated by: LCOV version 2.0-1