mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:08:27 +00:00
chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { TelegramAccountConfig } from "../config/types.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import { resolveTelegramToken } from "./token.js";
|
||||
|
||||
export type ResolvedTelegramAccount = {
|
||||
@@ -42,10 +39,7 @@ function resolveAccountConfig(
|
||||
return accounts[accountId] as TelegramAccountConfig | undefined;
|
||||
}
|
||||
|
||||
function mergeTelegramAccountConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
): TelegramAccountConfig {
|
||||
function mergeTelegramAccountConfig(cfg: ClawdbotConfig, accountId: string): TelegramAccountConfig {
|
||||
const { accounts: _ignored, ...base } = (cfg.channels?.telegram ??
|
||||
{}) as TelegramAccountConfig & { accounts?: unknown };
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
@@ -89,9 +83,7 @@ export function resolveTelegramAccount(params: {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function listEnabledTelegramAccounts(
|
||||
cfg: ClawdbotConfig,
|
||||
): ResolvedTelegramAccount[] {
|
||||
export function listEnabledTelegramAccounts(cfg: ClawdbotConfig): ResolvedTelegramAccount[] {
|
||||
return listTelegramAccountIds(cfg)
|
||||
.map((accountId) => resolveTelegramAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled);
|
||||
|
||||
@@ -24,13 +24,10 @@ describe("telegram audit", () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({ ok: true, result: { status: "member" } }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
),
|
||||
new Response(JSON.stringify({ ok: true, result: { status: "member" } }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
const res = await auditTelegramGroupMembership({
|
||||
|
||||
@@ -51,8 +51,7 @@ export function collectTelegramUnmentionedGroupIds(
|
||||
};
|
||||
}
|
||||
const hasWildcardUnmentionedGroups =
|
||||
Boolean(groups["*"]?.requireMention === false) &&
|
||||
groups["*"]?.enabled !== false;
|
||||
Boolean(groups["*"]?.requireMention === false) && groups["*"]?.enabled !== false;
|
||||
const groupIds: string[] = [];
|
||||
let unresolvedGroups = 0;
|
||||
for (const [key, value] of Object.entries(groups)) {
|
||||
@@ -100,14 +99,10 @@ export async function auditTelegramGroupMembership(params: {
|
||||
try {
|
||||
const url = `${base}/getChatMember?chat_id=${encodeURIComponent(chatId)}&user_id=${encodeURIComponent(String(params.botId))}`;
|
||||
const res = await fetchWithTimeout(url, params.timeoutMs, fetcher);
|
||||
const json = (await res.json()) as
|
||||
| TelegramApiOk<{ status?: string }>
|
||||
| TelegramApiErr;
|
||||
const json = (await res.json()) as TelegramApiOk<{ status?: string }> | TelegramApiErr;
|
||||
if (!res.ok || !isRecord(json) || json.ok !== true) {
|
||||
const desc =
|
||||
isRecord(json) &&
|
||||
json.ok === false &&
|
||||
typeof json.description === "string"
|
||||
isRecord(json) && json.ok === false && typeof json.description === "string"
|
||||
? json.description
|
||||
: `getChatMember failed (${res.status})`;
|
||||
groups.push({ chatId, ok: false, status: null, error: desc });
|
||||
@@ -116,10 +111,7 @@ export async function auditTelegramGroupMembership(params: {
|
||||
const status = isRecord((json as TelegramApiOk<unknown>).result)
|
||||
? ((json as TelegramApiOk<{ status?: string }>).result.status ?? null)
|
||||
: null;
|
||||
const ok =
|
||||
status === "creator" ||
|
||||
status === "administrator" ||
|
||||
status === "member";
|
||||
const ok = status === "creator" || status === "administrator" || status === "member";
|
||||
groups.push({
|
||||
chatId,
|
||||
ok,
|
||||
|
||||
@@ -5,12 +5,8 @@ export type NormalizedAllowFrom = {
|
||||
hasEntries: boolean;
|
||||
};
|
||||
|
||||
export const normalizeAllowFrom = (
|
||||
list?: Array<string | number>,
|
||||
): NormalizedAllowFrom => {
|
||||
const entries = (list ?? [])
|
||||
.map((value) => String(value).trim())
|
||||
.filter(Boolean);
|
||||
export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAllowFrom => {
|
||||
const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean);
|
||||
const hasWildcard = entries.includes("*");
|
||||
const normalized = entries
|
||||
.filter((value) => value !== "*")
|
||||
@@ -42,7 +38,5 @@ export const isSenderAllowed = (params: {
|
||||
if (senderId && allow.entries.includes(senderId)) return true;
|
||||
const username = senderUsername?.toLowerCase();
|
||||
if (!username) return false;
|
||||
return allow.entriesLower.some(
|
||||
(entry) => entry === username || entry === `@${username}`,
|
||||
);
|
||||
return allow.entriesLower.some((entry) => entry === username || entry === `@${username}`);
|
||||
};
|
||||
|
||||
@@ -3,11 +3,7 @@ import { danger, logVerbose } from "../globals.js";
|
||||
import { resolveMedia } from "./bot/delivery.js";
|
||||
import { resolveTelegramForumThreadId } from "./bot/helpers.js";
|
||||
import type { TelegramMessage } from "./bot/types.js";
|
||||
import {
|
||||
firstDefined,
|
||||
isSenderAllowed,
|
||||
normalizeAllowFrom,
|
||||
} from "./bot-access.js";
|
||||
import { firstDefined, isSenderAllowed, normalizeAllowFrom } from "./bot-access.js";
|
||||
import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js";
|
||||
import { readTelegramAllowFromStore } from "./pairing-store.js";
|
||||
|
||||
@@ -31,19 +27,12 @@ export const registerTelegramHandlers = ({
|
||||
try {
|
||||
entry.messages.sort((a, b) => a.msg.message_id - b.msg.message_id);
|
||||
|
||||
const captionMsg = entry.messages.find(
|
||||
(m) => m.msg.caption || m.msg.text,
|
||||
);
|
||||
const captionMsg = entry.messages.find((m) => m.msg.caption || m.msg.text);
|
||||
const primaryEntry = captionMsg ?? entry.messages[0];
|
||||
|
||||
const allMedia: Array<{ path: string; contentType?: string }> = [];
|
||||
for (const { ctx } of entry.messages) {
|
||||
const media = await resolveMedia(
|
||||
ctx,
|
||||
mediaMaxBytes,
|
||||
opts.token,
|
||||
opts.proxyFetch,
|
||||
);
|
||||
const media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
|
||||
if (media) {
|
||||
allMedia.push({ path: media.path, contentType: media.contentType });
|
||||
}
|
||||
@@ -74,16 +63,11 @@ export const registerTelegramHandlers = ({
|
||||
entities: undefined,
|
||||
};
|
||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||
const getFile =
|
||||
typeof ctx.getFile === "function"
|
||||
? ctx.getFile.bind(ctx)
|
||||
: async () => ({});
|
||||
await processMessage(
|
||||
{ message: syntheticMessage, me: ctx.me, getFile },
|
||||
[],
|
||||
storeAllowFrom,
|
||||
{ forceWasMentioned: true, messageIdOverride: callback.id },
|
||||
);
|
||||
const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({});
|
||||
await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, {
|
||||
forceWasMentioned: true,
|
||||
messageIdOverride: callback.id,
|
||||
});
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`callback handler failed: ${String(err)}`));
|
||||
} finally {
|
||||
@@ -98,24 +82,16 @@ export const registerTelegramHandlers = ({
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
|
||||
const chatId = msg.chat.id;
|
||||
const isGroup =
|
||||
msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||
const messageThreadId = (msg as { message_thread_id?: number })
|
||||
.message_thread_id;
|
||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
||||
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||
const resolvedThreadId = resolveTelegramForumThreadId({
|
||||
isForum,
|
||||
messageThreadId,
|
||||
});
|
||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(
|
||||
chatId,
|
||||
resolvedThreadId,
|
||||
);
|
||||
const groupAllowOverride = firstDefined(
|
||||
topicConfig?.allowFrom,
|
||||
groupConfig?.allowFrom,
|
||||
);
|
||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
||||
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
||||
const effectiveGroupAllow = normalizeAllowFrom([
|
||||
...(groupAllowOverride ?? groupAllowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
@@ -163,9 +139,7 @@ export const registerTelegramHandlers = ({
|
||||
// For allowlist mode, the sender (msg.from.id) must be in allowFrom
|
||||
const senderId = msg.from?.id;
|
||||
if (senderId == null) {
|
||||
logVerbose(
|
||||
`Blocked telegram group message (no sender ID, groupPolicy: allowlist)`,
|
||||
);
|
||||
logVerbose(`Blocked telegram group message (no sender ID, groupPolicy: allowlist)`);
|
||||
return;
|
||||
}
|
||||
if (!effectiveGroupAllow.hasEntries) {
|
||||
@@ -182,9 +156,7 @@ export const registerTelegramHandlers = ({
|
||||
senderUsername,
|
||||
})
|
||||
) {
|
||||
logVerbose(
|
||||
`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`,
|
||||
);
|
||||
logVerbose(`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -236,31 +208,22 @@ export const registerTelegramHandlers = ({
|
||||
|
||||
let media: Awaited<ReturnType<typeof resolveMedia>> = null;
|
||||
try {
|
||||
media = await resolveMedia(
|
||||
ctx,
|
||||
mediaMaxBytes,
|
||||
opts.token,
|
||||
opts.proxyFetch,
|
||||
);
|
||||
media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
|
||||
} catch (mediaErr) {
|
||||
const errMsg = String(mediaErr);
|
||||
if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) {
|
||||
const limitMb = Math.round(mediaMaxBytes / (1024 * 1024));
|
||||
await bot.api
|
||||
.sendMessage(
|
||||
chatId,
|
||||
`⚠️ File too large. Maximum size is ${limitMb}MB.`,
|
||||
{ reply_to_message_id: msg.message_id },
|
||||
)
|
||||
.sendMessage(chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, {
|
||||
reply_to_message_id: msg.message_id,
|
||||
})
|
||||
.catch(() => {});
|
||||
logger.warn({ chatId, error: errMsg }, "media exceeds size limit");
|
||||
return;
|
||||
}
|
||||
throw mediaErr;
|
||||
}
|
||||
const allMedia = media
|
||||
? [{ path: media.path, contentType: media.contentType }]
|
||||
: [];
|
||||
const allMedia = media ? [{ path: media.path, contentType: media.contentType }] : [];
|
||||
await processMessage(ctx, allMedia, storeAllowFrom);
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`handler failed: ${String(err)}`));
|
||||
|
||||
@@ -4,10 +4,7 @@ import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||
import { normalizeCommandBody } from "../auto-reply/commands-registry.js";
|
||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||
import { buildHistoryContextFromMap } from "../auto-reply/reply/history.js";
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
} from "../auto-reply/reply/mentions.js";
|
||||
import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js";
|
||||
import { formatLocationText, toLocationContext } from "../channels/location.js";
|
||||
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
@@ -26,11 +23,7 @@ import {
|
||||
hasBotMention,
|
||||
resolveTelegramForumThreadId,
|
||||
} from "./bot/helpers.js";
|
||||
import {
|
||||
firstDefined,
|
||||
isSenderAllowed,
|
||||
normalizeAllowFrom,
|
||||
} from "./bot-access.js";
|
||||
import { firstDefined, isSenderAllowed, normalizeAllowFrom } from "./bot-access.js";
|
||||
import { upsertTelegramPairingRequest } from "./pairing-store.js";
|
||||
|
||||
export const buildTelegramMessageContext = async ({
|
||||
@@ -60,20 +53,14 @@ export const buildTelegramMessageContext = async ({
|
||||
});
|
||||
const chatId = msg.chat.id;
|
||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||
const messageThreadId = (msg as { message_thread_id?: number })
|
||||
.message_thread_id;
|
||||
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
||||
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||
const resolvedThreadId = resolveTelegramForumThreadId({
|
||||
isForum,
|
||||
messageThreadId,
|
||||
});
|
||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(
|
||||
chatId,
|
||||
resolvedThreadId,
|
||||
);
|
||||
const peerId = isGroup
|
||||
? buildTelegramGroupPeerId(chatId, resolvedThreadId)
|
||||
: String(chatId);
|
||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
||||
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
channel: "telegram",
|
||||
@@ -84,14 +71,8 @@ export const buildTelegramMessageContext = async ({
|
||||
},
|
||||
});
|
||||
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
||||
const effectiveDmAllow = normalizeAllowFrom([
|
||||
...(allowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
]);
|
||||
const groupAllowOverride = firstDefined(
|
||||
topicConfig?.allowFrom,
|
||||
groupConfig?.allowFrom,
|
||||
);
|
||||
const effectiveDmAllow = normalizeAllowFrom([...(allowFrom ?? []), ...storeAllowFrom]);
|
||||
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
||||
const effectiveGroupAllow = normalizeAllowFrom([
|
||||
...(groupAllowOverride ?? groupAllowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
@@ -111,15 +92,9 @@ export const buildTelegramMessageContext = async ({
|
||||
|
||||
const sendTyping = async () => {
|
||||
try {
|
||||
await bot.api.sendChatAction(
|
||||
chatId,
|
||||
"typing",
|
||||
buildTelegramThreadParams(resolvedThreadId),
|
||||
);
|
||||
await bot.api.sendChatAction(chatId, "typing", buildTelegramThreadParams(resolvedThreadId));
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`telegram typing cue failed for chat ${chatId}: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`telegram typing cue failed for chat ${chatId}: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,14 +156,10 @@ export const buildTelegramMessageContext = async ({
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`telegram pairing reply failed for chat ${chatId}: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`);
|
||||
}
|
||||
} else {
|
||||
logVerbose(
|
||||
`Blocked unauthorized telegram sender ${candidate} (dmPolicy=${dmPolicy})`,
|
||||
);
|
||||
logVerbose(`Blocked unauthorized telegram sender ${candidate} (dmPolicy=${dmPolicy})`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -219,8 +190,7 @@ export const buildTelegramMessageContext = async ({
|
||||
const computedWasMentioned =
|
||||
(Boolean(botUsername) && hasBotMention(msg, botUsername)) ||
|
||||
matchesMentionPatterns(msg.text ?? msg.caption ?? "", mentionRegexes);
|
||||
const wasMentioned =
|
||||
options?.forceWasMentioned === true ? true : computedWasMentioned;
|
||||
const wasMentioned = options?.forceWasMentioned === true ? true : computedWasMentioned;
|
||||
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some(
|
||||
(ent) => ent.type === "mention",
|
||||
);
|
||||
@@ -277,19 +247,13 @@ export const buildTelegramMessageContext = async ({
|
||||
) => Promise<void>;
|
||||
};
|
||||
const reactionApi =
|
||||
typeof api.setMessageReaction === "function"
|
||||
? api.setMessageReaction.bind(api)
|
||||
: null;
|
||||
typeof api.setMessageReaction === "function" ? api.setMessageReaction.bind(api) : null;
|
||||
const ackReactionPromise =
|
||||
shouldAckReaction() && msg.message_id && reactionApi
|
||||
? reactionApi(chatId, msg.message_id, [
|
||||
{ type: "emoji", emoji: ackReaction },
|
||||
]).then(
|
||||
? reactionApi(chatId, msg.message_id, [{ type: "emoji", emoji: ackReaction }]).then(
|
||||
() => true,
|
||||
(err) => {
|
||||
logVerbose(
|
||||
`telegram react failed for chat ${chatId}: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`telegram react failed for chat ${chatId}: ${String(err)}`);
|
||||
return false;
|
||||
},
|
||||
)
|
||||
@@ -303,9 +267,7 @@ export const buildTelegramMessageContext = async ({
|
||||
|
||||
const replyTarget = describeReplyTarget(msg);
|
||||
const locationData = extractTelegramLocation(msg);
|
||||
const locationText = locationData
|
||||
? formatLocationText(locationData)
|
||||
: undefined;
|
||||
const locationText = locationData ? formatLocationText(locationData) : undefined;
|
||||
const rawText = (msg.text ?? msg.caption ?? "").trim();
|
||||
let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim();
|
||||
if (!rawBody) rawBody = placeholder;
|
||||
@@ -321,9 +283,7 @@ export const buildTelegramMessageContext = async ({
|
||||
replyTarget.id ? ` id:${replyTarget.id}` : ""
|
||||
}]\n${replyTarget.body}\n[/Replying]`
|
||||
: "";
|
||||
const groupLabel = isGroup
|
||||
? buildGroupLabel(msg, chatId, resolvedThreadId)
|
||||
: undefined;
|
||||
const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined;
|
||||
const body = formatAgentEnvelope({
|
||||
channel: "Telegram",
|
||||
from: isGroup
|
||||
@@ -333,9 +293,7 @@ export const buildTelegramMessageContext = async ({
|
||||
body: `${bodyText}${replySuffix}`,
|
||||
});
|
||||
let combinedBody = body;
|
||||
const historyKey = isGroup
|
||||
? buildTelegramGroupPeerId(chatId, resolvedThreadId)
|
||||
: undefined;
|
||||
const historyKey = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : undefined;
|
||||
if (isGroup && historyKey && historyLimit > 0) {
|
||||
combinedBody = buildHistoryContextFromMap({
|
||||
historyMap: groupHistories,
|
||||
@@ -345,10 +303,7 @@ export const buildTelegramMessageContext = async ({
|
||||
sender: buildSenderLabel(msg, senderId || chatId),
|
||||
body: rawBody,
|
||||
timestamp: msg.date ? msg.date * 1000 : undefined,
|
||||
messageId:
|
||||
typeof msg.message_id === "number"
|
||||
? String(msg.message_id)
|
||||
: undefined,
|
||||
messageId: typeof msg.message_id === "number" ? String(msg.message_id) : undefined,
|
||||
},
|
||||
currentMessage: combinedBody,
|
||||
formatEntry: (entry) =>
|
||||
@@ -373,9 +328,7 @@ export const buildTelegramMessageContext = async ({
|
||||
Body: combinedBody,
|
||||
RawBody: rawBody,
|
||||
CommandBody: commandBody,
|
||||
From: isGroup
|
||||
? buildTelegramGroupFrom(chatId, resolvedThreadId)
|
||||
: `telegram:${chatId}`,
|
||||
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
|
||||
To: `telegram:${chatId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
@@ -434,10 +387,8 @@ export const buildTelegramMessageContext = async ({
|
||||
|
||||
if (shouldLogVerbose()) {
|
||||
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
|
||||
const mediaInfo =
|
||||
allMedia.length > 1 ? ` mediaCount=${allMedia.length}` : "";
|
||||
const topicInfo =
|
||||
resolvedThreadId != null ? ` topic=${resolvedThreadId}` : "";
|
||||
const mediaInfo = allMedia.length > 1 ? ` mediaCount=${allMedia.length}` : "";
|
||||
const topicInfo = resolvedThreadId != null ? ` topic=${resolvedThreadId}` : "";
|
||||
logVerbose(
|
||||
`telegram inbound: chatId=${chatId} from=${ctxPayload.From} len=${body.length}${mediaInfo}${topicInfo} preview="${preview}"`,
|
||||
);
|
||||
|
||||
@@ -60,9 +60,7 @@ export const dispatchTelegramMessage = async ({
|
||||
draftStream && streamMode === "block"
|
||||
? resolveTelegramDraftStreamingChunking(cfg, route.accountId)
|
||||
: undefined;
|
||||
const draftChunker = draftChunking
|
||||
? new EmbeddedBlockChunker(draftChunking)
|
||||
: undefined;
|
||||
const draftChunker = draftChunking ? new EmbeddedBlockChunker(draftChunking) : undefined;
|
||||
let lastPartialText = "";
|
||||
let draftText = "";
|
||||
const updateDraftFromPartial = (text?: string) => {
|
||||
@@ -114,17 +112,14 @@ export const dispatchTelegramMessage = async ({
|
||||
|
||||
const disableBlockStreaming =
|
||||
Boolean(draftStream) ||
|
||||
(typeof telegramCfg.blockStreaming === "boolean"
|
||||
? !telegramCfg.blockStreaming
|
||||
: undefined);
|
||||
(typeof telegramCfg.blockStreaming === "boolean" ? !telegramCfg.blockStreaming : undefined);
|
||||
|
||||
let didSendReply = false;
|
||||
const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcherOptions: {
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
.responsePrefix,
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||
deliver: async (payload, info) => {
|
||||
if (info.kind === "final") {
|
||||
await flushDraft();
|
||||
@@ -143,17 +138,13 @@ export const dispatchTelegramMessage = async ({
|
||||
didSendReply = true;
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
danger(`telegram ${info.kind} reply failed: ${String(err)}`),
|
||||
);
|
||||
runtime.error?.(danger(`telegram ${info.kind} reply failed: ${String(err)}`));
|
||||
},
|
||||
onReplyStart: sendTyping,
|
||||
},
|
||||
replyOptions: {
|
||||
skillFilter,
|
||||
onPartialReply: draftStream
|
||||
? (payload) => updateDraftFromPartial(payload.text)
|
||||
: undefined,
|
||||
onPartialReply: draftStream ? (payload) => updateDraftFromPartial(payload.text) : undefined,
|
||||
onReasoningStream: draftStream
|
||||
? (payload) => {
|
||||
if (payload.text) draftStream.update(payload.text);
|
||||
@@ -169,12 +160,7 @@ export const dispatchTelegramMessage = async ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
removeAckAfterReply &&
|
||||
ackReactionPromise &&
|
||||
msg.message_id &&
|
||||
reactionApi
|
||||
) {
|
||||
if (removeAckAfterReply && ackReactionPromise && msg.message_id && reactionApi) {
|
||||
void ackReactionPromise.then((didAck) => {
|
||||
if (!didAck) return;
|
||||
reactionApi(chatId, msg.message_id, []).catch((err) => {
|
||||
|
||||
@@ -15,11 +15,7 @@ import {
|
||||
buildTelegramGroupPeerId,
|
||||
resolveTelegramForumThreadId,
|
||||
} from "./bot/helpers.js";
|
||||
import {
|
||||
firstDefined,
|
||||
isSenderAllowed,
|
||||
normalizeAllowFrom,
|
||||
} from "./bot-access.js";
|
||||
import { firstDefined, isSenderAllowed, normalizeAllowFrom } from "./bot-access.js";
|
||||
import { readTelegramAllowFromStore } from "./pairing-store.js";
|
||||
|
||||
export const registerTelegramNativeCommands = ({
|
||||
@@ -40,9 +36,7 @@ export const registerTelegramNativeCommands = ({
|
||||
shouldSkipUpdate,
|
||||
opts,
|
||||
}) => {
|
||||
const nativeCommands = nativeEnabled
|
||||
? listNativeCommandSpecsForConfig(cfg)
|
||||
: [];
|
||||
const nativeCommands = nativeEnabled ? listNativeCommandSpecsForConfig(cfg) : [];
|
||||
if (nativeCommands.length > 0) {
|
||||
const api = bot.api as unknown as {
|
||||
setMyCommands?: (
|
||||
@@ -58,17 +52,13 @@ export const registerTelegramNativeCommands = ({
|
||||
})),
|
||||
)
|
||||
.catch((err) => {
|
||||
runtime.error?.(
|
||||
danger(`telegram setMyCommands failed: ${String(err)}`),
|
||||
);
|
||||
runtime.error?.(danger(`telegram setMyCommands failed: ${String(err)}`));
|
||||
});
|
||||
} else {
|
||||
logVerbose("telegram: setMyCommands unavailable; skipping registration");
|
||||
}
|
||||
|
||||
if (
|
||||
typeof (bot as unknown as { command?: unknown }).command !== "function"
|
||||
) {
|
||||
if (typeof (bot as unknown as { command?: unknown }).command !== "function") {
|
||||
logVerbose("telegram: bot.command unavailable; skipping native handlers");
|
||||
} else {
|
||||
for (const command of nativeCommands) {
|
||||
@@ -77,33 +67,21 @@ export const registerTelegramNativeCommands = ({
|
||||
if (!msg) return;
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
const chatId = msg.chat.id;
|
||||
const isGroup =
|
||||
msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||
const messageThreadId = (msg as { message_thread_id?: number })
|
||||
.message_thread_id;
|
||||
const isForum =
|
||||
(msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
||||
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||
const resolvedThreadId = resolveTelegramForumThreadId({
|
||||
isForum,
|
||||
messageThreadId,
|
||||
});
|
||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(
|
||||
() => [],
|
||||
);
|
||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(
|
||||
chatId,
|
||||
resolvedThreadId,
|
||||
);
|
||||
const groupAllowOverride = firstDefined(
|
||||
topicConfig?.allowFrom,
|
||||
groupConfig?.allowFrom,
|
||||
);
|
||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
||||
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
||||
const effectiveGroupAllow = normalizeAllowFrom([
|
||||
...(groupAllowOverride ?? groupAllowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
]);
|
||||
const hasGroupAllowOverride =
|
||||
typeof groupAllowOverride !== "undefined";
|
||||
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
|
||||
|
||||
if (isGroup && groupConfig?.enabled === false) {
|
||||
await bot.api.sendMessage(chatId, "This group is disabled.");
|
||||
@@ -124,10 +102,7 @@ export const registerTelegramNativeCommands = ({
|
||||
senderUsername,
|
||||
})
|
||||
) {
|
||||
await bot.api.sendMessage(
|
||||
chatId,
|
||||
"You are not authorized to use this command.",
|
||||
);
|
||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -135,19 +110,13 @@ export const registerTelegramNativeCommands = ({
|
||||
if (isGroup && useAccessGroups) {
|
||||
const groupPolicy = telegramCfg.groupPolicy ?? "open";
|
||||
if (groupPolicy === "disabled") {
|
||||
await bot.api.sendMessage(
|
||||
chatId,
|
||||
"Telegram group commands are disabled.",
|
||||
);
|
||||
await bot.api.sendMessage(chatId, "Telegram group commands are disabled.");
|
||||
return;
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
const senderId = msg.from?.id;
|
||||
if (senderId == null) {
|
||||
await bot.api.sendMessage(
|
||||
chatId,
|
||||
"You are not authorized to use this command.",
|
||||
);
|
||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
||||
return;
|
||||
}
|
||||
const senderUsername = msg.from?.username ?? "";
|
||||
@@ -158,10 +127,7 @@ export const registerTelegramNativeCommands = ({
|
||||
senderUsername,
|
||||
})
|
||||
) {
|
||||
await bot.api.sendMessage(
|
||||
chatId,
|
||||
"You are not authorized to use this command.",
|
||||
);
|
||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -189,10 +155,7 @@ export const registerTelegramNativeCommands = ({
|
||||
entry.toLowerCase() === `@${senderUsername.toLowerCase()}`,
|
||||
));
|
||||
if (!commandAuthorized) {
|
||||
await bot.api.sendMessage(
|
||||
chatId,
|
||||
"You are not authorized to use this command.",
|
||||
);
|
||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,28 +166,19 @@ export const registerTelegramNativeCommands = ({
|
||||
accountId,
|
||||
peer: {
|
||||
kind: isGroup ? "group" : "dm",
|
||||
id: isGroup
|
||||
? buildTelegramGroupPeerId(chatId, resolvedThreadId)
|
||||
: String(chatId),
|
||||
id: isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId),
|
||||
},
|
||||
});
|
||||
const skillFilter = firstDefined(
|
||||
topicConfig?.skills,
|
||||
groupConfig?.skills,
|
||||
);
|
||||
const skillFilter = firstDefined(topicConfig?.skills, groupConfig?.skills);
|
||||
const systemPromptParts = [
|
||||
groupConfig?.systemPrompt?.trim() || null,
|
||||
topicConfig?.systemPrompt?.trim() || null,
|
||||
].filter((entry): entry is string => Boolean(entry));
|
||||
const groupSystemPrompt =
|
||||
systemPromptParts.length > 0
|
||||
? systemPromptParts.join("\n\n")
|
||||
: undefined;
|
||||
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||
const ctxPayload = {
|
||||
Body: prompt,
|
||||
From: isGroup
|
||||
? buildTelegramGroupFrom(chatId, resolvedThreadId)
|
||||
: `telegram:${chatId}`,
|
||||
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
|
||||
To: `slash:${senderId || chatId}`,
|
||||
ChatType: isGroup ? "group" : "direct",
|
||||
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
||||
@@ -253,8 +207,7 @@ export const registerTelegramNativeCommands = ({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcherOptions: {
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
.responsePrefix,
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||
deliver: async (payload) => {
|
||||
await deliverReplies({
|
||||
replies: [payload],
|
||||
@@ -268,11 +221,7 @@ export const registerTelegramNativeCommands = ({
|
||||
});
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
danger(
|
||||
`telegram slash ${info.kind} reply failed: ${String(err)}`,
|
||||
),
|
||||
);
|
||||
runtime.error?.(danger(`telegram slash ${info.kind} reply failed: ${String(err)}`));
|
||||
},
|
||||
},
|
||||
replyOptions: {
|
||||
@@ -289,9 +238,7 @@ export const registerTelegramNativeCommands = ({
|
||||
};
|
||||
if (typeof api.setMyCommands === "function") {
|
||||
api.setMyCommands([]).catch((err) => {
|
||||
runtime.error?.(
|
||||
danger(`telegram clear commands failed: ${String(err)}`),
|
||||
);
|
||||
runtime.error?.(danger(`telegram clear commands failed: ${String(err)}`));
|
||||
});
|
||||
} else {
|
||||
logVerbose("telegram: setMyCommands unavailable; skipping clear");
|
||||
|
||||
@@ -33,10 +33,7 @@ export const buildTelegramUpdateKey = (ctx: TelegramUpdateKeyContext) => {
|
||||
const callbackId = ctx.callbackQuery?.id;
|
||||
if (callbackId) return `callback:${callbackId}`;
|
||||
const msg =
|
||||
ctx.message ??
|
||||
ctx.update?.message ??
|
||||
ctx.update?.edited_message ??
|
||||
ctx.callbackQuery?.message;
|
||||
ctx.message ?? ctx.update?.message ?? ctx.update?.edited_message ?? ctx.callbackQuery?.message;
|
||||
const chatId = msg?.chat?.id;
|
||||
const messageId = msg?.message_id;
|
||||
if (typeof chatId !== "undefined" && typeof messageId === "number") {
|
||||
|
||||
@@ -21,15 +21,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -144,9 +142,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
it("accepts group messages when mentionPatterns match (without @botUsername)", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -161,9 +157,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -180,15 +174,11 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.WasMentioned).toBe(true);
|
||||
expect(payload.Body).toMatch(
|
||||
/^\[Telegram Test Group id:7 from Ada id:9 2025-01-09T00:00Z\]/,
|
||||
);
|
||||
expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 from Ada id:9 2025-01-09T00:00Z\]/);
|
||||
});
|
||||
it("includes sender identity in group envelope headers", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -201,9 +191,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -231,9 +219,7 @@ describe("createTelegramBot", () => {
|
||||
it("reacts to mention-gated group messages when ackReaction is enabled", async () => {
|
||||
onSpy.mockReset();
|
||||
setMessageReactionSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -251,9 +237,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -267,9 +251,7 @@ describe("createTelegramBot", () => {
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(setMessageReactionSpy).toHaveBeenCalledWith(7, 123, [
|
||||
{ type: "emoji", emoji: "👀" },
|
||||
]);
|
||||
expect(setMessageReactionSpy).toHaveBeenCalledWith(7, 123, [{ type: "emoji", emoji: "👀" }]);
|
||||
});
|
||||
it("clears native commands when disabled", () => {
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -282,9 +264,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("skips group messages when requireMention is enabled and no mention matches", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -298,9 +278,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -318,9 +296,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows group messages when requireMention is enabled but mentions cannot be detected", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -334,9 +310,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -357,15 +331,11 @@ describe("createTelegramBot", () => {
|
||||
it("includes reply-to context when a Telegram reply is received", async () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
|
||||
@@ -21,15 +21,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -144,9 +142,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
it("applies topic skill filters and system prompts", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -171,9 +167,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -203,9 +197,7 @@ describe("createTelegramBot", () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
commandSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
replySpy.mockResolvedValue({ text: "response" });
|
||||
|
||||
@@ -219,9 +211,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -251,9 +241,7 @@ describe("createTelegramBot", () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
commandSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
replySpy.mockResolvedValue({ text: "response" });
|
||||
|
||||
@@ -270,9 +258,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
expect(commandSpy).toHaveBeenCalled();
|
||||
const handler = commandSpy.mock.calls[0][1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = commandSpy.mock.calls[0][1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -301,9 +287,7 @@ describe("createTelegramBot", () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
commandSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
replySpy.mockImplementation(async (_ctx, opts) => {
|
||||
await opts?.onToolResult?.({ text: "tool update" });
|
||||
@@ -319,9 +303,9 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const verboseHandler = commandSpy.mock.calls.find(
|
||||
(call) => call[0] === "verbose",
|
||||
)?.[1] as ((ctx: Record<string, unknown>) => Promise<void>) | undefined;
|
||||
const verboseHandler = commandSpy.mock.calls.find((call) => call[0] === "verbose")?.[1] as
|
||||
| ((ctx: Record<string, unknown>) => Promise<void>)
|
||||
| undefined;
|
||||
if (!verboseHandler) throw new Error("verbose command handler missing");
|
||||
|
||||
await verboseHandler({
|
||||
@@ -341,9 +325,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("dedupes duplicate message updates by update_id", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -353,9 +335,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
const ctx = {
|
||||
update: { update_id: 111 },
|
||||
|
||||
@@ -21,15 +21,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -144,9 +142,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
it("blocks all group messages when groupPolicy is 'disabled'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -158,9 +154,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -178,9 +172,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("blocks group messages from senders not in allowFrom when groupPolicy is 'allowlist'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -192,9 +184,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -211,9 +201,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows group messages from senders in allowFrom (by ID) when groupPolicy is 'allowlist'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -226,9 +214,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -245,9 +231,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows group messages from senders in allowFrom (by username) when groupPolicy is 'allowlist'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -260,9 +244,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -279,9 +261,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows group messages from telegram:-prefixed allowFrom entries when groupPolicy is 'allowlist'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -294,9 +274,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -313,9 +291,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows group messages from tg:-prefixed allowFrom entries case-insensitively when groupPolicy is 'allowlist'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -328,9 +304,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -347,9 +321,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows all group messages when groupPolicy is 'open'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -361,9 +333,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
|
||||
@@ -21,15 +21,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -144,9 +142,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
it("dedupes duplicate callback_query updates by update_id", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -183,9 +179,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows distinct callback_query ids without update_id", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
|
||||
@@ -22,15 +22,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -235,13 +233,9 @@ describe("createTelegramBot", () => {
|
||||
it("sequentializes updates by chat and thread", () => {
|
||||
createTelegramBot({ token: "tok" });
|
||||
expect(sequentializeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(middlewareUseSpy).toHaveBeenCalledWith(
|
||||
sequentializeSpy.mock.results[0]?.value,
|
||||
);
|
||||
expect(middlewareUseSpy).toHaveBeenCalledWith(sequentializeSpy.mock.results[0]?.value);
|
||||
expect(sequentializeKey).toBe(getTelegramSequentialKey);
|
||||
expect(getTelegramSequentialKey({ message: { chat: { id: 123 } } })).toBe(
|
||||
"telegram:123",
|
||||
);
|
||||
expect(getTelegramSequentialKey({ message: { chat: { id: 123 } } })).toBe("telegram:123");
|
||||
expect(
|
||||
getTelegramSequentialKey({
|
||||
message: { chat: { id: 123 }, message_thread_id: 9 },
|
||||
@@ -260,15 +254,13 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("routes callback_query payloads as messages and answers callbacks", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const callbackHandler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "callback_query",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(callbackHandler).toBeDefined();
|
||||
|
||||
await callbackHandler({
|
||||
@@ -297,16 +289,12 @@ describe("createTelegramBot", () => {
|
||||
|
||||
try {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
expect(onSpy).toHaveBeenCalledWith("message", expect.any(Function));
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
const message = {
|
||||
chat: { id: 1234, type: "private" },
|
||||
@@ -337,9 +325,7 @@ describe("createTelegramBot", () => {
|
||||
it("requests pairing by default for unknown DM senders", async () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -352,9 +338,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -370,20 +354,14 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageSpy.mock.calls[0]?.[0]).toBe(1234);
|
||||
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain(
|
||||
"Your Telegram user id: 999",
|
||||
);
|
||||
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain(
|
||||
"Pairing code:",
|
||||
);
|
||||
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("Your Telegram user id: 999");
|
||||
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("Pairing code:");
|
||||
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("PAIRME12");
|
||||
});
|
||||
it("does not resend pairing code when a request is already pending", async () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -395,9 +373,7 @@ describe("createTelegramBot", () => {
|
||||
.mockResolvedValueOnce({ code: "PAIRME12", created: false });
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
const message = {
|
||||
chat: { id: 1234, type: "private" },
|
||||
@@ -425,9 +401,7 @@ describe("createTelegramBot", () => {
|
||||
sendChatActionSpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
await handler({
|
||||
message: { chat: { id: 42, type: "private" }, text: "hi" },
|
||||
me: { username: "clawdbot_bot" },
|
||||
|
||||
@@ -21,15 +21,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -144,9 +142,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
it("matches tg:-prefixed allowFrom entries case-insensitively in group allowlist", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -159,9 +155,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -179,9 +173,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("blocks group messages when groupPolicy allowlist has no groupAllowFrom", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -193,9 +185,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -212,9 +202,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows control commands with TG-prefixed groupAllowFrom entries", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -227,9 +215,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -247,9 +233,7 @@ describe("createTelegramBot", () => {
|
||||
it("isolates forum topic sessions and carries thread metadata", async () => {
|
||||
onSpy.mockReset();
|
||||
sendChatActionSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -262,9 +246,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -286,9 +268,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.SessionKey).toContain(
|
||||
"telegram:group:-1001234567890:topic:99",
|
||||
);
|
||||
expect(payload.SessionKey).toContain("telegram:group:-1001234567890:topic:99");
|
||||
expect(payload.From).toBe("group:-1001234567890:topic:99");
|
||||
expect(payload.MessageThreadId).toBe(99);
|
||||
expect(payload.IsForum).toBe(true);
|
||||
@@ -299,9 +279,7 @@ describe("createTelegramBot", () => {
|
||||
it("falls back to General topic thread id for typing in forums", async () => {
|
||||
onSpy.mockReset();
|
||||
sendChatActionSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -314,9 +292,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -343,9 +319,7 @@ describe("createTelegramBot", () => {
|
||||
it("routes General topic replies using thread id 1", async () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
replySpy.mockResolvedValue({ text: "response" });
|
||||
|
||||
@@ -359,9 +333,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
|
||||
@@ -21,15 +21,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -144,9 +142,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
it("matches usernames case-insensitively when groupPolicy is 'allowlist'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -159,9 +155,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -178,9 +172,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows direct messages regardless of groupPolicy", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -192,9 +184,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -211,9 +201,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows direct messages with tg/Telegram-prefixed allowFrom entries", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -224,9 +212,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -243,9 +229,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows direct messages with telegram:-prefixed allowFrom entries", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -256,9 +240,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -275,9 +257,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows group messages with wildcard in allowFrom when groupPolicy is 'allowlist'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -290,9 +270,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -309,9 +287,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("blocks group messages with no sender ID when groupPolicy is 'allowlist'", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -323,9 +299,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -342,9 +316,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("matches telegram:-prefixed allowFrom entries in group allowlist", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -357,9 +329,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
|
||||
@@ -21,15 +21,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -144,9 +142,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
it("routes DMs by telegram accountId binding", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
@@ -169,9 +165,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok", accountId: "opie" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -192,9 +186,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows per-group requireMention override", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -209,9 +201,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -227,9 +217,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("allows per-topic requireMention override", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -249,9 +237,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -273,9 +259,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("honors groups default when no explicit group override exists", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -287,9 +271,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -305,9 +287,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("does not block group messages when bot username is unknown", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -319,9 +299,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -336,9 +314,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("sends GIF replies as animations", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
|
||||
replySpy.mockResolvedValueOnce({
|
||||
@@ -353,9 +329,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
|
||||
@@ -24,15 +24,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(
|
||||
() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||
code: "PAIRCODE",
|
||||
created: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./pairing-store.js", () => ({
|
||||
readTelegramAllowFromStore,
|
||||
@@ -148,16 +146,12 @@ describe("createTelegramBot", () => {
|
||||
it("sends replies without native reply threading", async () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
replySpy.mockResolvedValue({ text: "a".repeat(4500) });
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 5, type: "private" },
|
||||
@@ -177,9 +171,7 @@ describe("createTelegramBot", () => {
|
||||
it("honors replyToMode=first for threaded replies", async () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
replySpy.mockResolvedValue({
|
||||
text: "a".repeat(4500),
|
||||
@@ -187,9 +179,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok", replyToMode: "first" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 5, type: "private" },
|
||||
@@ -211,9 +201,7 @@ describe("createTelegramBot", () => {
|
||||
it("prefixes tool and final replies with responsePrefix", async () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
replySpy.mockImplementation(async (_ctx, opts) => {
|
||||
await opts?.onToolResult?.({ text: "tool result" });
|
||||
@@ -227,9 +215,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 5, type: "private" },
|
||||
@@ -247,9 +233,7 @@ describe("createTelegramBot", () => {
|
||||
it("honors replyToMode=all for threaded replies", async () => {
|
||||
onSpy.mockReset();
|
||||
sendMessageSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
replySpy.mockResolvedValue({
|
||||
text: "a".repeat(4500),
|
||||
@@ -257,9 +241,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok", replyToMode: "all" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 5, type: "private" },
|
||||
@@ -278,9 +260,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("blocks group messages when telegram.groups is set without a wildcard", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -293,9 +273,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -311,9 +289,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("skips group messages without mention when requireMention is enabled", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
@@ -322,9 +298,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -340,13 +314,9 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
it("honors routed group activation from session store", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
replySpy.mockReset();
|
||||
const storeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-telegram-"),
|
||||
);
|
||||
const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-telegram-"));
|
||||
const storePath = path.join(storeDir, "sessions.json");
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
@@ -375,9 +345,7 @@ describe("createTelegramBot", () => {
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
|
||||
@@ -10,9 +10,7 @@ const sendChatActionSpy = vi.fn();
|
||||
type ApiStub = {
|
||||
config: { use: (arg: unknown) => void };
|
||||
sendChatAction: typeof sendChatActionSpy;
|
||||
setMyCommands: (
|
||||
commands: Array<{ command: string; description: string }>,
|
||||
) => Promise<void>;
|
||||
setMyCommands: (commands: Array<{ command: string; description: string }>) => Promise<void>;
|
||||
};
|
||||
|
||||
const apiStub: ApiStub = {
|
||||
@@ -95,17 +93,14 @@ vi.mock("../auto-reply/reply.js", () => {
|
||||
});
|
||||
|
||||
describe("telegram inbound media", () => {
|
||||
const INBOUND_MEDIA_TEST_TIMEOUT_MS =
|
||||
process.platform === "win32" ? 30_000 : 20_000;
|
||||
const INBOUND_MEDIA_TEST_TIMEOUT_MS = process.platform === "win32" ? 30_000 : 20_000;
|
||||
|
||||
it(
|
||||
"downloads media via file_path (no file.download)",
|
||||
async () => {
|
||||
const { createTelegramBot } = await import("./bot.js");
|
||||
const replyModule = await import("../auto-reply/reply.js");
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
onSpy.mockReset();
|
||||
replySpy.mockReset();
|
||||
@@ -123,21 +118,18 @@ describe("telegram inbound media", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, "fetch" as never)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { get: () => "image/jpeg" },
|
||||
arrayBuffer: async () =>
|
||||
new Uint8Array([0xff, 0xd8, 0xff, 0x00]).buffer,
|
||||
} as Response);
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch" as never).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { get: () => "image/jpeg" },
|
||||
arrayBuffer: async () => new Uint8Array([0xff, 0xd8, 0xff, 0x00]).buffer,
|
||||
} as Response);
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -151,9 +143,7 @@ describe("telegram inbound media", () => {
|
||||
});
|
||||
|
||||
expect(runtimeError).not.toHaveBeenCalled();
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"https://api.telegram.org/file/bottok/photos/1.jpg",
|
||||
);
|
||||
expect(fetchSpy).toHaveBeenCalledWith("https://api.telegram.org/file/bottok/photos/1.jpg");
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.Body).toContain("<media:image>");
|
||||
@@ -170,11 +160,9 @@ describe("telegram inbound media", () => {
|
||||
|
||||
const runtimeLog = vi.fn();
|
||||
const runtimeError = vi.fn();
|
||||
const globalFetchSpy = vi
|
||||
.spyOn(globalThis, "fetch" as never)
|
||||
.mockImplementation(() => {
|
||||
throw new Error("global fetch should not be called");
|
||||
});
|
||||
const globalFetchSpy = vi.spyOn(globalThis, "fetch" as never).mockImplementation(() => {
|
||||
throw new Error("global fetch should not be called");
|
||||
});
|
||||
const proxyFetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
@@ -194,9 +182,9 @@ describe("telegram inbound media", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
@@ -210,9 +198,7 @@ describe("telegram inbound media", () => {
|
||||
});
|
||||
|
||||
expect(runtimeError).not.toHaveBeenCalled();
|
||||
expect(proxyFetch).toHaveBeenCalledWith(
|
||||
"https://api.telegram.org/file/bottok/photos/2.jpg",
|
||||
);
|
||||
expect(proxyFetch).toHaveBeenCalledWith("https://api.telegram.org/file/bottok/photos/2.jpg");
|
||||
|
||||
globalFetchSpy.mockRestore();
|
||||
});
|
||||
@@ -220,9 +206,7 @@ describe("telegram inbound media", () => {
|
||||
it("logs a handler error when getFile returns no file_path", async () => {
|
||||
const { createTelegramBot } = await import("./bot.js");
|
||||
const replyModule = await import("../auto-reply/reply.js");
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
onSpy.mockReset();
|
||||
replySpy.mockReset();
|
||||
@@ -241,9 +225,9 @@ describe("telegram inbound media", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
@@ -273,10 +257,8 @@ describe("telegram media groups", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const MEDIA_GROUP_POLL_TIMEOUT_MS =
|
||||
process.platform === "win32" ? 30_000 : 15_000;
|
||||
const MEDIA_GROUP_TEST_TIMEOUT_MS =
|
||||
process.platform === "win32" ? 45_000 : 20_000;
|
||||
const MEDIA_GROUP_POLL_TIMEOUT_MS = process.platform === "win32" ? 30_000 : 15_000;
|
||||
const MEDIA_GROUP_TEST_TIMEOUT_MS = process.platform === "win32" ? 45_000 : 20_000;
|
||||
|
||||
const waitForMediaGroupProcessing = async (
|
||||
replySpy: ReturnType<typeof vi.fn>,
|
||||
@@ -294,24 +276,19 @@ describe("telegram media groups", () => {
|
||||
async () => {
|
||||
const { createTelegramBot } = await import("./bot.js");
|
||||
const replyModule = await import("../auto-reply/reply.js");
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
onSpy.mockReset();
|
||||
replySpy.mockReset();
|
||||
|
||||
const runtimeError = vi.fn();
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, "fetch" as never)
|
||||
.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { get: () => "image/png" },
|
||||
arrayBuffer: async () =>
|
||||
new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer,
|
||||
} as Response);
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch" as never).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { get: () => "image/png" },
|
||||
arrayBuffer: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer,
|
||||
} as Response);
|
||||
|
||||
createTelegramBot({
|
||||
token: "tok",
|
||||
@@ -323,9 +300,9 @@ describe("telegram media groups", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
const first = handler({
|
||||
@@ -375,28 +352,23 @@ describe("telegram media groups", () => {
|
||||
async () => {
|
||||
const { createTelegramBot } = await import("./bot.js");
|
||||
const replyModule = await import("../auto-reply/reply.js");
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
onSpy.mockReset();
|
||||
replySpy.mockReset();
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, "fetch" as never)
|
||||
.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { get: () => "image/png" },
|
||||
arrayBuffer: async () =>
|
||||
new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer,
|
||||
} as Response);
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch" as never).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { get: () => "image/png" },
|
||||
arrayBuffer: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer,
|
||||
} as Response);
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
const first = handler({
|
||||
|
||||
@@ -10,9 +10,7 @@ const sendChatActionSpy = vi.fn();
|
||||
type ApiStub = {
|
||||
config: { use: (arg: unknown) => void };
|
||||
sendChatAction: typeof sendChatActionSpy;
|
||||
setMyCommands: (
|
||||
commands: Array<{ command: string; description: string }>,
|
||||
) => Promise<void>;
|
||||
setMyCommands: (commands: Array<{ command: string; description: string }>) => Promise<void>;
|
||||
};
|
||||
|
||||
const apiStub: ApiStub = {
|
||||
@@ -95,24 +93,21 @@ vi.mock("../auto-reply/reply.js", () => {
|
||||
});
|
||||
|
||||
describe("telegram inbound media", () => {
|
||||
const _INBOUND_MEDIA_TEST_TIMEOUT_MS =
|
||||
process.platform === "win32" ? 30_000 : 20_000;
|
||||
const _INBOUND_MEDIA_TEST_TIMEOUT_MS = process.platform === "win32" ? 30_000 : 20_000;
|
||||
it(
|
||||
"includes location text and ctx fields for pins",
|
||||
async () => {
|
||||
const { createTelegramBot } = await import("./bot.js");
|
||||
const replyModule = await import("../auto-reply/reply.js");
|
||||
const replySpy = replyModule.getReplyFromConfig as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.getReplyFromConfig as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
onSpy.mockReset();
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
@@ -148,17 +143,15 @@ describe("telegram inbound media", () => {
|
||||
async () => {
|
||||
const { createTelegramBot } = await import("./bot.js");
|
||||
const replyModule = await import("../auto-reply/reply.js");
|
||||
const replySpy = replyModule.getReplyFromConfig as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const replySpy = replyModule.getReplyFromConfig as unknown as ReturnType<typeof vi.fn>;
|
||||
|
||||
onSpy.mockReset();
|
||||
replySpy.mockReset();
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "message",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
await handler({
|
||||
|
||||
@@ -5,10 +5,7 @@ import type { ApiClientOptions } from "grammy";
|
||||
import { Bot, webhookCallback } from "grammy";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||
import {
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
type HistoryEntry,
|
||||
} from "../auto-reply/reply/history.js";
|
||||
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "../auto-reply/reply/history.js";
|
||||
import {
|
||||
isNativeCommandsExplicitlyDisabled,
|
||||
resolveNativeCommandsEnabled,
|
||||
@@ -24,10 +21,7 @@ import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
import { getChildLogger } from "../logging.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import {
|
||||
resolveTelegramForumThreadId,
|
||||
resolveTelegramStreamMode,
|
||||
} from "./bot/helpers.js";
|
||||
import { resolveTelegramForumThreadId, resolveTelegramStreamMode } from "./bot/helpers.js";
|
||||
import type { TelegramContext, TelegramMessage } from "./bot/types.js";
|
||||
import { registerTelegramHandlers } from "./bot-handlers.js";
|
||||
import { createTelegramMessageProcessor } from "./bot-message.js";
|
||||
@@ -78,9 +72,7 @@ export function getTelegramSequentialKey(ctx: {
|
||||
messageThreadId: msg?.message_thread_id,
|
||||
});
|
||||
if (typeof chatId === "number") {
|
||||
return threadId != null
|
||||
? `telegram:${chatId}:topic:${threadId}`
|
||||
: `telegram:${chatId}`;
|
||||
return threadId != null ? `telegram:${chatId}:topic:${threadId}` : `telegram:${chatId}`;
|
||||
}
|
||||
return "telegram:unknown";
|
||||
}
|
||||
@@ -104,8 +96,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
const isBun = "Bun" in globalThis || Boolean(process?.versions?.bun);
|
||||
const shouldProvideFetch = Boolean(opts.proxyFetch) || isBun;
|
||||
const timeoutSeconds =
|
||||
typeof telegramCfg?.timeoutSeconds === "number" &&
|
||||
Number.isFinite(telegramCfg.timeoutSeconds)
|
||||
typeof telegramCfg?.timeoutSeconds === "number" && Number.isFinite(telegramCfg.timeoutSeconds)
|
||||
? Math.max(1, Math.floor(telegramCfg.timeoutSeconds))
|
||||
: undefined;
|
||||
const client: ApiClientOptions | undefined =
|
||||
@@ -124,9 +115,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
|
||||
const recentUpdates = createTelegramUpdateDedupe();
|
||||
let lastUpdateId =
|
||||
typeof opts.updateOffset?.lastUpdateId === "number"
|
||||
? opts.updateOffset.lastUpdateId
|
||||
: null;
|
||||
typeof opts.updateOffset?.lastUpdateId === "number" ? opts.updateOffset.lastUpdateId : null;
|
||||
|
||||
const recordUpdateId = (ctx: TelegramUpdateKeyContext) => {
|
||||
const updateId = resolveTelegramUpdateId(ctx);
|
||||
@@ -184,8 +173,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
});
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
const mediaMaxBytes =
|
||||
(opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 5) * 1024 * 1024;
|
||||
const mediaMaxBytes = (opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 5) * 1024 * 1024;
|
||||
const logger = getChildLogger({ module: "telegram-auto-reply" });
|
||||
let botHasTopicsEnabled: boolean | undefined;
|
||||
const resolveBotTopicsEnabled = async (ctx?: TelegramContext) => {
|
||||
@@ -241,18 +229,13 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
requireMentionOverride: opts.requireMention,
|
||||
overrideOrder: "after-config",
|
||||
});
|
||||
const resolveTelegramGroupConfig = (
|
||||
chatId: string | number,
|
||||
messageThreadId?: number,
|
||||
) => {
|
||||
const resolveTelegramGroupConfig = (chatId: string | number, messageThreadId?: number) => {
|
||||
const groups = telegramCfg.groups;
|
||||
if (!groups) return { groupConfig: undefined, topicConfig: undefined };
|
||||
const groupKey = String(chatId);
|
||||
const groupConfig = groups[groupKey] ?? groups["*"];
|
||||
const topicConfig =
|
||||
messageThreadId != null
|
||||
? groupConfig?.topics?.[String(messageThreadId)]
|
||||
: undefined;
|
||||
messageThreadId != null ? groupConfig?.topics?.[String(messageThreadId)] : undefined;
|
||||
return { groupConfig, topicConfig };
|
||||
};
|
||||
|
||||
@@ -315,9 +298,6 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
return bot;
|
||||
}
|
||||
|
||||
export function createTelegramWebhookCallback(
|
||||
bot: Bot,
|
||||
path = "/telegram-webhook",
|
||||
) {
|
||||
export function createTelegramWebhookCallback(bot: Bot, path = "/telegram-webhook") {
|
||||
return { path, handler: webhookCallback(bot, "http") };
|
||||
}
|
||||
|
||||
@@ -12,14 +12,10 @@ import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { loadWebMedia } from "../../web/media.js";
|
||||
import { markdownToTelegramHtml } from "../format.js";
|
||||
import { resolveTelegramVoiceSend } from "../voice.js";
|
||||
import {
|
||||
buildTelegramThreadParams,
|
||||
resolveTelegramReplyId,
|
||||
} from "./helpers.js";
|
||||
import { buildTelegramThreadParams, resolveTelegramReplyId } from "./helpers.js";
|
||||
import type { TelegramContext } from "./types.js";
|
||||
|
||||
const PARSE_ERR_RE =
|
||||
/can't parse entities|parse entities|find end of the entity/i;
|
||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||
|
||||
export async function deliverReplies(params: {
|
||||
replies: ReplyPayload[];
|
||||
@@ -31,15 +27,7 @@ export async function deliverReplies(params: {
|
||||
textLimit: number;
|
||||
messageThreadId?: number;
|
||||
}) {
|
||||
const {
|
||||
replies,
|
||||
chatId,
|
||||
runtime,
|
||||
bot,
|
||||
replyToMode,
|
||||
textLimit,
|
||||
messageThreadId,
|
||||
} = params;
|
||||
const { replies, chatId, runtime, bot, replyToMode, textLimit, messageThreadId } = params;
|
||||
const threadParams = buildTelegramThreadParams(messageThreadId);
|
||||
let hasReplied = false;
|
||||
for (const reply of replies) {
|
||||
@@ -47,10 +35,7 @@ export async function deliverReplies(params: {
|
||||
runtime.error?.(danger("reply missing text/media"));
|
||||
continue;
|
||||
}
|
||||
const replyToId =
|
||||
replyToMode === "off"
|
||||
? undefined
|
||||
: resolveTelegramReplyId(reply.replyToId);
|
||||
const replyToId = replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId);
|
||||
const mediaList = reply.mediaUrls?.length
|
||||
? reply.mediaUrls
|
||||
: reply.mediaUrl
|
||||
@@ -60,9 +45,7 @@ export async function deliverReplies(params: {
|
||||
for (const chunk of chunkMarkdownText(reply.text || "", textLimit)) {
|
||||
await sendTelegramText(bot, chatId, chunk, runtime, {
|
||||
replyToMessageId:
|
||||
replyToId && (replyToMode === "all" || !hasReplied)
|
||||
? replyToId
|
||||
: undefined,
|
||||
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined,
|
||||
messageThreadId,
|
||||
});
|
||||
if (replyToId && !hasReplied) {
|
||||
@@ -85,9 +68,7 @@ export async function deliverReplies(params: {
|
||||
const caption = first ? (reply.text ?? undefined) : undefined;
|
||||
first = false;
|
||||
const replyToMessageId =
|
||||
replyToId && (replyToMode === "all" || !hasReplied)
|
||||
? replyToId
|
||||
: undefined;
|
||||
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined;
|
||||
const mediaParams: Record<string, unknown> = {
|
||||
caption,
|
||||
reply_to_message_id: replyToMessageId,
|
||||
@@ -145,11 +126,7 @@ export async function resolveMedia(
|
||||
): Promise<{ path: string; contentType?: string; placeholder: string } | null> {
|
||||
const msg = ctx.message;
|
||||
const m =
|
||||
msg.photo?.[msg.photo.length - 1] ??
|
||||
msg.video ??
|
||||
msg.document ??
|
||||
msg.audio ??
|
||||
msg.voice;
|
||||
msg.photo?.[msg.photo.length - 1] ?? msg.video ?? msg.document ?? msg.audio ?? msg.voice;
|
||||
if (!m?.file_id) return null;
|
||||
const file = await ctx.getFile();
|
||||
if (!file.file_path) {
|
||||
@@ -157,9 +134,7 @@ export async function resolveMedia(
|
||||
}
|
||||
const fetchImpl = proxyFetch ?? globalThis.fetch;
|
||||
if (!fetchImpl) {
|
||||
throw new Error(
|
||||
"fetch is not available; set channels.telegram.proxy in config",
|
||||
);
|
||||
throw new Error("fetch is not available; set channels.telegram.proxy in config");
|
||||
}
|
||||
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
||||
const fetched = await fetchRemoteMedia({
|
||||
@@ -167,12 +142,7 @@ export async function resolveMedia(
|
||||
fetchImpl,
|
||||
filePathHint: file.file_path,
|
||||
});
|
||||
const saved = await saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
fetched.contentType,
|
||||
"inbound",
|
||||
maxBytes,
|
||||
);
|
||||
const saved = await saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", maxBytes);
|
||||
let placeholder = "<media:document>";
|
||||
if (msg.photo) placeholder = "<media:image>";
|
||||
else if (msg.video) placeholder = "<media:video>";
|
||||
@@ -204,9 +174,7 @@ async function sendTelegramText(
|
||||
} catch (err) {
|
||||
const errText = formatErrorMessage(err);
|
||||
if (PARSE_ERR_RE.test(errText)) {
|
||||
runtime.log?.(
|
||||
`telegram HTML parse failed; retrying without formatting: ${errText}`,
|
||||
);
|
||||
runtime.log?.(`telegram HTML parse failed; retrying without formatting: ${errText}`);
|
||||
const res = await bot.api.sendMessage(chatId, text, {
|
||||
...baseParams,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
formatLocationText,
|
||||
type NormalizedLocation,
|
||||
} from "../../channels/location.js";
|
||||
import { formatLocationText, type NormalizedLocation } from "../../channels/location.js";
|
||||
import type { TelegramAccountConfig } from "../../config/types.telegram.js";
|
||||
import type {
|
||||
TelegramLocation,
|
||||
@@ -23,9 +20,7 @@ export function resolveTelegramForumThreadId(params: {
|
||||
}
|
||||
|
||||
export function buildTelegramThreadParams(messageThreadId?: number) {
|
||||
return messageThreadId != null
|
||||
? { message_thread_id: messageThreadId }
|
||||
: undefined;
|
||||
return messageThreadId != null ? { message_thread_id: messageThreadId } : undefined;
|
||||
}
|
||||
|
||||
export function resolveTelegramStreamMode(
|
||||
@@ -36,37 +31,22 @@ export function resolveTelegramStreamMode(
|
||||
return "partial";
|
||||
}
|
||||
|
||||
export function buildTelegramGroupPeerId(
|
||||
chatId: number | string,
|
||||
messageThreadId?: number,
|
||||
) {
|
||||
return messageThreadId != null
|
||||
? `${chatId}:topic:${messageThreadId}`
|
||||
: String(chatId);
|
||||
export function buildTelegramGroupPeerId(chatId: number | string, messageThreadId?: number) {
|
||||
return messageThreadId != null ? `${chatId}:topic:${messageThreadId}` : String(chatId);
|
||||
}
|
||||
|
||||
export function buildTelegramGroupFrom(
|
||||
chatId: number | string,
|
||||
messageThreadId?: number,
|
||||
) {
|
||||
return messageThreadId != null
|
||||
? `group:${chatId}:topic:${messageThreadId}`
|
||||
: `group:${chatId}`;
|
||||
export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?: number) {
|
||||
return messageThreadId != null ? `group:${chatId}:topic:${messageThreadId}` : `group:${chatId}`;
|
||||
}
|
||||
|
||||
export function buildSenderName(msg: TelegramMessage) {
|
||||
const name =
|
||||
[msg.from?.first_name, msg.from?.last_name]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.trim() || msg.from?.username;
|
||||
[msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() ||
|
||||
msg.from?.username;
|
||||
return name || undefined;
|
||||
}
|
||||
|
||||
export function buildSenderLabel(
|
||||
msg: TelegramMessage,
|
||||
senderId?: number | string,
|
||||
) {
|
||||
export function buildSenderLabel(msg: TelegramMessage, senderId?: number | string) {
|
||||
const name = buildSenderName(msg);
|
||||
const username = msg.from?.username ? `@${msg.from.username}` : undefined;
|
||||
let label = name;
|
||||
@@ -77,9 +57,7 @@ export function buildSenderLabel(
|
||||
}
|
||||
const normalizedSenderId =
|
||||
senderId != null && `${senderId}`.trim() ? `${senderId}`.trim() : undefined;
|
||||
const fallbackId =
|
||||
normalizedSenderId ??
|
||||
(msg.from?.id != null ? String(msg.from.id) : undefined);
|
||||
const fallbackId = normalizedSenderId ?? (msg.from?.id != null ? String(msg.from.id) : undefined);
|
||||
const idPart = fallbackId ? `id:${fallbackId}` : undefined;
|
||||
if (label && idPart) return `${label} ${idPart}`;
|
||||
if (label) return label;
|
||||
@@ -92,8 +70,7 @@ export function buildGroupLabel(
|
||||
messageThreadId?: number,
|
||||
) {
|
||||
const title = msg.chat?.title;
|
||||
const topicSuffix =
|
||||
messageThreadId != null ? ` topic:${messageThreadId}` : "";
|
||||
const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
|
||||
if (title) return `${title} id:${chatId}${topicSuffix}`;
|
||||
return `group:${chatId}${topicSuffix}`;
|
||||
}
|
||||
@@ -115,10 +92,7 @@ export function hasBotMention(msg: TelegramMessage, botUsername: string) {
|
||||
const entities = msg.entities ?? msg.caption_entities ?? [];
|
||||
for (const ent of entities) {
|
||||
if (ent.type !== "mention") continue;
|
||||
const slice = (msg.text ?? msg.caption ?? "").slice(
|
||||
ent.offset,
|
||||
ent.offset + ent.length,
|
||||
);
|
||||
const slice = (msg.text ?? msg.caption ?? "").slice(ent.offset, ent.offset + ent.length);
|
||||
if (slice.toLowerCase() === `@${botUsername}`) return true;
|
||||
}
|
||||
return false;
|
||||
@@ -156,9 +130,7 @@ export function describeReplyTarget(msg: TelegramMessage) {
|
||||
};
|
||||
}
|
||||
|
||||
export function extractTelegramLocation(
|
||||
msg: TelegramMessage,
|
||||
): NormalizedLocation | null {
|
||||
export function extractTelegramLocation(msg: TelegramMessage): NormalizedLocation | null {
|
||||
const msgWithLocation = msg as {
|
||||
location?: TelegramLocation;
|
||||
venue?: TelegramVenue;
|
||||
@@ -178,8 +150,7 @@ export function extractTelegramLocation(
|
||||
}
|
||||
|
||||
if (location) {
|
||||
const isLive =
|
||||
typeof location.live_period === "number" && location.live_period > 0;
|
||||
const isLive = typeof location.live_period === "number" && location.live_period > 0;
|
||||
return {
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
downloadTelegramFile,
|
||||
getTelegramFile,
|
||||
type TelegramFileInfo,
|
||||
} from "./download.js";
|
||||
import { downloadTelegramFile, getTelegramFile, type TelegramFileInfo } from "./download.js";
|
||||
|
||||
describe("telegram download", () => {
|
||||
it("fetches file info", async () => {
|
||||
const json = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ok: true, result: { file_path: "photos/1.jpg" } });
|
||||
const json = vi.fn().mockResolvedValue({ ok: true, result: { file_path: "photos/1.jpg" } });
|
||||
vi.spyOn(global, "fetch" as never).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
|
||||
@@ -8,10 +8,7 @@ export type TelegramFileInfo = {
|
||||
file_path?: string;
|
||||
};
|
||||
|
||||
export async function getTelegramFile(
|
||||
token: string,
|
||||
fileId: string,
|
||||
): Promise<TelegramFileInfo> {
|
||||
export async function getTelegramFile(token: string, fileId: string): Promise<TelegramFileInfo> {
|
||||
const res = await fetch(
|
||||
`https://api.telegram.org/bot${token}/getFile?file_id=${encodeURIComponent(fileId)}`,
|
||||
);
|
||||
|
||||
@@ -5,10 +5,7 @@ import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js";
|
||||
|
||||
describe("resolveTelegramDraftStreamingChunking", () => {
|
||||
it("uses smaller defaults than block streaming", () => {
|
||||
const chunking = resolveTelegramDraftStreamingChunking(
|
||||
undefined,
|
||||
"default",
|
||||
);
|
||||
const chunking = resolveTelegramDraftStreamingChunking(undefined, "default");
|
||||
expect(chunking).toEqual({
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
|
||||
@@ -14,8 +14,7 @@ export function resolveTelegramDraftStreamingChunking(
|
||||
maxChars: number;
|
||||
breakPreference: "paragraph" | "newline" | "sentence";
|
||||
} {
|
||||
const providerChunkLimit =
|
||||
getChannelDock("telegram")?.outbound?.textChunkLimit;
|
||||
const providerChunkLimit = getChannelDock("telegram")?.outbound?.textChunkLimit;
|
||||
const textLimit = resolveTextChunkLimit(cfg, "telegram", accountId, {
|
||||
fallbackLimit: providerChunkLimit,
|
||||
});
|
||||
@@ -35,8 +34,7 @@ export function resolveTelegramDraftStreamingChunking(
|
||||
);
|
||||
const minChars = Math.min(minRequested, maxChars);
|
||||
const breakPreference =
|
||||
draftCfg?.breakPreference === "newline" ||
|
||||
draftCfg?.breakPreference === "sentence"
|
||||
draftCfg?.breakPreference === "newline" || draftCfg?.breakPreference === "sentence"
|
||||
? draftCfg.breakPreference
|
||||
: "paragraph";
|
||||
return { minChars, maxChars, breakPreference };
|
||||
|
||||
@@ -19,14 +19,9 @@ export function createTelegramDraftStream(params: {
|
||||
log?: (message: string) => void;
|
||||
warn?: (message: string) => void;
|
||||
}): TelegramDraftStream {
|
||||
const maxChars = Math.min(
|
||||
params.maxChars ?? TELEGRAM_DRAFT_MAX_CHARS,
|
||||
TELEGRAM_DRAFT_MAX_CHARS,
|
||||
);
|
||||
const maxChars = Math.min(params.maxChars ?? TELEGRAM_DRAFT_MAX_CHARS, TELEGRAM_DRAFT_MAX_CHARS);
|
||||
const throttleMs = Math.max(50, params.throttleMs ?? DEFAULT_THROTTLE_MS);
|
||||
const rawDraftId = Number.isFinite(params.draftId)
|
||||
? Math.trunc(params.draftId)
|
||||
: 1;
|
||||
const rawDraftId = Number.isFinite(params.draftId) ? Math.trunc(params.draftId) : 1;
|
||||
const draftId = rawDraftId === 0 ? 1 : Math.abs(rawDraftId);
|
||||
const chatId = params.chatId;
|
||||
const threadParams =
|
||||
@@ -49,9 +44,7 @@ export function createTelegramDraftStream(params: {
|
||||
// Drafts are capped at 4096 chars. Stop streaming once we exceed the cap
|
||||
// so we don't keep sending failing updates or a truncated preview.
|
||||
stopped = true;
|
||||
params.warn?.(
|
||||
`telegram draft stream stopped (draft length ${trimmed.length} > ${maxChars})`,
|
||||
);
|
||||
params.warn?.(`telegram draft stream stopped (draft length ${trimmed.length} > ${maxChars})`);
|
||||
return;
|
||||
}
|
||||
if (trimmed === lastSentText) return;
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
// Bun-only: force native fetch to avoid grammY's Node shim under Bun.
|
||||
export function resolveTelegramFetch(
|
||||
proxyFetch?: typeof fetch,
|
||||
): typeof fetch | undefined {
|
||||
export function resolveTelegramFetch(proxyFetch?: typeof fetch): typeof fetch | undefined {
|
||||
if (proxyFetch) return proxyFetch;
|
||||
const fetchImpl = globalThis.fetch;
|
||||
const isBun = "Bun" in globalThis || Boolean(process?.versions?.bun);
|
||||
if (!isBun) return undefined;
|
||||
if (!fetchImpl) {
|
||||
throw new Error(
|
||||
"fetch is not available; set channels.telegram.proxy in config",
|
||||
);
|
||||
throw new Error("fetch is not available; set channels.telegram.proxy in config");
|
||||
}
|
||||
return fetchImpl;
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ function getLinkStack(env: RenderEnv): boolean[] {
|
||||
return env.telegramLinkStack;
|
||||
}
|
||||
|
||||
md.renderer.rules.text = (tokens, idx) =>
|
||||
escapeHtml(tokens[idx]?.content ?? "");
|
||||
md.renderer.rules.text = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
|
||||
|
||||
md.renderer.rules.softbreak = () => "\n";
|
||||
md.renderer.rules.hardbreak = () => "\n";
|
||||
@@ -110,10 +109,8 @@ md.renderer.rules.image = (tokens, idx) => {
|
||||
return escapeHtml(alt);
|
||||
};
|
||||
|
||||
md.renderer.rules.html_block = (tokens, idx) =>
|
||||
escapeHtml(tokens[idx]?.content ?? "");
|
||||
md.renderer.rules.html_inline = (tokens, idx) =>
|
||||
escapeHtml(tokens[idx]?.content ?? "");
|
||||
md.renderer.rules.html_block = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
|
||||
md.renderer.rules.html_inline = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
|
||||
|
||||
md.renderer.rules.table_open = () => "";
|
||||
md.renderer.rules.table_close = () => "";
|
||||
|
||||
@@ -7,10 +7,7 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import { createTelegramBot } from "./bot.js";
|
||||
import { makeProxyFetch } from "./proxy.js";
|
||||
import {
|
||||
readTelegramUpdateOffset,
|
||||
writeTelegramUpdateOffset,
|
||||
} from "./update-offset-store.js";
|
||||
import { readTelegramUpdateOffset, writeTelegramUpdateOffset } from "./update-offset-store.js";
|
||||
import { startTelegramWebhook } from "./webhook.js";
|
||||
|
||||
export type MonitorTelegramOpts = {
|
||||
@@ -27,9 +24,7 @@ export type MonitorTelegramOpts = {
|
||||
webhookUrl?: string;
|
||||
};
|
||||
|
||||
export function createTelegramRunnerOptions(
|
||||
cfg: ClawdbotConfig,
|
||||
): RunOptions<unknown> {
|
||||
export function createTelegramRunnerOptions(cfg: ClawdbotConfig): RunOptions<unknown> {
|
||||
return {
|
||||
sink: {
|
||||
concurrency: cfg.agents?.defaults?.maxConcurrent ?? 1,
|
||||
@@ -85,9 +80,7 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
||||
|
||||
const proxyFetch =
|
||||
opts.proxyFetch ??
|
||||
(account.config.proxy
|
||||
? makeProxyFetch(account.config.proxy as string)
|
||||
: undefined);
|
||||
(account.config.proxy ? makeProxyFetch(account.config.proxy as string) : undefined);
|
||||
|
||||
let lastUpdateId = await readTelegramUpdateOffset({
|
||||
accountId: account.accountId,
|
||||
@@ -159,13 +152,8 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
||||
throw err;
|
||||
}
|
||||
restartAttempts += 1;
|
||||
const delayMs = computeBackoff(
|
||||
TELEGRAM_POLL_RESTART_POLICY,
|
||||
restartAttempts,
|
||||
);
|
||||
log(
|
||||
`Telegram getUpdates conflict; retrying in ${formatDurationMs(delayMs)}.`,
|
||||
);
|
||||
const delayMs = computeBackoff(TELEGRAM_POLL_RESTART_POLICY, restartAttempts);
|
||||
log(`Telegram getUpdates conflict; retrying in ${formatDurationMs(delayMs)}.`);
|
||||
try {
|
||||
await sleepWithAbort(delayMs, opts.abortSignal);
|
||||
} catch (sleepErr) {
|
||||
|
||||
@@ -104,9 +104,7 @@ export async function resolveTelegramEffectiveAllowFrom(params: {
|
||||
.filter(Boolean)
|
||||
.map((v) => v.replace(/^(telegram|tg):/i, ""))
|
||||
.filter((v) => v !== "*");
|
||||
const cfgGroupAllowFrom = (
|
||||
params.cfg.channels?.telegram?.groupAllowFrom ?? []
|
||||
)
|
||||
const cfgGroupAllowFrom = (params.cfg.channels?.telegram?.groupAllowFrom ?? [])
|
||||
.map((v) => String(v).trim())
|
||||
.filter(Boolean)
|
||||
.map((v) => v.replace(/^(telegram|tg):/i, ""))
|
||||
|
||||
@@ -61,11 +61,7 @@ export async function probeTelegram(
|
||||
|
||||
// Try to fetch webhook info, but don't fail health if it errors.
|
||||
try {
|
||||
const webhookRes = await fetchWithTimeout(
|
||||
`${base}/getWebhookInfo`,
|
||||
timeoutMs,
|
||||
fetcher,
|
||||
);
|
||||
const webhookRes = await fetchWithTimeout(`${base}/getWebhookInfo`, timeoutMs, fetcher);
|
||||
const webhookJson = (await webhookRes.json()) as {
|
||||
ok?: boolean;
|
||||
result?: { url?: string; has_custom_certificate?: boolean };
|
||||
|
||||
@@ -56,16 +56,11 @@ describe("buildInlineKeyboard", () => {
|
||||
});
|
||||
|
||||
// First call: with HTML + thread params
|
||||
expect(sendMessage).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
chatId,
|
||||
"<i>bad markdown</i>",
|
||||
{
|
||||
parse_mode: "HTML",
|
||||
message_thread_id: 271,
|
||||
reply_to_message_id: 100,
|
||||
},
|
||||
);
|
||||
expect(sendMessage).toHaveBeenNthCalledWith(1, chatId, "<i>bad markdown</i>", {
|
||||
parse_mode: "HTML",
|
||||
message_thread_id: 271,
|
||||
reply_to_message_id: 100,
|
||||
});
|
||||
// Second call: plain text BUT still with thread params (critical!)
|
||||
expect(sendMessage).toHaveBeenNthCalledWith(2, chatId, "_bad markdown_", {
|
||||
message_thread_id: 271,
|
||||
@@ -116,9 +111,7 @@ describe("reactMessageTelegram", () => {
|
||||
api,
|
||||
});
|
||||
|
||||
expect(setMessageReaction).toHaveBeenCalledWith("123", 456, [
|
||||
{ type: "emoji", emoji: "✅" },
|
||||
]);
|
||||
expect(setMessageReaction).toHaveBeenCalledWith("123", 456, [{ type: "emoji", emoji: "✅" }]);
|
||||
});
|
||||
|
||||
it("removes reactions when emoji is empty", async () => {
|
||||
|
||||
@@ -207,12 +207,12 @@ describe("sendMessageTelegram", () => {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram(chatId, "hi", { token: "tok", api }),
|
||||
).rejects.toThrow(/chat not found/i);
|
||||
await expect(
|
||||
sendMessageTelegram(chatId, "hi", { token: "tok", api }),
|
||||
).rejects.toThrow(/chat_id=123/);
|
||||
await expect(sendMessageTelegram(chatId, "hi", { token: "tok", api })).rejects.toThrow(
|
||||
/chat not found/i,
|
||||
);
|
||||
await expect(sendMessageTelegram(chatId, "hi", { token: "tok", api })).rejects.toThrow(
|
||||
/chat_id=123/,
|
||||
);
|
||||
});
|
||||
|
||||
it("retries on transient errors with retry_after", async () => {
|
||||
@@ -248,9 +248,7 @@ describe("sendMessageTelegram", () => {
|
||||
|
||||
it("does not retry on non-transient errors", async () => {
|
||||
const chatId = "123";
|
||||
const sendMessage = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("400: Bad Request"));
|
||||
const sendMessage = vi.fn().mockRejectedValue(new Error("400: Bad Request"));
|
||||
const api = { sendMessage } as unknown as {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
@@ -430,14 +428,10 @@ describe("sendMessageTelegram", () => {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
await sendMessageTelegram(
|
||||
`telegram:group:${chatId}:topic:271`,
|
||||
"hello forum",
|
||||
{
|
||||
token: "tok",
|
||||
api,
|
||||
},
|
||||
);
|
||||
await sendMessageTelegram(`telegram:group:${chatId}:topic:271`, "hello forum", {
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith(chatId, "hello forum", {
|
||||
parse_mode: "HTML",
|
||||
|
||||
@@ -17,10 +17,7 @@ import { loadWebMedia } from "../web/media.js";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import { resolveTelegramFetch } from "./fetch.js";
|
||||
import { markdownToTelegramHtml } from "./format.js";
|
||||
import {
|
||||
parseTelegramTarget,
|
||||
stripTelegramInternalPrefixes,
|
||||
} from "./targets.js";
|
||||
import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js";
|
||||
import { resolveTelegramVoiceSend } from "./voice.js";
|
||||
|
||||
type TelegramSendOpts = {
|
||||
@@ -55,13 +52,9 @@ type TelegramReactionOpts = {
|
||||
retry?: RetryConfig;
|
||||
};
|
||||
|
||||
const PARSE_ERR_RE =
|
||||
/can't parse entities|parse entities|find end of the entity/i;
|
||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||
|
||||
function resolveToken(
|
||||
explicit: string | undefined,
|
||||
params: { accountId: string; token: string },
|
||||
) {
|
||||
function resolveToken(explicit: string | undefined, params: { accountId: string; token: string }) {
|
||||
if (explicit?.trim()) return explicit.trim();
|
||||
if (!params.token) {
|
||||
throw new Error(
|
||||
@@ -156,9 +149,7 @@ export async function sendMessageTelegram(
|
||||
const client: ApiClientOptions | undefined =
|
||||
fetchImpl || timeoutSeconds
|
||||
? {
|
||||
...(fetchImpl
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: {}),
|
||||
...(fetchImpl ? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] } : {}),
|
||||
...(timeoutSeconds ? { timeoutSeconds } : {}),
|
||||
}
|
||||
: undefined;
|
||||
@@ -170,9 +161,7 @@ export async function sendMessageTelegram(
|
||||
// Only include these if actually provided to keep API calls clean.
|
||||
const threadParams: Record<string, number> = {};
|
||||
const messageThreadId =
|
||||
opts.messageThreadId != null
|
||||
? opts.messageThreadId
|
||||
: target.messageThreadId;
|
||||
opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId;
|
||||
if (messageThreadId != null) {
|
||||
threadParams.message_thread_id = Math.trunc(messageThreadId);
|
||||
}
|
||||
@@ -187,8 +176,7 @@ export async function sendMessageTelegram(
|
||||
});
|
||||
|
||||
const wrapChatNotFound = (err: unknown) => {
|
||||
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err)))
|
||||
return err;
|
||||
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) return err;
|
||||
return new Error(
|
||||
[
|
||||
`Telegram send failed: chat not found (chat_id=${chatId}).`,
|
||||
@@ -205,10 +193,7 @@ export async function sendMessageTelegram(
|
||||
contentType: media.contentType,
|
||||
fileName: media.fileName,
|
||||
});
|
||||
const fileName =
|
||||
media.fileName ??
|
||||
(isGif ? "animation.gif" : inferFilename(kind)) ??
|
||||
"file";
|
||||
const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind)) ?? "file";
|
||||
const file = new InputFile(media.buffer, fileName);
|
||||
const caption = text?.trim() || undefined;
|
||||
const mediaParams = hasThreadParams
|
||||
@@ -229,26 +214,23 @@ export async function sendMessageTelegram(
|
||||
| Awaited<ReturnType<typeof api.sendAnimation>>
|
||||
| Awaited<ReturnType<typeof api.sendDocument>>;
|
||||
if (isGif) {
|
||||
result = await request(
|
||||
() => api.sendAnimation(chatId, file, mediaParams),
|
||||
"animation",
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
result = await request(() => api.sendAnimation(chatId, file, mediaParams), "animation").catch(
|
||||
(err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
},
|
||||
);
|
||||
} else if (kind === "image") {
|
||||
result = await request(
|
||||
() => api.sendPhoto(chatId, file, mediaParams),
|
||||
"photo",
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
result = await request(() => api.sendPhoto(chatId, file, mediaParams), "photo").catch(
|
||||
(err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
},
|
||||
);
|
||||
} else if (kind === "video") {
|
||||
result = await request(
|
||||
() => api.sendVideo(chatId, file, mediaParams),
|
||||
"video",
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
result = await request(() => api.sendVideo(chatId, file, mediaParams), "video").catch(
|
||||
(err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
},
|
||||
);
|
||||
} else if (kind === "audio") {
|
||||
const { useVoice } = resolveTelegramVoiceSend({
|
||||
wantsVoice: opts.asVoice === true, // default false (backward compatible)
|
||||
@@ -257,27 +239,24 @@ export async function sendMessageTelegram(
|
||||
logFallback: logVerbose,
|
||||
});
|
||||
if (useVoice) {
|
||||
result = await request(
|
||||
() => api.sendVoice(chatId, file, mediaParams),
|
||||
"voice",
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
result = await request(() => api.sendVoice(chatId, file, mediaParams), "voice").catch(
|
||||
(err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
result = await request(
|
||||
() => api.sendAudio(chatId, file, mediaParams),
|
||||
"audio",
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
result = await request(() => api.sendAudio(chatId, file, mediaParams), "audio").catch(
|
||||
(err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
result = await request(
|
||||
() => api.sendDocument(chatId, file, mediaParams),
|
||||
"document",
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
result = await request(() => api.sendDocument(chatId, file, mediaParams), "document").catch(
|
||||
(err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
const messageId = String(result?.message_id ?? "unknown");
|
||||
recordChannelActivity({
|
||||
@@ -302,38 +281,35 @@ export async function sendMessageTelegram(
|
||||
parse_mode: "HTML" as const,
|
||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
};
|
||||
const res = await request(
|
||||
() => api.sendMessage(chatId, htmlText, textParams),
|
||||
"message",
|
||||
).catch(async (err) => {
|
||||
// Telegram rejects malformed HTML (e.g., unsupported tags or entities).
|
||||
// When that happens, fall back to plain text so the message still delivers.
|
||||
const errText = formatErrorMessage(err);
|
||||
if (PARSE_ERR_RE.test(errText)) {
|
||||
if (opts.verbose) {
|
||||
console.warn(
|
||||
`telegram HTML parse failed, retrying as plain text: ${errText}`,
|
||||
);
|
||||
const res = await request(() => api.sendMessage(chatId, htmlText, textParams), "message").catch(
|
||||
async (err) => {
|
||||
// Telegram rejects malformed HTML (e.g., unsupported tags or entities).
|
||||
// When that happens, fall back to plain text so the message still delivers.
|
||||
const errText = formatErrorMessage(err);
|
||||
if (PARSE_ERR_RE.test(errText)) {
|
||||
if (opts.verbose) {
|
||||
console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`);
|
||||
}
|
||||
const plainParams =
|
||||
hasThreadParams || replyMarkup
|
||||
? {
|
||||
...threadParams,
|
||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
}
|
||||
: undefined;
|
||||
return await request(
|
||||
() =>
|
||||
plainParams
|
||||
? api.sendMessage(chatId, text, plainParams)
|
||||
: api.sendMessage(chatId, text),
|
||||
"message-plain",
|
||||
).catch((err2) => {
|
||||
throw wrapChatNotFound(err2);
|
||||
});
|
||||
}
|
||||
const plainParams =
|
||||
hasThreadParams || replyMarkup
|
||||
? {
|
||||
...threadParams,
|
||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
}
|
||||
: undefined;
|
||||
return await request(
|
||||
() =>
|
||||
plainParams
|
||||
? api.sendMessage(chatId, text, plainParams)
|
||||
: api.sendMessage(chatId, text),
|
||||
"message-plain",
|
||||
).catch((err2) => {
|
||||
throw wrapChatNotFound(err2);
|
||||
});
|
||||
}
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
throw wrapChatNotFound(err);
|
||||
},
|
||||
);
|
||||
const messageId = String(res?.message_id ?? "unknown");
|
||||
recordChannelActivity({
|
||||
channel: "telegram",
|
||||
@@ -378,10 +354,7 @@ export async function reactMessageTelegram(
|
||||
if (typeof api.setMessageReaction !== "function") {
|
||||
throw new Error("Telegram reactions are unavailable in this bot API.");
|
||||
}
|
||||
await request(
|
||||
() => api.setMessageReaction(chatId, messageId, reactions),
|
||||
"reaction",
|
||||
);
|
||||
await request(() => api.setMessageReaction(chatId, messageId, reactions), "reaction");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
parseTelegramTarget,
|
||||
stripTelegramInternalPrefixes,
|
||||
} from "./targets.js";
|
||||
import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js";
|
||||
|
||||
describe("stripTelegramInternalPrefixes", () => {
|
||||
it("strips telegram prefix", () => {
|
||||
@@ -11,9 +8,7 @@ describe("stripTelegramInternalPrefixes", () => {
|
||||
});
|
||||
|
||||
it("strips telegram+group prefixes", () => {
|
||||
expect(stripTelegramInternalPrefixes("telegram:group:-100123")).toBe(
|
||||
"-100123",
|
||||
);
|
||||
expect(stripTelegramInternalPrefixes("telegram:group:-100123")).toBe("-100123");
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
@@ -62,9 +57,7 @@ describe("parseTelegramTarget", () => {
|
||||
});
|
||||
|
||||
it("strips internal prefixes before parsing", () => {
|
||||
expect(
|
||||
parseTelegramTarget("telegram:group:-1001234567890:topic:456"),
|
||||
).toEqual({
|
||||
expect(parseTelegramTarget("telegram:group:-1001234567890:topic:456")).toEqual({
|
||||
chatId: "-1001234567890",
|
||||
messageThreadId: 456,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
|
||||
export type TelegramTokenSource = "env" | "tokenFile" | "config" | "none";
|
||||
|
||||
@@ -44,9 +41,7 @@ export function resolveTelegramToken(
|
||||
}
|
||||
} catch (err) {
|
||||
opts.logMissingFile?.(
|
||||
`channels.telegram.accounts.${accountId}.tokenFile read failed: ${String(
|
||||
err,
|
||||
)}`,
|
||||
`channels.telegram.accounts.${accountId}.tokenFile read failed: ${String(err)}`,
|
||||
);
|
||||
return { token: "", source: "none" };
|
||||
}
|
||||
@@ -59,9 +54,7 @@ export function resolveTelegramToken(
|
||||
}
|
||||
|
||||
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const envToken = allowEnv
|
||||
? (opts.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim()
|
||||
: "";
|
||||
const envToken = allowEnv ? (opts.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim() : "";
|
||||
if (envToken) {
|
||||
return { token: envToken, source: "env" };
|
||||
}
|
||||
@@ -69,9 +62,7 @@ export function resolveTelegramToken(
|
||||
const tokenFile = telegramCfg?.tokenFile?.trim();
|
||||
if (tokenFile && allowEnv) {
|
||||
if (!fs.existsSync(tokenFile)) {
|
||||
opts.logMissingFile?.(
|
||||
`channels.telegram.tokenFile not found: ${tokenFile}`,
|
||||
);
|
||||
opts.logMissingFile?.(`channels.telegram.tokenFile not found: ${tokenFile}`);
|
||||
return { token: "", source: "none" };
|
||||
}
|
||||
try {
|
||||
@@ -80,9 +71,7 @@ export function resolveTelegramToken(
|
||||
return { token, source: "tokenFile" };
|
||||
}
|
||||
} catch (err) {
|
||||
opts.logMissingFile?.(
|
||||
`channels.telegram.tokenFile read failed: ${String(err)}`,
|
||||
);
|
||||
opts.logMissingFile?.(`channels.telegram.tokenFile read failed: ${String(err)}`);
|
||||
return { token: "", source: "none" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
readTelegramUpdateOffset,
|
||||
writeTelegramUpdateOffset,
|
||||
} from "./update-offset-store.js";
|
||||
import { readTelegramUpdateOffset, writeTelegramUpdateOffset } from "./update-offset-store.js";
|
||||
|
||||
async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
|
||||
const previous = process.env.CLAWDBOT_STATE_DIR;
|
||||
@@ -25,18 +22,14 @@ async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
|
||||
describe("telegram update offset store", () => {
|
||||
it("persists and reloads the last update id", async () => {
|
||||
await withTempStateDir(async () => {
|
||||
expect(
|
||||
await readTelegramUpdateOffset({ accountId: "primary" }),
|
||||
).toBeNull();
|
||||
expect(await readTelegramUpdateOffset({ accountId: "primary" })).toBeNull();
|
||||
|
||||
await writeTelegramUpdateOffset({
|
||||
accountId: "primary",
|
||||
updateId: 421,
|
||||
});
|
||||
|
||||
expect(await readTelegramUpdateOffset({ accountId: "primary" })).toBe(
|
||||
421,
|
||||
);
|
||||
expect(await readTelegramUpdateOffset({ accountId: "primary" })).toBe(421);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,10 +31,7 @@ function safeParseState(raw: string): TelegramUpdateOffsetState | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as TelegramUpdateOffsetState;
|
||||
if (parsed?.version !== STORE_VERSION) return null;
|
||||
if (
|
||||
parsed.lastUpdateId !== null &&
|
||||
typeof parsed.lastUpdateId !== "number"
|
||||
) {
|
||||
if (parsed.lastUpdateId !== null && typeof parsed.lastUpdateId !== "number") {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
@@ -47,10 +44,7 @@ export async function readTelegramUpdateOffset(params: {
|
||||
accountId?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<number | null> {
|
||||
const filePath = resolveTelegramUpdateOffsetPath(
|
||||
params.accountId,
|
||||
params.env,
|
||||
);
|
||||
const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env);
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
const parsed = safeParseState(raw);
|
||||
@@ -67,16 +61,10 @@ export async function writeTelegramUpdateOffset(params: {
|
||||
updateId: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<void> {
|
||||
const filePath = resolveTelegramUpdateOffsetPath(
|
||||
params.accountId,
|
||||
params.env,
|
||||
);
|
||||
const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env);
|
||||
const dir = path.dirname(filePath);
|
||||
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
||||
const tmp = path.join(
|
||||
dir,
|
||||
`${path.basename(filePath)}.${crypto.randomUUID()}.tmp`,
|
||||
);
|
||||
const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
|
||||
const payload: TelegramUpdateOffsetState = {
|
||||
version: STORE_VERSION,
|
||||
lastUpdateId: params.updateId,
|
||||
|
||||
@@ -3,10 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { startTelegramWebhook } from "./webhook.js";
|
||||
|
||||
const handlerSpy = vi.fn(
|
||||
(
|
||||
_req: unknown,
|
||||
res: { writeHead: (status: number) => void; end: (body?: string) => void },
|
||||
) => {
|
||||
(_req: unknown, res: { writeHead: (status: number) => void; end: (body?: string) => void }) => {
|
||||
res.writeHead(200);
|
||||
res.end("ok");
|
||||
},
|
||||
|
||||
@@ -59,8 +59,7 @@ export async function startTelegramWebhook(opts: {
|
||||
});
|
||||
|
||||
const publicUrl =
|
||||
opts.publicUrl ??
|
||||
`http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
|
||||
opts.publicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
|
||||
|
||||
await bot.api.setWebhook(publicUrl, {
|
||||
secret_token: opts.secret,
|
||||
|
||||
Reference in New Issue
Block a user