mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 04:57:40 +00:00
fix: preselect Telegram-supported status reaction variants (#22380)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 018fcd6e2e
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -74,6 +74,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram/Streaming: always clean up draft previews even when dispatch throws before fallback handling, preventing orphaned preview messages during failed runs. (#19041) thanks @mudrii.
|
- Telegram/Streaming: always clean up draft previews even when dispatch throws before fallback handling, preventing orphaned preview messages during failed runs. (#19041) thanks @mudrii.
|
||||||
- Telegram/Streaming: split reasoning and answer draft preview lanes to prevent cross-lane overwrites, and ignore literal `<think>` tags inside inline/fenced code snippets so sample markup is not misrouted as reasoning. (#20774) Thanks @obviyus.
|
- Telegram/Streaming: split reasoning and answer draft preview lanes to prevent cross-lane overwrites, and ignore literal `<think>` tags inside inline/fenced code snippets so sample markup is not misrouted as reasoning. (#20774) Thanks @obviyus.
|
||||||
- Telegram/Status reactions: refresh stall timers on repeated phase updates and honor ack-reaction scope when lifecycle reactions are enabled, preventing false stall emojis and unwanted group reactions. Thanks @wolly-tundracube and @thewilloftheshadow.
|
- Telegram/Status reactions: refresh stall timers on repeated phase updates and honor ack-reaction scope when lifecycle reactions are enabled, preventing false stall emojis and unwanted group reactions. Thanks @wolly-tundracube and @thewilloftheshadow.
|
||||||
|
- Telegram/Status reactions: keep lifecycle reactions active when available-reactions lookup fails by falling back to unrestricted variant selection instead of suppressing reaction updates. (#22380) thanks @obviyus.
|
||||||
- Discord/Streaming: apply `replyToMode: first` only to the first Discord chunk so block-streamed replies do not spam mention pings. (#20726) Thanks @thewilloftheshadow for the report.
|
- Discord/Streaming: apply `replyToMode: first` only to the first Discord chunk so block-streamed replies do not spam mention pings. (#20726) Thanks @thewilloftheshadow for the report.
|
||||||
- Discord/Components: map DM channel targets back to user-scoped component sessions so button/select interactions stay in the main DM session. Thanks @thewilloftheshadow.
|
- Discord/Components: map DM channel targets back to user-scoped component sessions so button/select interactions stay in the main DM session. Thanks @thewilloftheshadow.
|
||||||
- Discord/Allowlist: lazy-load guild lists when resolving Discord user allowlists so ID-only entries resolve even if guild fetch fails. (#20208) Thanks @zhangjunmengyang.
|
- Discord/Allowlist: lazy-load guild lists when resolving Discord user allowlists so ID-only entries resolve even if guild fetch fails. (#20208) Thanks @zhangjunmengyang.
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ import {
|
|||||||
} from "./bot/helpers.js";
|
} from "./bot/helpers.js";
|
||||||
import type { StickerMetadata, TelegramContext } from "./bot/types.js";
|
import type { StickerMetadata, TelegramContext } from "./bot/types.js";
|
||||||
import { evaluateTelegramGroupBaseAccess } from "./group-access.js";
|
import { evaluateTelegramGroupBaseAccess } from "./group-access.js";
|
||||||
|
import {
|
||||||
|
buildTelegramStatusReactionVariants,
|
||||||
|
resolveTelegramAllowedEmojiReactions,
|
||||||
|
resolveTelegramReactionVariant,
|
||||||
|
resolveTelegramStatusReactionEmojis,
|
||||||
|
} from "./status-reaction-variants.js";
|
||||||
|
|
||||||
export type TelegramMediaRef = {
|
export type TelegramMediaRef = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -522,14 +528,24 @@ export const buildTelegramMessageContext = async ({
|
|||||||
messageId: number,
|
messageId: number,
|
||||||
reactions: Array<{ type: "emoji"; emoji: string }>,
|
reactions: Array<{ type: "emoji"; emoji: string }>,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
getChat?: (chatId: number | string) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
const reactionApi =
|
const reactionApi =
|
||||||
typeof api.setMessageReaction === "function" ? api.setMessageReaction.bind(api) : null;
|
typeof api.setMessageReaction === "function" ? api.setMessageReaction.bind(api) : null;
|
||||||
|
const getChatApi = typeof api.getChat === "function" ? api.getChat.bind(api) : null;
|
||||||
|
|
||||||
// Status Reactions controller (lifecycle reactions)
|
// Status Reactions controller (lifecycle reactions)
|
||||||
const statusReactionsConfig = cfg.messages?.statusReactions;
|
const statusReactionsConfig = cfg.messages?.statusReactions;
|
||||||
const statusReactionsEnabled =
|
const statusReactionsEnabled =
|
||||||
statusReactionsConfig?.enabled === true && Boolean(reactionApi) && shouldAckReaction();
|
statusReactionsConfig?.enabled === true && Boolean(reactionApi) && shouldAckReaction();
|
||||||
|
const resolvedStatusReactionEmojis = resolveTelegramStatusReactionEmojis({
|
||||||
|
initialEmoji: ackReaction,
|
||||||
|
overrides: statusReactionsConfig?.emojis,
|
||||||
|
});
|
||||||
|
const statusReactionVariantsByEmoji = buildTelegramStatusReactionVariants(
|
||||||
|
resolvedStatusReactionEmojis,
|
||||||
|
);
|
||||||
|
let allowedStatusReactionEmojisPromise: Promise<Set<string> | null> | null = null;
|
||||||
const statusReactionController: StatusReactionController | null =
|
const statusReactionController: StatusReactionController | null =
|
||||||
statusReactionsEnabled && msg.message_id
|
statusReactionsEnabled && msg.message_id
|
||||||
? createStatusReactionController({
|
? createStatusReactionController({
|
||||||
@@ -537,13 +553,36 @@ export const buildTelegramMessageContext = async ({
|
|||||||
adapter: {
|
adapter: {
|
||||||
setReaction: async (emoji: string) => {
|
setReaction: async (emoji: string) => {
|
||||||
if (reactionApi) {
|
if (reactionApi) {
|
||||||
await reactionApi(chatId, msg.message_id, [{ type: "emoji", emoji }]);
|
if (!allowedStatusReactionEmojisPromise) {
|
||||||
|
allowedStatusReactionEmojisPromise = resolveTelegramAllowedEmojiReactions({
|
||||||
|
chat: msg.chat,
|
||||||
|
chatId,
|
||||||
|
getChat: getChatApi ?? undefined,
|
||||||
|
}).catch((err) => {
|
||||||
|
logVerbose(
|
||||||
|
`telegram status-reaction available_reactions lookup failed for chat ${chatId}: ${String(err)}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const allowedStatusReactionEmojis = await allowedStatusReactionEmojisPromise;
|
||||||
|
const resolvedEmoji = resolveTelegramReactionVariant({
|
||||||
|
requestedEmoji: emoji,
|
||||||
|
variantsByRequestedEmoji: statusReactionVariantsByEmoji,
|
||||||
|
allowedEmojiReactions: allowedStatusReactionEmojis,
|
||||||
|
});
|
||||||
|
if (!resolvedEmoji) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await reactionApi(chatId, msg.message_id, [
|
||||||
|
{ type: "emoji", emoji: resolvedEmoji },
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Telegram replaces atomically — no removeReaction needed
|
// Telegram replaces atomically — no removeReaction needed
|
||||||
},
|
},
|
||||||
initialEmoji: ackReaction,
|
initialEmoji: ackReaction,
|
||||||
emojis: statusReactionsConfig?.emojis,
|
emojis: resolvedStatusReactionEmojis,
|
||||||
timing: statusReactionsConfig?.timing,
|
timing: statusReactionsConfig?.timing,
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`);
|
logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`);
|
||||||
|
|||||||
192
src/telegram/status-reaction-variants.test.ts
Normal file
192
src/telegram/status-reaction-variants.test.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { DEFAULT_EMOJIS } from "../channels/status-reactions.js";
|
||||||
|
import {
|
||||||
|
buildTelegramStatusReactionVariants,
|
||||||
|
extractTelegramAllowedEmojiReactions,
|
||||||
|
isTelegramSupportedReactionEmoji,
|
||||||
|
resolveTelegramAllowedEmojiReactions,
|
||||||
|
resolveTelegramReactionVariant,
|
||||||
|
resolveTelegramStatusReactionEmojis,
|
||||||
|
} from "./status-reaction-variants.js";
|
||||||
|
|
||||||
|
describe("resolveTelegramStatusReactionEmojis", () => {
|
||||||
|
it("falls back to Telegram-safe defaults for empty overrides", () => {
|
||||||
|
const result = resolveTelegramStatusReactionEmojis({
|
||||||
|
initialEmoji: "👀",
|
||||||
|
overrides: {
|
||||||
|
thinking: " ",
|
||||||
|
done: "\n",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.queued).toBe("👀");
|
||||||
|
expect(result.thinking).toBe(DEFAULT_EMOJIS.thinking);
|
||||||
|
expect(result.done).toBe(DEFAULT_EMOJIS.done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves explicit non-empty overrides", () => {
|
||||||
|
const result = resolveTelegramStatusReactionEmojis({
|
||||||
|
initialEmoji: "👀",
|
||||||
|
overrides: {
|
||||||
|
thinking: "🫡",
|
||||||
|
done: "🎉",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.thinking).toBe("🫡");
|
||||||
|
expect(result.done).toBe("🎉");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildTelegramStatusReactionVariants", () => {
|
||||||
|
it("puts requested emoji first and appends Telegram fallbacks", () => {
|
||||||
|
const variants = buildTelegramStatusReactionVariants({
|
||||||
|
...DEFAULT_EMOJIS,
|
||||||
|
coding: "🛠️",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(variants.get("🛠️")).toEqual(["🛠️", "👨💻", "🔥", "⚡"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isTelegramSupportedReactionEmoji", () => {
|
||||||
|
it("accepts Telegram-supported reaction emojis", () => {
|
||||||
|
expect(isTelegramSupportedReactionEmoji("👀")).toBe(true);
|
||||||
|
expect(isTelegramSupportedReactionEmoji("👨💻")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unsupported emojis", () => {
|
||||||
|
expect(isTelegramSupportedReactionEmoji("🫠")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractTelegramAllowedEmojiReactions", () => {
|
||||||
|
it("returns undefined when chat does not include available_reactions", () => {
|
||||||
|
const result = extractTelegramAllowedEmojiReactions({ id: 1 });
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when available_reactions is omitted/null", () => {
|
||||||
|
const result = extractTelegramAllowedEmojiReactions({ available_reactions: null });
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts emoji reactions only", () => {
|
||||||
|
const result = extractTelegramAllowedEmojiReactions({
|
||||||
|
available_reactions: [
|
||||||
|
{ type: "emoji", emoji: "👍" },
|
||||||
|
{ type: "custom_emoji", custom_emoji_id: "abc" },
|
||||||
|
{ type: "emoji", emoji: "🔥" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(result ? Array.from(result).toSorted() : null).toEqual(["👍", "🔥"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveTelegramAllowedEmojiReactions", () => {
|
||||||
|
it("uses getChat lookup when message chat does not include available_reactions", async () => {
|
||||||
|
const getChat = async () => ({
|
||||||
|
available_reactions: [{ type: "emoji", emoji: "👍" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await resolveTelegramAllowedEmojiReactions({
|
||||||
|
chat: { id: 1 },
|
||||||
|
chatId: 1,
|
||||||
|
getChat,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result ? Array.from(result) : null).toEqual(["👍"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to unrestricted reactions when getChat lookup fails", async () => {
|
||||||
|
const getChat = async () => {
|
||||||
|
throw new Error("lookup failed");
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolveTelegramAllowedEmojiReactions({
|
||||||
|
chat: { id: 1 },
|
||||||
|
chatId: 1,
|
||||||
|
getChat,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveTelegramReactionVariant", () => {
|
||||||
|
it("returns requested emoji when already Telegram-supported", () => {
|
||||||
|
const variantsByEmoji = buildTelegramStatusReactionVariants({
|
||||||
|
...DEFAULT_EMOJIS,
|
||||||
|
coding: "👨💻",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = resolveTelegramReactionVariant({
|
||||||
|
requestedEmoji: "👨💻",
|
||||||
|
variantsByRequestedEmoji: variantsByEmoji,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe("👨💻");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns first Telegram-supported fallback for unsupported requested emoji", () => {
|
||||||
|
const variantsByEmoji = buildTelegramStatusReactionVariants({
|
||||||
|
...DEFAULT_EMOJIS,
|
||||||
|
coding: "🛠️",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = resolveTelegramReactionVariant({
|
||||||
|
requestedEmoji: "🛠️",
|
||||||
|
variantsByRequestedEmoji: variantsByEmoji,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe("👨💻");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses generic Telegram fallbacks for unknown emojis", () => {
|
||||||
|
const result = resolveTelegramReactionVariant({
|
||||||
|
requestedEmoji: "🫠",
|
||||||
|
variantsByRequestedEmoji: new Map(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe("👍");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects chat allowed reactions", () => {
|
||||||
|
const variantsByEmoji = buildTelegramStatusReactionVariants({
|
||||||
|
...DEFAULT_EMOJIS,
|
||||||
|
coding: "👨💻",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = resolveTelegramReactionVariant({
|
||||||
|
requestedEmoji: "👨💻",
|
||||||
|
variantsByRequestedEmoji: variantsByEmoji,
|
||||||
|
allowedEmojiReactions: new Set(["👍"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe("👍");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when no candidate is chat-allowed", () => {
|
||||||
|
const variantsByEmoji = buildTelegramStatusReactionVariants({
|
||||||
|
...DEFAULT_EMOJIS,
|
||||||
|
coding: "👨💻",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = resolveTelegramReactionVariant({
|
||||||
|
requestedEmoji: "👨💻",
|
||||||
|
variantsByRequestedEmoji: variantsByEmoji,
|
||||||
|
allowedEmojiReactions: new Set(["🎉"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for empty requested emoji", () => {
|
||||||
|
const result = resolveTelegramReactionVariant({
|
||||||
|
requestedEmoji: " ",
|
||||||
|
variantsByRequestedEmoji: new Map(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
245
src/telegram/status-reaction-variants.ts
Normal file
245
src/telegram/status-reaction-variants.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { DEFAULT_EMOJIS, type StatusReactionEmojis } from "../channels/status-reactions.js";
|
||||||
|
|
||||||
|
type StatusReactionEmojiKey = keyof Required<StatusReactionEmojis>;
|
||||||
|
|
||||||
|
const TELEGRAM_GENERIC_REACTION_FALLBACKS = ["👍", "👀", "🔥"] as const;
|
||||||
|
|
||||||
|
const TELEGRAM_SUPPORTED_REACTION_EMOJIS = new Set<string>([
|
||||||
|
"❤",
|
||||||
|
"👍",
|
||||||
|
"👎",
|
||||||
|
"🔥",
|
||||||
|
"🥰",
|
||||||
|
"👏",
|
||||||
|
"😁",
|
||||||
|
"🤔",
|
||||||
|
"🤯",
|
||||||
|
"😱",
|
||||||
|
"🤬",
|
||||||
|
"😢",
|
||||||
|
"🎉",
|
||||||
|
"🤩",
|
||||||
|
"🤮",
|
||||||
|
"💩",
|
||||||
|
"🙏",
|
||||||
|
"👌",
|
||||||
|
"🕊",
|
||||||
|
"🤡",
|
||||||
|
"🥱",
|
||||||
|
"🥴",
|
||||||
|
"😍",
|
||||||
|
"🐳",
|
||||||
|
"❤🔥",
|
||||||
|
"🌚",
|
||||||
|
"🌭",
|
||||||
|
"💯",
|
||||||
|
"🤣",
|
||||||
|
"⚡",
|
||||||
|
"🍌",
|
||||||
|
"🏆",
|
||||||
|
"💔",
|
||||||
|
"🤨",
|
||||||
|
"😐",
|
||||||
|
"🍓",
|
||||||
|
"🍾",
|
||||||
|
"💋",
|
||||||
|
"🖕",
|
||||||
|
"😈",
|
||||||
|
"😴",
|
||||||
|
"😭",
|
||||||
|
"🤓",
|
||||||
|
"👻",
|
||||||
|
"👨💻",
|
||||||
|
"👀",
|
||||||
|
"🎃",
|
||||||
|
"🙈",
|
||||||
|
"😇",
|
||||||
|
"😨",
|
||||||
|
"🤝",
|
||||||
|
"✍",
|
||||||
|
"🤗",
|
||||||
|
"🫡",
|
||||||
|
"🎅",
|
||||||
|
"🎄",
|
||||||
|
"☃",
|
||||||
|
"💅",
|
||||||
|
"🤪",
|
||||||
|
"🗿",
|
||||||
|
"🆒",
|
||||||
|
"💘",
|
||||||
|
"🙉",
|
||||||
|
"🦄",
|
||||||
|
"😘",
|
||||||
|
"💊",
|
||||||
|
"🙊",
|
||||||
|
"😎",
|
||||||
|
"👾",
|
||||||
|
"🤷♂",
|
||||||
|
"🤷",
|
||||||
|
"🤷♀",
|
||||||
|
"😡",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const TELEGRAM_STATUS_REACTION_VARIANTS: Record<StatusReactionEmojiKey, string[]> = {
|
||||||
|
queued: ["👀", "👍", "🔥"],
|
||||||
|
thinking: ["🤔", "🤓", "👀"],
|
||||||
|
tool: ["🔥", "⚡", "👍"],
|
||||||
|
coding: ["👨💻", "🔥", "⚡"],
|
||||||
|
web: ["⚡", "🔥", "👍"],
|
||||||
|
done: ["👍", "🎉", "💯"],
|
||||||
|
error: ["😱", "😨", "🤯"],
|
||||||
|
stallSoft: ["🥱", "😴", "🤔"],
|
||||||
|
stallHard: ["😨", "😱", "⚡"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_REACTION_EMOJI_KEYS: StatusReactionEmojiKey[] = [
|
||||||
|
"queued",
|
||||||
|
"thinking",
|
||||||
|
"tool",
|
||||||
|
"coding",
|
||||||
|
"web",
|
||||||
|
"done",
|
||||||
|
"error",
|
||||||
|
"stallSoft",
|
||||||
|
"stallHard",
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeEmoji(value: string | undefined): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
return trimmed ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUniqueNonEmpty(values: string[]): string[] {
|
||||||
|
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTelegramStatusReactionEmojis(params: {
|
||||||
|
initialEmoji: string;
|
||||||
|
overrides?: StatusReactionEmojis;
|
||||||
|
}): Required<StatusReactionEmojis> {
|
||||||
|
const { overrides } = params;
|
||||||
|
const queuedFallback = normalizeEmoji(params.initialEmoji) ?? DEFAULT_EMOJIS.queued;
|
||||||
|
return {
|
||||||
|
queued: normalizeEmoji(overrides?.queued) ?? queuedFallback,
|
||||||
|
thinking: normalizeEmoji(overrides?.thinking) ?? DEFAULT_EMOJIS.thinking,
|
||||||
|
tool: normalizeEmoji(overrides?.tool) ?? DEFAULT_EMOJIS.tool,
|
||||||
|
coding: normalizeEmoji(overrides?.coding) ?? DEFAULT_EMOJIS.coding,
|
||||||
|
web: normalizeEmoji(overrides?.web) ?? DEFAULT_EMOJIS.web,
|
||||||
|
done: normalizeEmoji(overrides?.done) ?? DEFAULT_EMOJIS.done,
|
||||||
|
error: normalizeEmoji(overrides?.error) ?? DEFAULT_EMOJIS.error,
|
||||||
|
stallSoft: normalizeEmoji(overrides?.stallSoft) ?? DEFAULT_EMOJIS.stallSoft,
|
||||||
|
stallHard: normalizeEmoji(overrides?.stallHard) ?? DEFAULT_EMOJIS.stallHard,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTelegramStatusReactionVariants(
|
||||||
|
emojis: Required<StatusReactionEmojis>,
|
||||||
|
): Map<string, string[]> {
|
||||||
|
const variantsByRequested = new Map<string, string[]>();
|
||||||
|
for (const key of STATUS_REACTION_EMOJI_KEYS) {
|
||||||
|
const requested = normalizeEmoji(emojis[key]);
|
||||||
|
if (!requested) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fallbackVariants = TELEGRAM_STATUS_REACTION_VARIANTS[key] ?? [];
|
||||||
|
const candidates = toUniqueNonEmpty([requested, ...fallbackVariants]);
|
||||||
|
variantsByRequested.set(requested, candidates);
|
||||||
|
}
|
||||||
|
return variantsByRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTelegramSupportedReactionEmoji(emoji: string): boolean {
|
||||||
|
return TELEGRAM_SUPPORTED_REACTION_EMOJIS.has(emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTelegramAllowedEmojiReactions(
|
||||||
|
chat: unknown,
|
||||||
|
): Set<string> | null | undefined {
|
||||||
|
if (!chat || typeof chat !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(chat, "available_reactions")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableReactions = (chat as { available_reactions?: unknown }).available_reactions;
|
||||||
|
if (availableReactions == null) {
|
||||||
|
// Explicitly omitted/null => all emoji reactions are allowed in this chat.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(availableReactions)) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = new Set<string>();
|
||||||
|
for (const reaction of availableReactions) {
|
||||||
|
if (!reaction || typeof reaction !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const typedReaction = reaction as { type?: unknown; emoji?: unknown };
|
||||||
|
if (typedReaction.type !== "emoji" || typeof typedReaction.emoji !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const emoji = typedReaction.emoji.trim();
|
||||||
|
if (emoji) {
|
||||||
|
allowed.add(emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveTelegramAllowedEmojiReactions(params: {
|
||||||
|
chat: unknown;
|
||||||
|
chatId: string | number;
|
||||||
|
getChat?: (chatId: string | number) => Promise<unknown>;
|
||||||
|
}): Promise<Set<string> | null> {
|
||||||
|
const fromMessage = extractTelegramAllowedEmojiReactions(params.chat);
|
||||||
|
if (fromMessage !== undefined) {
|
||||||
|
return fromMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.getChat) {
|
||||||
|
try {
|
||||||
|
const chatInfo = await params.getChat(params.chatId);
|
||||||
|
const fromLookup = extractTelegramAllowedEmojiReactions(chatInfo);
|
||||||
|
if (fromLookup !== undefined) {
|
||||||
|
return fromLookup;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If unavailable, assume no explicit restriction.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTelegramReactionVariant(params: {
|
||||||
|
requestedEmoji: string;
|
||||||
|
variantsByRequestedEmoji: Map<string, string[]>;
|
||||||
|
allowedEmojiReactions?: Set<string> | null;
|
||||||
|
}): string | undefined {
|
||||||
|
const requestedEmoji = normalizeEmoji(params.requestedEmoji);
|
||||||
|
if (!requestedEmoji) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredVariants = params.variantsByRequestedEmoji.get(requestedEmoji) ?? [
|
||||||
|
requestedEmoji,
|
||||||
|
];
|
||||||
|
const variants = toUniqueNonEmpty([
|
||||||
|
...configuredVariants,
|
||||||
|
...TELEGRAM_GENERIC_REACTION_FALLBACKS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const candidate of variants) {
|
||||||
|
const isAllowedByChat =
|
||||||
|
params.allowedEmojiReactions == null || params.allowedEmojiReactions.has(candidate);
|
||||||
|
if (isAllowedByChat && isTelegramSupportedReactionEmoji(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user