LCOV - code coverage report
Current view: top level - src/domain/read - dialogs.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 90.3 % 186 168
Test Date: 2026-04-20 19:54:24 Functions: 100.0 % 6 6

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file domain/read/dialogs.c
       6              :  * @brief messages.getDialogs parser (minimal v1).
       7              :  */
       8              : 
       9              : #include "domain/read/dialogs.h"
      10              : 
      11              : #include "tl_serial.h"
      12              : #include "tl_registry.h"
      13              : #include "tl_skip.h"
      14              : #include "mtproto_rpc.h"
      15              : #include "logger.h"
      16              : #include "raii.h"
      17              : 
      18              : #include <stddef.h>
      19              : #include <time.h>
      20              : 
      21              : #include <stdlib.h>
      22              : #include <string.h>
      23              : 
      24              : /* ---- In-memory TTL cache ---- */
      25              : 
      26              : /** Default TTL for the dialogs cache (seconds). */
      27              : #ifndef DIALOGS_CACHE_TTL_S
      28              : #define DIALOGS_CACHE_TTL_S 60
      29              : #endif
      30              : 
      31              : /** Maximum cached dialogs per call site (archived=0 and archived=1). */
      32              : #define DIALOGS_CACHE_SLOTS 2
      33              : #define DIALOGS_CACHE_MAX   512
      34              : 
      35              : typedef struct {
      36              :     int         valid;
      37              :     int         archived;
      38              :     time_t      fetched_at;
      39              :     int         count;
      40              :     int         total_count;
      41              :     DialogEntry entries[DIALOGS_CACHE_MAX];
      42              : } DialogsCache;
      43              : 
      44              : static DialogsCache s_cache[DIALOGS_CACHE_SLOTS]; /* [0]=inbox [1]=archive */
      45              : 
      46              : /** @brief Mockable clock — tests may replace this with a fake. */
      47              : static time_t (*s_now_fn)(void) = NULL;
      48              : 
      49           43 : static time_t dialogs_now(void) {
      50           43 :     if (s_now_fn) return s_now_fn();
      51           38 :     return time(NULL);
      52              : }
      53              : 
      54              : /**
      55              :  * @brief Override the clock used for TTL checks (test use only).
      56              :  * Pass NULL to restore the real clock.
      57              :  */
      58           16 : void dialogs_cache_set_now_fn(time_t (*fn)(void)) {
      59           16 :     s_now_fn = fn;
      60           16 : }
      61              : 
      62              : /**
      63              :  * @brief Flush the in-memory dialogs cache (test use only).
      64              :  *
      65              :  * Call before each unit test that drives domain_get_dialogs directly so
      66              :  * that cached state from a previous test does not mask a fresh RPC.
      67              :  */
      68           21 : void dialogs_cache_flush(void) {
      69           21 :     memset(s_cache, 0, sizeof(s_cache));
      70           21 : }
      71              : 
      72              : 
      73              : #define CRC_messages_getDialogs 0xa0f4cb4fu
      74              : #define CRC_inputPeerEmpty      0x7f3b18eau
      75              : 
      76              : /* ---- Request builder ---- */
      77              : 
      78              : /* flags bit for the optional folder_id field in messages.getDialogs */
      79              : #define GETDIALOGS_FLAG_FOLDER_ID (1u << 1)
      80              : 
      81           23 : static int build_request(int limit, int archived,
      82              :                          uint8_t *buf, size_t cap, size_t *out_len) {
      83              :     TlWriter w;
      84           23 :     tl_writer_init(&w);
      85           23 :     tl_write_uint32(&w, CRC_messages_getDialogs);
      86           23 :     uint32_t flags = archived ? GETDIALOGS_FLAG_FOLDER_ID : 0u;
      87           23 :     tl_write_uint32(&w, flags);                   /* flags */
      88           23 :     if (archived)
      89            2 :         tl_write_int32(&w, 1);                    /* folder_id = 1 (Archive) */
      90           23 :     tl_write_int32 (&w, 0);                       /* offset_date */
      91           23 :     tl_write_int32 (&w, 0);                       /* offset_id */
      92           23 :     tl_write_uint32(&w, CRC_inputPeerEmpty);      /* offset_peer */
      93           23 :     tl_write_int32 (&w, limit);                   /* limit */
      94           23 :     tl_write_int64 (&w, 0);                       /* hash */
      95              : 
      96           23 :     int rc = -1;
      97           23 :     if (w.len <= cap) {
      98           23 :         memcpy(buf, w.data, w.len);
      99           23 :         *out_len = w.len;
     100           23 :         rc = 0;
     101              :     }
     102           23 :     tl_writer_free(&w);
     103           23 :     return rc;
     104              : }
     105              : 
     106              : /* ---- Dialog TL parsing (minimal, schema-tolerant) ----
     107              :  *
     108              :  * dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true
     109              :  *     view_forum_as_messages:flags.6?true
     110              :  *     peer:Peer
     111              :  *     top_message:int
     112              :  *     read_inbox_max_id:int
     113              :  *     read_outbox_max_id:int
     114              :  *     unread_count:int
     115              :  *     unread_mentions_count:int
     116              :  *     unread_reactions_count:int
     117              :  *     notify_settings:PeerNotifySettings
     118              :  *     pts:flags.0?int
     119              :  *     draft:flags.1?DraftMessage
     120              :  *     folder_id:flags.4?int
     121              :  *     ttl_period:flags.5?int
     122              :  *     = Dialog
     123              :  *
     124              :  * We only pull the first block (peer + unread_count + top_message_id).
     125              :  * Optional trailing fields are not read because callers don't need them;
     126              :  * the reader stops advancing after this function returns.
     127              :  */
     128              : #define CRC_dialog       0xd58a08c6u
     129              : #define CRC_dialogFolder 0x71bd134cu
     130              : 
     131          418 : static int parse_peer(TlReader *r, DialogEntry *out) {
     132          418 :     if (!tl_reader_ok(r)) return -1;
     133          418 :     uint32_t crc = tl_read_uint32(r);
     134          418 :     switch (crc) {
     135          416 :     case TL_peerUser:    out->kind = DIALOG_PEER_USER;    break;
     136            1 :     case TL_peerChat:    out->kind = DIALOG_PEER_CHAT;    break;
     137            1 :     case TL_peerChannel: out->kind = DIALOG_PEER_CHANNEL; break;
     138            0 :     default:
     139            0 :         logger_log(LOG_WARN, "dialogs: unknown Peer constructor 0x%08x", crc);
     140            0 :         out->kind = DIALOG_PEER_UNKNOWN;
     141            0 :         return -1;
     142              :     }
     143          418 :     out->peer_id = tl_read_int64(r);
     144          418 :     return 0;
     145              : }
     146              : 
     147           25 : int domain_get_dialogs(const ApiConfig *cfg,
     148              :                        MtProtoSession *s, Transport *t,
     149              :                        int max_entries, int archived,
     150              :                        DialogEntry *out, int *out_count,
     151              :                        int *total_count) {
     152           25 :     if (!cfg || !s || !t || !out || !out_count || max_entries <= 0) return -1;
     153           25 :     *out_count = 0;
     154           25 :     if (total_count) *total_count = 0;
     155              : 
     156              :     /* ---- TTL cache check ---- */
     157           25 :     int slot = archived ? 1 : 0;
     158           25 :     DialogsCache *cache = &s_cache[slot];
     159           25 :     time_t now = dialogs_now();
     160           25 :     if (cache->valid && (now - cache->fetched_at) < DIALOGS_CACHE_TTL_S) {
     161            2 :         int n = cache->count < max_entries ? cache->count : max_entries;
     162            2 :         memcpy(out, cache->entries, (size_t)n * sizeof(DialogEntry));
     163            2 :         *out_count = n;
     164            2 :         if (total_count) *total_count = cache->total_count;
     165            2 :         logger_log(LOG_DEBUG, "dialogs: served %d entries from cache (age=%lds)",
     166            2 :                    n, (long)(now - cache->fetched_at));
     167            2 :         return 0;
     168              :     }
     169              : 
     170              :     uint8_t query[132];
     171           23 :     size_t qlen = 0;
     172           23 :     if (build_request(max_entries, archived, query, sizeof(query), &qlen) != 0) {
     173            0 :         logger_log(LOG_ERROR, "dialogs: build_request overflow");
     174            0 :         return -1;
     175              :     }
     176              : 
     177           23 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(262144);
     178           23 :     if (!resp) return -1;
     179           23 :     size_t resp_len = 0;
     180           23 :     if (api_call(cfg, s, t, query, qlen, resp, 262144, &resp_len) != 0) {
     181            0 :         logger_log(LOG_ERROR, "dialogs: api_call failed");
     182            0 :         return -1;
     183              :     }
     184           23 :     if (resp_len < 8) {
     185            0 :         logger_log(LOG_ERROR, "dialogs: response too short");
     186            0 :         return -1;
     187              :     }
     188              : 
     189              :     uint32_t top;
     190           23 :     memcpy(&top, resp, 4);
     191           23 :     if (top == TL_rpc_error) {
     192              :         RpcError err;
     193            1 :         rpc_parse_error(resp, resp_len, &err);
     194            1 :         logger_log(LOG_ERROR, "dialogs: RPC error %d: %s",
     195              :                    err.error_code, err.error_msg);
     196            1 :         return -1;
     197              :     }
     198              : 
     199              :     /* messages.dialogsNotModified#f0e3e596 count:int
     200              :      * Returned by the server when the client's hash matches the cached list —
     201              :      * no entries follow.  We surface count via total_count and return 0
     202              :      * dialogs so callers know the cache is valid. */
     203           22 :     if (top == TL_messages_dialogsNotModified) {
     204            2 :         TlReader r = tl_reader_init(resp, resp_len);
     205            2 :         tl_read_uint32(&r); /* skip constructor */
     206            2 :         int32_t srv_count = tl_read_int32(&r);
     207            2 :         if (total_count) *total_count = (int)srv_count;
     208            2 :         logger_log(LOG_DEBUG,
     209              :                    "dialogs: not-modified, server count=%d", srv_count);
     210            2 :         return 0; /* *out_count remains 0; caller should use its cache */
     211              :     }
     212              : 
     213           20 :     if (top != TL_messages_dialogs && top != TL_messages_dialogsSlice) {
     214            1 :         logger_log(LOG_ERROR,
     215              :                    "dialogs: unexpected top constructor 0x%08x", top);
     216            1 :         return -1;
     217              :     }
     218              : 
     219           19 :     TlReader r = tl_reader_init(resp, resp_len);
     220           19 :     tl_read_uint32(&r); /* top constructor */
     221              : 
     222              :     /* messages.dialogsSlice#71e094f3 count:int dialogs:Vector<Dialog>... */
     223           19 :     if (top == TL_messages_dialogsSlice) {
     224            8 :         int32_t slice_total = tl_read_int32(&r);
     225            8 :         if (total_count) *total_count = (int)slice_total;
     226              :     }
     227              : 
     228              :     /* dialogs vector */
     229           19 :     uint32_t vec = tl_read_uint32(&r);
     230           19 :     if (vec != TL_vector) {
     231            1 :         logger_log(LOG_ERROR, "dialogs: expected Vector<Dialog>, got 0x%08x", vec);
     232            1 :         return -1;
     233              :     }
     234           18 :     uint32_t count = tl_read_uint32(&r);
     235              :     /* For the complete-list variant, total == the vector length. */
     236           18 :     if (top == TL_messages_dialogs && total_count) *total_count = (int)count;
     237           18 :     int written = 0;
     238          436 :     for (uint32_t i = 0; i < count && written < max_entries; i++) {
     239          422 :         if (!tl_reader_ok(&r)) break;
     240          420 :         uint32_t dcrc = tl_read_uint32(&r);
     241          420 :         if (dcrc == CRC_dialogFolder) {
     242              :             /* dialogFolder has a separate layout — skipping support is a
     243              :              * follow-up. Stop iteration cleanly. */
     244            1 :             logger_log(LOG_DEBUG, "dialogs: folder entry — stopping parse");
     245            1 :             break;
     246              :         }
     247          419 :         if (dcrc != CRC_dialog) {
     248            1 :             logger_log(LOG_WARN, "dialogs: unknown Dialog constructor 0x%08x",
     249              :                        dcrc);
     250            1 :             break;
     251              :         }
     252              : 
     253          418 :         uint32_t flags = tl_read_uint32(&r);
     254          418 :         DialogEntry e = {0};
     255          418 :         if (parse_peer(&r, &e) != 0) break;
     256          418 :         e.top_message_id = tl_read_int32(&r);
     257          418 :         tl_read_int32(&r); /* read_inbox_max_id */
     258          418 :         tl_read_int32(&r); /* read_outbox_max_id */
     259          418 :         e.unread_count = tl_read_int32(&r);
     260          418 :         tl_read_int32(&r); /* unread_mentions_count */
     261          418 :         tl_read_int32(&r); /* unread_reactions_count */
     262              : 
     263          418 :         if (tl_skip_peer_notify_settings(&r) != 0) {
     264            0 :             logger_log(LOG_WARN,
     265              :                        "dialogs: failed to skip PeerNotifySettings");
     266            0 :             out[written++] = e;
     267            0 :             break;
     268              :         }
     269              : 
     270          418 :         if (flags & (1u << 0)) tl_read_int32(&r); /* pts */
     271          418 :         if (flags & (1u << 1)) {
     272            0 :             if (tl_skip_draft_message(&r) != 0) {
     273            0 :                 logger_log(LOG_WARN,
     274              :                            "dialogs: complex draft — stopping after entry %u",
     275              :                            i);
     276            0 :                 out[written++] = e;
     277            0 :                 break;
     278              :             }
     279              :         }
     280          418 :         if (flags & (1u << 4)) tl_read_int32(&r); /* folder_id */
     281          418 :         if (flags & (1u << 5)) tl_read_int32(&r); /* ttl_period */
     282              : 
     283          418 :         out[written++] = e;
     284              :     }
     285              : 
     286           18 :     *out_count = written;
     287              : 
     288              :     /* ---- Populate cache after successful RPC (before title join) ----
     289              :      * The join adds titles in-place so we store after the join, but we
     290              :      * need to handle all exit paths.  Prime the cache fields now so that
     291              :      * the goto-jump at join_done always sees a consistent state. */
     292              :     {
     293           18 :         int cached_n = written < DIALOGS_CACHE_MAX ? written : DIALOGS_CACHE_MAX;
     294           18 :         cache->valid      = 1;
     295           18 :         cache->archived   = archived;
     296           18 :         cache->fetched_at = dialogs_now();
     297           18 :         cache->count      = cached_n;
     298           18 :         cache->total_count = total_count ? *total_count : written;
     299           18 :         memcpy(cache->entries, out, (size_t)cached_n * sizeof(DialogEntry));
     300              :     }
     301              : 
     302              :     /* ---- Title join ----
     303              :      *
     304              :      * messages.dialogs / dialogsSlice continues with:
     305              :      *   messages:Vector<Message>
     306              :      *   chats:Vector<Chat>
     307              :      *   users:Vector<User>
     308              :      *
     309              :      * If we consumed every Dialog above, the cursor is positioned at the
     310              :      * start of the messages vector. Walk it using tl_skip_message, then
     311              :      * build id→title maps from the chats and users vectors and back-fill
     312              :      * titles on the returned DialogEntry rows.
     313              :      *
     314              :      * If ANY step fails (unsupported flag etc.) we stop gracefully and
     315              :      * leave whatever titles we collected so far — the caller already has
     316              :      * ids and counts, so the feature degrades instead of failing. */
     317           18 :     if (written < (int)count) return 0;              /* partial parse — skip join */
     318           16 :     if (!tl_reader_ok(&r))    return 0;
     319              : 
     320           16 :     uint32_t mvec = tl_read_uint32(&r);
     321           16 :     if (mvec != TL_vector) return 0;
     322           16 :     uint32_t mcount = tl_read_uint32(&r);
     323           16 :     for (uint32_t i = 0; i < mcount; i++) {
     324            0 :         if (tl_skip_message(&r) != 0) return 0;
     325              :     }
     326              : 
     327           16 :     uint32_t cvec = tl_read_uint32(&r);
     328           16 :     if (cvec != TL_vector) return 0;
     329           16 :     uint32_t ccount = tl_read_uint32(&r);
     330           16 :     ChatSummary *chats = (ccount > 0)
     331            1 :         ? (ChatSummary *)calloc(ccount, sizeof(ChatSummary))
     332           16 :         : NULL;
     333           16 :     uint32_t chats_written = 0;
     334           17 :     for (uint32_t i = 0; i < ccount; i++) {
     335            1 :         ChatSummary cs = {0};
     336            1 :         if (tl_extract_chat(&r, &cs) != 0) { free(chats); chats = NULL; goto join_done; }
     337            1 :         if (chats) chats[chats_written++] = cs;
     338              :     }
     339              : 
     340           16 :     uint32_t uvec = tl_read_uint32(&r);
     341           16 :     if (uvec != TL_vector) { free(chats); goto join_done; }
     342           16 :     uint32_t ucount = tl_read_uint32(&r);
     343           16 :     UserSummary *users = (ucount > 0)
     344            4 :         ? (UserSummary *)calloc(ucount, sizeof(UserSummary))
     345           16 :         : NULL;
     346           16 :     uint32_t users_written = 0;
     347           20 :     for (uint32_t i = 0; i < ucount; i++) {
     348            4 :         UserSummary us = {0};
     349            4 :         if (tl_extract_user(&r, &us) != 0) { free(chats); free(users); goto join_done; }
     350            4 :         if (users) users[users_written++] = us;
     351              :     }
     352              : 
     353              :     /* Fill DialogEntry title/username + access_hash by looking up peer_id. */
     354          432 :     for (int i = 0; i < *out_count; i++) {
     355          416 :         DialogEntry *e = &out[i];
     356          416 :         if (e->kind == DIALOG_PEER_USER) {
     357          414 :             for (uint32_t j = 0; j < users_written; j++) {
     358            4 :                 if (users[j].id == e->peer_id) {
     359            4 :                     memcpy(e->title,    users[j].name,     sizeof(e->title));
     360            4 :                     memcpy(e->username, users[j].username, sizeof(e->username));
     361            4 :                     e->access_hash      = users[j].access_hash;
     362            4 :                     e->have_access_hash = users[j].have_access_hash;
     363            4 :                     break;
     364              :                 }
     365              :             }
     366              :         } else { /* CHAT / CHANNEL */
     367            2 :             for (uint32_t j = 0; j < chats_written; j++) {
     368            1 :                 if (chats[j].id == e->peer_id) {
     369            1 :                     memcpy(e->title, chats[j].title, sizeof(e->title));
     370              :                     /* Legacy chat has no access_hash on the wire; leave
     371              :                      * have_access_hash=0 so the caller knows. Channels do. */
     372            1 :                     e->access_hash      = chats[j].access_hash;
     373            1 :                     e->have_access_hash = chats[j].have_access_hash;
     374            1 :                     break;
     375              :                 }
     376              :             }
     377              :         }
     378              :     }
     379           16 :     free(chats);
     380           16 :     free(users);
     381              :     /* Refresh cached entries to include joined titles / access_hashes. */
     382              :     {
     383           16 :         int cached_n = *out_count < DIALOGS_CACHE_MAX ? *out_count : DIALOGS_CACHE_MAX;
     384           16 :         memcpy(cache->entries, out, (size_t)cached_n * sizeof(DialogEntry));
     385              :     }
     386           16 : join_done:
     387           16 :     return 0;
     388              : }
        

Generated by: LCOV version 2.0-1