chore: migrate to oxlint and oxfmt

Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-14 14:31:43 +00:00
parent 912ebffc63
commit c379191f80
1480 changed files with 28608 additions and 43547 deletions

View File

@@ -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);

View File

@@ -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({

View File

@@ -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,

View File

@@ -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}`);
};

View File

@@ -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)}`));

View File

@@ -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}"`,
);

View File

@@ -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) => {

View File

@@ -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");

View File

@@ -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") {

View File

@@ -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: {

View File

@@ -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 },

View File

@@ -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: {

View File

@@ -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({

View File

@@ -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" },

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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({

View File

@@ -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({

View File

@@ -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") };
}

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)}`,
);

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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 = () => "";

View File

@@ -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) {

View File

@@ -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, ""))

View File

@@ -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 };

View File

@@ -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 () => {

View File

@@ -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",

View File

@@ -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 };
}

View File

@@ -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,
});

View File

@@ -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" };
}
}

View File

@@ -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);
});
});
});

View File

@@ -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,

View File

@@ -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");
},

View File

@@ -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,