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

Generated by: LCOV version 2.0-1