diff --git a/src/channels/telegram.c b/src/channels/telegram.c index 45e3da3..c2e4a87 100644 --- a/src/channels/telegram.c +++ b/src/channels/telegram.c @@ -148,14 +148,15 @@ static int parse_update(const char *json, channel_incoming_msg_t *out, long *upd return 0; } cJSON *from = cJSON_GetObjectItem(msg, "from"); - if (!from || !cJSON_IsObject(from)) { - cJSON_Delete(root); - return -1; - } - cJSON *from_id = cJSON_GetObjectItem(from, "id"); + cJSON *from_id = (from && cJSON_IsObject(from)) ? cJSON_GetObjectItem(from, "id") : NULL; + /* Channel and anonymous-admin posts can omit a numeric from.id. Failing + * the poll here left last_update_id unchanged, so this update stayed at + * the head of getUpdates and later messages never ran. Skip it instead. */ if (!from_id || !cJSON_IsNumber(from_id)) { + fprintf(stderr, "shellclaw: telegram: skip update %ld (message has no from id)\n", + *update_id_out); cJSON_Delete(root); - return -1; + return 0; } long user_id = (long)from_id->valuedouble; if (!is_user_allowed(g_tg.cfg, user_id)) { @@ -221,14 +222,32 @@ static int parse_update(const char *json, channel_incoming_msg_t *out, long *upd return 1; } +static int format_getupdates_url(char *url, size_t url_cap, const char *token, + int timeout_sec, long offset) +{ + int n; + if (!url || url_cap == 0 || !token || token[0] == '\0') + return -1; + /* Default getUpdates limit is 100. A backlog of long messages exceeds the + * 256 KiB read buffer, curl aborts, and the offset never advances, so + * every later poll retries the same batch. The parser only consumes + * result[0]; ask for that one update so a single message always fits. */ + n = snprintf(url, url_cap, "%s%s/%s?timeout=%d&offset=%ld&limit=1", + TG_API_BASE, token, TG_GET_UPDATES, timeout_sec, offset); + if (n < 0 || (size_t)n >= url_cap) + return -1; + return 0; +} + static int tg_poll(channel_incoming_msg_t *out, int timeout_ms) { if (!out || !g_tg.initialized || !g_tg.token) return -1; memset(out, 0, sizeof(*out)); int timeout_sec = (timeout_ms / 1000) > 0 ? (timeout_ms / 1000) : 1; char url[512]; - snprintf(url, sizeof(url), "%s%s/%s?timeout=%d&offset=%ld", - TG_API_BASE, g_tg.token, TG_GET_UPDATES, timeout_sec, g_tg.last_update_id + 1); + if (format_getupdates_url(url, sizeof(url), g_tg.token, timeout_sec, + g_tg.last_update_id + 1) != 0) + return -1; CURL *curl = curl_easy_init(); if (!curl) return -1; tg_curl_harden(curl); @@ -331,6 +350,12 @@ int telegram_parse_update_for_test(const char *json, channel_incoming_msg_t *out return parse_update(json, out, update_id); } +int telegram_format_getupdates_url_for_test(char *url, size_t url_cap, const char *token, + int timeout_sec, long offset) +{ + return format_getupdates_url(url, url_cap, token, timeout_sec, offset); +} + void telegram_set_test_config(const config_t *cfg) { g_tg.cfg = cfg; diff --git a/tests/test_telegram.c b/tests/test_telegram.c index 0a4d611..69d2269 100644 --- a/tests/test_telegram.c +++ b/tests/test_telegram.c @@ -13,6 +13,8 @@ /* Test-only entry points from telegram.c (compiled with -DSHELLCLAW_TEST) */ extern int telegram_parse_update_for_test(const char *json, channel_incoming_msg_t *out, long *update_id); extern void telegram_set_test_config(const config_t *cfg); +extern int telegram_format_getupdates_url_for_test(char *url, size_t url_cap, const char *token, + int timeout_sec, long offset); static int tests_run = 0; static int tests_failed = 0; @@ -191,6 +193,9 @@ static void test_parse_missing_from_field(void) "{\"ok\":true,\"result\":[{" "\"update_id\":400," "\"message\":{\"text\":\"no from\"}" + "},{" + "\"update_id\":401," + "\"message\":{\"from\":{\"id\":42},\"text\":\"kept\"}" "}]}"; config_t *cfg = load_test_config("/tmp/test_tg_nofrom.toml", "42"); MU_ASSERT(cfg != NULL, "load config"); @@ -200,13 +205,27 @@ static void test_parse_missing_from_field(void) memset(&msg, 0, sizeof(msg)); long uid = 0; int r = telegram_parse_update_for_test(json, &msg, &uid); - MU_ASSERT(r == -1, "missing 'from' returns -1"); + /* Skip, but keep this update_id so the next poll can fetch 401. + * Returning -1 left the offset unchanged and pinned the inbox. */ + MU_ASSERT(r == 0, "missing 'from' is skipped"); + MU_ASSERT(uid == 400, "offset stays on the skipped update"); + MU_ASSERT(msg.text == NULL, "skipped update is not delivered"); channel_incoming_msg_clear(&msg); config_free(cfg); remove("/tmp/test_tg_nofrom.toml"); } +static void test_getupdates_url_requests_one_update(void) +{ + char url[512]; + int rc = telegram_format_getupdates_url_for_test(url, sizeof(url), "tok", 1, 99); + MU_ASSERT(rc == 0, "getUpdates url built"); + MU_ASSERT(strstr(url, "/bottok/getUpdates?") != NULL, "token and method in url"); + MU_ASSERT(strstr(url, "offset=99") != NULL, "offset preserved"); + MU_ASSERT(strstr(url, "limit=1") != NULL, "batch capped at one update"); +} + static void test_vtable_fields_set(void) { const channel_t *ch = channel_telegram_get(); @@ -227,6 +246,7 @@ int main(void) MU_RUN(test_parse_photo_with_caption); MU_RUN(test_parse_no_message_key); MU_RUN(test_parse_missing_from_field); + MU_RUN(test_getupdates_url_requests_one_update); MU_RUN(test_vtable_fields_set); printf("%d tests run, %d failed\n", tests_run, tests_failed); return tests_failed ? 1 : 0;