mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 12:21:24 +00:00
refactor(telegram): split bot handlers
This commit is contained in:
269
src/telegram/bot-handlers.ts
Normal file
269
src/telegram/bot-handlers.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
// @ts-nocheck
|
||||
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 { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js";
|
||||
import { readTelegramAllowFromStore } from "./pairing-store.js";
|
||||
|
||||
export const registerTelegramHandlers = ({
|
||||
bot,
|
||||
opts,
|
||||
runtime,
|
||||
mediaMaxBytes,
|
||||
telegramCfg,
|
||||
groupAllowFrom,
|
||||
resolveGroupPolicy,
|
||||
resolveTelegramGroupConfig,
|
||||
shouldSkipUpdate,
|
||||
processMessage,
|
||||
logger,
|
||||
}) => {
|
||||
const mediaGroupBuffer = new Map<string, MediaGroupEntry>();
|
||||
let mediaGroupProcessing: Promise<void> = Promise.resolve();
|
||||
|
||||
const processMediaGroup = async (entry: MediaGroupEntry) => {
|
||||
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 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,
|
||||
);
|
||||
if (media) {
|
||||
allMedia.push({ path: media.path, contentType: media.contentType });
|
||||
}
|
||||
}
|
||||
|
||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||
await processMessage(primaryEntry.ctx, allMedia, storeAllowFrom);
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`media group handler failed: ${String(err)}`));
|
||||
}
|
||||
};
|
||||
|
||||
bot.on("callback_query", async (ctx) => {
|
||||
const callback = ctx.callbackQuery;
|
||||
if (!callback) return;
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
try {
|
||||
const data = (callback.data ?? "").trim();
|
||||
const callbackMessage = callback.message;
|
||||
if (!data || !callbackMessage) return;
|
||||
|
||||
const syntheticMessage: TelegramMessage = {
|
||||
...callbackMessage,
|
||||
from: callback.from,
|
||||
text: data,
|
||||
caption: undefined,
|
||||
caption_entities: undefined,
|
||||
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 },
|
||||
);
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`callback handler failed: ${String(err)}`));
|
||||
} finally {
|
||||
await bot.api.answerCallbackQuery(callback.id).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
bot.on("message", async (ctx) => {
|
||||
try {
|
||||
const msg = ctx.message;
|
||||
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 resolvedThreadId = resolveTelegramForumThreadId({
|
||||
isForum,
|
||||
messageThreadId,
|
||||
});
|
||||
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";
|
||||
|
||||
if (isGroup) {
|
||||
if (groupConfig?.enabled === false) {
|
||||
logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
|
||||
return;
|
||||
}
|
||||
if (topicConfig?.enabled === false) {
|
||||
logVerbose(
|
||||
`Blocked telegram topic ${chatId} (${resolvedThreadId ?? "unknown"}) (topic disabled)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (hasGroupAllowOverride) {
|
||||
const senderId = msg.from?.id;
|
||||
const senderUsername = msg.from?.username ?? "";
|
||||
const allowed =
|
||||
senderId != null &&
|
||||
isSenderAllowed({
|
||||
allow: effectiveGroupAllow,
|
||||
senderId: String(senderId),
|
||||
senderUsername,
|
||||
});
|
||||
if (!allowed) {
|
||||
logVerbose(
|
||||
`Blocked telegram group sender ${senderId ?? "unknown"} (group allowFrom override)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Group policy filtering: controls how group messages are handled
|
||||
// - "open": groups bypass allowFrom, only mention-gating applies
|
||||
// - "disabled": block all group messages entirely
|
||||
// - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
|
||||
const groupPolicy = telegramCfg.groupPolicy ?? "open";
|
||||
if (groupPolicy === "disabled") {
|
||||
logVerbose(`Blocked telegram group message (groupPolicy: disabled)`);
|
||||
return;
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
// 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)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!effectiveGroupAllow.hasEntries) {
|
||||
logVerbose(
|
||||
"Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const senderUsername = msg.from?.username ?? "";
|
||||
if (
|
||||
!isSenderAllowed({
|
||||
allow: effectiveGroupAllow,
|
||||
senderId: String(senderId),
|
||||
senderUsername,
|
||||
})
|
||||
) {
|
||||
logVerbose(
|
||||
`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Group allowlist based on configured group IDs.
|
||||
const groupAllowlist = resolveGroupPolicy(chatId);
|
||||
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
||||
logger.info(
|
||||
{ chatId, title: msg.chat.title, reason: "not-allowed" },
|
||||
"skipping group message",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Media group handling - buffer multi-image messages
|
||||
const mediaGroupId = (msg as { media_group_id?: string }).media_group_id;
|
||||
if (mediaGroupId) {
|
||||
const existing = mediaGroupBuffer.get(mediaGroupId);
|
||||
if (existing) {
|
||||
clearTimeout(existing.timer);
|
||||
existing.messages.push({ msg, ctx });
|
||||
existing.timer = setTimeout(async () => {
|
||||
mediaGroupBuffer.delete(mediaGroupId);
|
||||
mediaGroupProcessing = mediaGroupProcessing
|
||||
.then(async () => {
|
||||
await processMediaGroup(existing);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
await mediaGroupProcessing;
|
||||
}, MEDIA_GROUP_TIMEOUT_MS);
|
||||
} else {
|
||||
const entry: MediaGroupEntry = {
|
||||
messages: [{ msg, ctx }],
|
||||
timer: setTimeout(async () => {
|
||||
mediaGroupBuffer.delete(mediaGroupId);
|
||||
mediaGroupProcessing = mediaGroupProcessing
|
||||
.then(async () => {
|
||||
await processMediaGroup(entry);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
await mediaGroupProcessing;
|
||||
}, MEDIA_GROUP_TIMEOUT_MS),
|
||||
};
|
||||
mediaGroupBuffer.set(mediaGroupId, entry);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let media: Awaited<ReturnType<typeof resolveMedia>> = null;
|
||||
try {
|
||||
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 },
|
||||
)
|
||||
.catch(() => {});
|
||||
logger.warn({ chatId, error: errMsg }, "media exceeds size limit");
|
||||
return;
|
||||
}
|
||||
throw mediaErr;
|
||||
}
|
||||
const allMedia = media
|
||||
? [{ path: media.path, contentType: media.contentType }]
|
||||
: [];
|
||||
await processMessage(ctx, allMedia, storeAllowFrom);
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`handler failed: ${String(err)}`));
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user