feat(telegram): add sticker support with vision caching

Add support for receiving and sending Telegram stickers:

Inbound:
- Receive static WEBP stickers (skip animated/video)
- Process stickers through dedicated vision call for descriptions
- Cache vision descriptions to avoid repeated API calls
- Graceful error handling for fetch failures

Outbound:
- Add sticker action to send stickers by fileId
- Add sticker-search action to find cached stickers by query
- Accept stickerId from shared schema, convert to fileId

Cache:
- Store sticker metadata (fileId, emoji, setName, description)
- Fuzzy search by description, emoji, and set name
- Persist to ~/.clawdbot/telegram/sticker-cache.json

Config:
- Single `channels.telegram.actions.sticker` option enables both
  send and search actions

🤖 AI-assisted: Built with Claude Code (claude-opus-4-5)
Testing: Fully tested - unit tests pass, live tested on dev gateway
The contributor understands and has reviewed all code changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Josh Long
2026-01-26 22:07:43 +00:00
committed by Ayaan Zaidi
parent 9daa846457
commit 506bed5aed
18 changed files with 1365 additions and 14 deletions

View File

@@ -6,7 +6,9 @@ import {
editMessageTelegram,
reactMessageTelegram,
sendMessageTelegram,
sendStickerTelegram,
} from "../../telegram/send.js";
import { getCacheStats, searchStickers } from "../../telegram/sticker-cache.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import {
resolveTelegramInlineButtonsScope,
@@ -255,5 +257,64 @@ export async function handleTelegramAction(
});
}
if (action === "sendSticker") {
if (!isActionEnabled("sticker")) {
throw new Error(
"Telegram sticker actions are disabled. Set channels.telegram.actions.sticker to true.",
);
}
const to = readStringParam(params, "to", { required: true });
const fileId = readStringParam(params, "fileId", { required: true });
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
integer: true,
});
const messageThreadId = readNumberParam(params, "messageThreadId", {
integer: true,
});
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
);
}
const result = await sendStickerTelegram(to, fileId, {
token,
accountId: accountId ?? undefined,
replyToMessageId: replyToMessageId ?? undefined,
messageThreadId: messageThreadId ?? undefined,
});
return jsonResult({
ok: true,
messageId: result.messageId,
chatId: result.chatId,
});
}
if (action === "searchSticker") {
if (!isActionEnabled("sticker")) {
throw new Error(
"Telegram sticker actions are disabled. Set channels.telegram.actions.sticker to true.",
);
}
const query = readStringParam(params, "query", { required: true });
const limit = readNumberParam(params, "limit", { integer: true }) ?? 5;
const results = searchStickers(query, limit);
return jsonResult({
ok: true,
count: results.length,
stickers: results.map((s) => ({
fileId: s.fileId,
emoji: s.emoji,
description: s.description,
setName: s.setName,
})),
});
}
if (action === "stickerCacheStats") {
const stats = getCacheStats();
return jsonResult({ ok: true, ...stats });
}
throw new Error(`Unsupported Telegram action: ${action}`);
}