270 lines
9.0 KiB
TypeScript
270 lines
9.0 KiB
TypeScript
// @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)}`));
|
|
}
|
|
});
|
|
};
|