
















@@ -238,6 +238,110 @@ export const registerTelegramHandlers = ({
238238 : async () => ({});
239239return { message, me: ctx.me, getFile };
240240};
241+242+const MULTI_SELECT_PREFIX = "OC_MULTI|";
243+const SELECT_PREFIX = "OC_SELECT|";
244+const SELECTED_PREFIX = "✅ ";
245+246+type TelegramCallbackButton = {
247+text: string;
248+callback_data: string;
249+style?: "danger" | "success" | "primary";
250+};
251+252+const cloneInlineKeyboardButtons = (message: Message): TelegramCallbackButton[][] => {
253+const rows = (message as { reply_markup?: { inline_keyboard?: unknown } }).reply_markup
254+?.inline_keyboard;
255+if (!Array.isArray(rows)) {
256+return [];
257+}
258+return rows
259+.map((row) =>
260+Array.isArray(row)
261+ ? row
262+.map((button): TelegramCallbackButton | null => {
263+const candidate = button as {
264+text?: unknown;
265+callback_data?: unknown;
266+style?: unknown;
267+};
268+if (
269+typeof candidate.text !== "string" ||
270+typeof candidate.callback_data !== "string"
271+) {
272+return null;
273+}
274+const style =
275+candidate.style === "danger" ||
276+candidate.style === "success" ||
277+candidate.style === "primary"
278+ ? candidate.style
279+ : undefined;
280+return {
281+text: candidate.text,
282+callback_data: candidate.callback_data,
283+ ...(style ? { style } : {}),
284+};
285+})
286+.filter((button): button is TelegramCallbackButton => button !== null)
287+ : [],
288+)
289+.filter((row) => row.length > 0);
290+};
291+const stripMultiSelectPrefix = (text: string): string => text.replace(/^✅\s*/, "");
292+const isSelectedMultiButton = (button: TelegramCallbackButton): boolean =>
293+/^✅\s*/.test(button.text);
294+const isMultiToggleButton = (button: TelegramCallbackButton): boolean =>
295+typeof button.callback_data === "string" &&
296+button.callback_data.startsWith(`${MULTI_SELECT_PREFIX}toggle|`);
297+const resolveMultiSelectedValues = (buttons: TelegramCallbackButton[][]): string[] =>
298+buttons.flatMap((row) =>
299+row.flatMap((button) => {
300+if (!isMultiToggleButton(button) || !isSelectedMultiButton(button)) {
301+return [];
302+}
303+return [button.callback_data!.slice(`${MULTI_SELECT_PREFIX}toggle|`.length)];
304+}),
305+);
306+const updateMultiSelectKeyboard = (
307+message: Message,
308+action: "toggle" | "clear",
309+value = "",
310+): TelegramCallbackButton[][] =>
311+cloneInlineKeyboardButtons(message).map((row) =>
312+row.map((button) => {
313+if (!isMultiToggleButton(button)) {
314+return button;
315+}
316+const buttonValue = button.callback_data!.slice(`${MULTI_SELECT_PREFIX}toggle|`.length);
317+const baseText = stripMultiSelectPrefix(button.text);
318+const selected =
319+action === "clear"
320+ ? false
321+ : buttonValue === value
322+ ? !isSelectedMultiButton(button)
323+ : isSelectedMultiButton(button);
324+return {
325+ ...button,
326+text: selected ? `${SELECTED_PREFIX}${baseText}` : baseText,
327+};
328+}),
329+);
330+const buildCallbackSyntheticTextContext = (params: {
331+ctx: Pick<TelegramContext, "me"> & { getFile?: unknown };
332+callbackMessage: Message;
333+callback: { from?: Message["from"] };
334+text: string;
335+isForum: boolean;
336+}): { ctx: TelegramContext; message: Message } => {
337+const message = buildSyntheticTextMessage({
338+base: withResolvedTelegramForumFlag(params.callbackMessage, params.isForum),
339+from: params.callback.from,
340+text: params.text,
341+});
342+return { ctx: buildSyntheticContext(params.ctx, message), message };
343+};
344+241345const inboundDebouncer = createInboundDebouncer<TelegramDebounceEntry>({
242346 debounceMs,
243347resolveDebounceMs: (entry) =>
@@ -1590,6 +1694,65 @@ export const registerTelegramHandlers = ({
15901694return;
15911695}
159216961697+if (data.startsWith(MULTI_SELECT_PREFIX)) {
1698+const [, action, value = ""] = data.split("|");
1699+if (action === "toggle" || action === "clear") {
1700+const buttons = updateMultiSelectKeyboard(callbackMessage, action, value);
1701+if (buttons.length > 0) {
1702+try {
1703+await editCallbackButtons(buttons);
1704+} catch (editErr) {
1705+if (!String(editErr).includes("message is not modified")) {
1706+throw new TelegramRetryableCallbackError(editErr);
1707+}
1708+}
1709+}
1710+return;
1711+}
1712+if (action === "submit") {
1713+const selected = resolveMultiSelectedValues(cloneInlineKeyboardButtons(callbackMessage));
1714+const synthetic = buildCallbackSyntheticTextContext({
1715+ ctx,
1716+ callbackMessage,
1717+ callback,
1718+text: `Multi-select submitted: ${selected.length > 0 ? selected.join(", ") : "none"}`,
1719+ isForum,
1720+});
1721+await processMessageWithReplyChain(synthetic.ctx, synthetic.message, [], storeAllowFrom, {
1722+forceWasMentioned: true,
1723+messageIdOverride: callback.id,
1724+});
1725+return;
1726+}
1727+}
1728+1729+if (data.startsWith(SELECT_PREFIX)) {
1730+const value = data.slice(SELECT_PREFIX.length);
1731+try {
1732+await clearCallbackButtons();
1733+} catch (editErr) {
1734+const errStr = String(editErr);
1735+if (
1736+!errStr.includes("message is not modified") &&
1737+!errStr.includes("there is no text in the message to edit")
1738+) {
1739+throw new TelegramRetryableCallbackError(editErr);
1740+}
1741+}
1742+const synthetic = buildCallbackSyntheticTextContext({
1743+ ctx,
1744+ callbackMessage,
1745+ callback,
1746+text: `Single-select submitted: ${value}`,
1747+ isForum,
1748+});
1749+await processMessageWithReplyChain(synthetic.ctx, synthetic.message, [], storeAllowFrom, {
1750+forceWasMentioned: true,
1751+messageIdOverride: callback.id,
1752+});
1753+return;
1754+}
1755+15931756if (approvalCallback) {
15941757const isPluginApproval = approvalCallback.approvalId.startsWith("plugin:");
15951758const pluginApprovalAuthorizedSender = isTelegramExecApprovalApprover({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。