fix: preselect Telegram-supported status reaction variants

This commit is contained in:
Ayaan Zaidi 2026-02-21 08:51:40 +05:30
parent 6a27787209
commit d28b61dc2d
3 changed files with 309 additions and 2 deletions

View File

@ -62,6 +62,11 @@ import {
} from "./bot/helpers.js";
import type { StickerMetadata, TelegramContext } from "./bot/types.js";
import { evaluateTelegramGroupBaseAccess } from "./group-access.js";
import {
buildTelegramStatusReactionVariants,
resolveTelegramReactionVariant,
resolveTelegramStatusReactionEmojis,
} from "./status-reaction-variants.js";
export type TelegramMediaRef = {
path: string;
@ -530,6 +535,13 @@ export const buildTelegramMessageContext = async ({
const statusReactionsConfig = cfg.messages?.statusReactions;
const statusReactionsEnabled =
statusReactionsConfig?.enabled === true && Boolean(reactionApi) && shouldAckReaction();
const resolvedStatusReactionEmojis = resolveTelegramStatusReactionEmojis({
initialEmoji: ackReaction,
overrides: statusReactionsConfig?.emojis,
});
const statusReactionVariantsByEmoji = buildTelegramStatusReactionVariants(
resolvedStatusReactionEmojis,
);
const statusReactionController: StatusReactionController | null =
statusReactionsEnabled && msg.message_id
? createStatusReactionController({
@ -537,13 +549,22 @@ export const buildTelegramMessageContext = async ({
adapter: {
setReaction: async (emoji: string) => {
if (reactionApi) {
await reactionApi(chatId, msg.message_id, [{ type: "emoji", emoji }]);
const resolvedEmoji = resolveTelegramReactionVariant({
requestedEmoji: emoji,
variantsByRequestedEmoji: statusReactionVariantsByEmoji,
});
if (!resolvedEmoji) {
return;
}
await reactionApi(chatId, msg.message_id, [
{ type: "emoji", emoji: resolvedEmoji },
]);
}
},
// Telegram replaces atomically — no removeReaction needed
},
initialEmoji: ackReaction,
emojis: statusReactionsConfig?.emojis,
emojis: resolvedStatusReactionEmojis,
timing: statusReactionsConfig?.timing,
onError: (err) => {
logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`);

View File

@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_EMOJIS } from "../channels/status-reactions.js";
import {
buildTelegramStatusReactionVariants,
isTelegramSupportedReactionEmoji,
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("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("returns undefined for empty requested emoji", () => {
const result = resolveTelegramReactionVariant({
requestedEmoji: " ",
variantsByRequestedEmoji: new Map(),
});
expect(result).toBeUndefined();
});
});

View File

@ -0,0 +1,179 @@
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 resolveTelegramReactionVariant(params: {
requestedEmoji: string;
variantsByRequestedEmoji: Map<string, string[]>;
}): 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) {
if (isTelegramSupportedReactionEmoji(candidate)) {
return candidate;
}
}
return variants[0];
}