Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions src/channels/telegram.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 21 additions & 1 deletion tests/test_telegram.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand All @@ -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();
Expand All @@ -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;
Expand Down
Loading