Skip to content

cap_im_tg — Telegram IM integration

Source: cap_im_tg.c · header: cap_im_tg.h

cap_im_tg is the reference IM capability, showing one component playing two roles:

  1. Event source: background long-poll of the Telegram Bot API, turning user messages into claw_event_router events
  2. Callable tools: send text/image/file so the Agent can proactively reply

That dual pattern is the common architecture for IM caps (Feishu, QQ, WeChat).

Diagram

cap_im_tg starts two FreeRTOS tasks from the start hook:

Calls getUpdates with a 20 s long-poll timeout, parses each update, and publishes events:

// Text → standard message event
claw_event_router_publish_message(
    "tg_gateway",   // source_cap
    "telegram",     // source_channel
    chat_id,        // chat id
    text,           // body
    sender_id,      // sender id
    message_id      // message id
);

claw_event_router then routes to claw_core for the Agent or to automation actions.

Network jitter can replay updates; cap_im_tg keeps a ring of FNV-1a 64-bit hashes (64 slots) so the same message is not handled twice:

#define CAP_IM_TG_DEDUP_CACHE_SIZE 64

static bool cap_im_tg_dedup_check_and_record(const char *update_key)
{
    uint64_t key = cap_im_tg_fnv1a64(update_key);
    for (size_t i = 0; i < CAP_IM_TG_DEDUP_CACHE_SIZE; i++) {
        if (s_tg.seen_update_keys[i] == key) return true; // seen
    }
    s_tg.seen_update_keys[s_tg.seen_update_idx] = key;
    s_tg.seen_update_idx = (s_tg.seen_update_idx + 1) % CAP_IM_TG_DEDUP_CACHE_SIZE;
    return false;
}

Media download is slow, so it is async:

  1. tg_poll_task enqueues cap_im_tg_attachment_job_t (depth 8)
  2. tg_attachment_task consumes jobs, calls getFile, streams into FATFS
  3. On completion it publishes attachment_saved with payload_json (local path, MIME, size, …)
// Example attachment_saved payload_json
{
  "platform": "telegram",
  "attachment_kind": "photo",
  "saved_path": "/fatfs/inbox/telegram/-123456/789/photo.jpg",
  "saved_dir": "/fatfs/inbox/telegram/-123456/789",
  "saved_name": "photo.jpg",
  "mime": "image/jpeg",
  "caption": "Look at this",
  "platform_file_id": "AgACAgIAAxkBAAI...",
  "size_bytes": 45231,
  "saved_at_ms": 1714000000000
}

Downstream rules can listen for attachment_saved and chain cap_llm_inspect, etc.

cap_im_tg registers four descriptors:

Tool IDDescriptionkind
tg_gatewayPoll gateway (event source)EVENT_SOURCE
tg_send_messageSend text to a chat_idCALLABLE
tg_send_imageSend a local image fileCALLABLE
tg_send_fileSend a local arbitrary fileCALLABLE
  • If chat_id is missing in input_json, fall back to ctx->chat_id (reply to the current conversation)
  • Long text is chunked (4096 bytes max per sendMessage)
// chat_id precedence: JSON arg > call context
if (cJSON_IsString(chat_id_json) && chat_id_json->valuestring[0]) {
    chat_id = chat_id_json->valuestring;
} else if (ctx && ctx->chat_id && ctx->chat_id[0]) {
    chat_id = ctx->chat_id;  // inherit session context
}

tg_send_image / tg_send_file upload via multipart/form-data:

  • stat() for exact Content-Length
  • esp_http_client_open + manual parts—no full-file RAM buffer
  • MIME guessed from extension (.jpg, .png, .pdf, .txt, .json)

Application code configures cap_im_tg through:

// Bot token (must be set before start)
cap_im_tg_set_token("YOUR_BOT_TOKEN");

// Optional inbound attachment policy
cap_im_tg_set_attachment_config(&(cap_im_tg_attachment_config_t){
    .storage_root_dir         = "/fatfs/inbox",
    .max_inbound_file_bytes   = 2 * 1024 * 1024,  // max 2 MB
    .enable_inbound_attachments = true,
});

// Manual start (normally via claw_cap_start_group)
cap_im_tg_start();

cap_im_feishu, cap_im_qq, cap_im_wechat share the same split; only protocol and auth differ:

ComponentProtocolNotes
cap_im_tgBot API + long pollSimplest, no server of your own
cap_im_feishuWebhook / Event APINeeds public reachability
cap_im_qqQQ Bot APITencent approval
cap_im_wechatWeCom APIEnterprise scenarios