/** * Telegram message reactions: WHICH emoji are allowed, and WHO owns the bot's * reaction on a given message. * * Two halves, one domain. `TELEGRAM_REACTIONS` / `isReaction` / `reactionsFor` * answer what Telegram will accept; `reactionPolicy` further down arbitrates * between independent features that want to react to the same message. * * # The accepted set * * ## Why this is a list and not an API call * * The Bot API does not enumerate them. `getChat().available_reactions` is * populated ONLY when a chat restricts its own reactions; a chat that allows * all of them returns `null`, which is the common case. So a caller that wants * to know the set โ€” to offer it to a language model, to validate input, to * build a picker โ€” has no method to ask. * * ## Where these came from * * Measured, not copied from documentation: `scripts/probe-reactions.ts` sets * each candidate on a real message and records Telegram's verdict. Re-run it to * refresh this list. * * BOT_TOKEN=โ€ฆ bun scripts/probe-reactions.ts * * Gathered **2026-08-07**: 86 candidates probed, 73 accepted, 13 refused with * `Bad Request: REACTION_INVALID`. * * The refusals are worth knowing, because they are the ones people (and models) * reach for by instinct: * * ๐Ÿ‘‹ ๐Ÿ˜Š ๐Ÿ™‚ ๐Ÿ˜„ โœ… ๐Ÿค– ๐Ÿ’ช ๐Ÿ™Œ ๐Ÿ˜… ๐Ÿฅณ ๐Ÿ˜‚ โ“ โ€ผ * * **There is no wave.** A greeting has to be answered with something from the * accepted set โ€” ๐Ÿค ๐Ÿซก ๐Ÿค— ๐Ÿ˜ โ€” or with words. */ export declare const TELEGRAM_REACTIONS: readonly ["๐Ÿ‘", "๐Ÿ‘Ž", "โค", "๐Ÿ”ฅ", "๐Ÿฅฐ", "๐Ÿ‘", "๐Ÿ˜", "๐Ÿค”", "๐Ÿคฏ", "๐Ÿ˜ฑ", "๐Ÿคฌ", "๐Ÿ˜ข", "๐ŸŽ‰", "๐Ÿคฉ", "๐Ÿคฎ", "๐Ÿ’ฉ", "๐Ÿ™", "๐Ÿ‘Œ", "๐Ÿ•Š", "๐Ÿคก", "๐Ÿฅฑ", "๐Ÿฅด", "๐Ÿ˜", "๐Ÿณ", "โคโ€๐Ÿ”ฅ", "๐ŸŒš", "๐ŸŒญ", "๐Ÿ’ฏ", "๐Ÿคฃ", "โšก", "๐ŸŒ", "๐Ÿ†", "๐Ÿ’”", "๐Ÿคจ", "๐Ÿ˜", "๐Ÿ“", "๐Ÿพ", "๐Ÿ’‹", "๐Ÿ–•", "๐Ÿ˜ˆ", "๐Ÿ˜ด", "๐Ÿ˜ญ", "๐Ÿค“", "๐Ÿ‘ป", "๐Ÿ‘จโ€๐Ÿ’ป", "๐Ÿ‘€", "๐ŸŽƒ", "๐Ÿ™ˆ", "๐Ÿ˜‡", "๐Ÿ˜จ", "๐Ÿค", "โœ", "๐Ÿค—", "๐Ÿซก", "๐ŸŽ…", "๐ŸŽ„", "โ˜ƒ", "๐Ÿ’…", "๐Ÿคช", "๐Ÿ—ฟ", "๐Ÿ†’", "๐Ÿ’˜", "๐Ÿ™‰", "๐Ÿฆ„", "๐Ÿ˜˜", "๐Ÿ’Š", "๐Ÿ™Š", "๐Ÿ˜Ž", "๐Ÿ‘พ", "๐Ÿคทโ€โ™‚", "๐Ÿคท", "๐Ÿคทโ€โ™€", "๐Ÿ˜ก"]; export type TelegramReaction = (typeof TELEGRAM_REACTIONS)[number]; /** Whether Telegram will accept this emoji as a reaction. */ export declare const isReaction: (emoji: string) => emoji is TelegramReaction; /** * The set a chat will actually accept: what `getChat` reports when the room * restricts its reactions, otherwise the full measured set. Pass * `chat.available_reactions` straight through. */ export declare const reactionsFor: (available?: { type: string; emoji?: string; }[] | null) => readonly string[]; /** * Message-reaction arbitration: ONE owner for the bot's reaction on each * message. Telegram's setMessageReaction REPLACES the bot's whole reaction * set, so independent features reacting to the same message (an "on it" ack, * an outcome, a side-feature's shrug) silently clobber each other โ€” last * writer wins, and the client shows flickering swaps. * * The fix is semantic states with ranks, arbitrated per message: * * const status = reactionPolicy({ * working: { emoji: "๐Ÿซก", rank: 1 }, * offTopic: { emoji: "๐Ÿคท", rank: 2 }, * failed: { emoji: "๐Ÿ‘Ž", rank: 3 }, * done: { emoji: "๐Ÿซก", rank: 4 }, // same emoji as working: no API call, but locks out offTopic * }); * const r = status.for(ctx); * await r.set("working"); // applied * await r.set("done"); // rank bookkeeping only (same emoji) โ€” zero API calls, zero flicker * await r.set("offTopic"); // REJECTED: outranked by done โ€” the shrug never lands * * Guarantees: * - a state only applies when its rank is >= the current state's rank * (equal rank may refine; strictly lower is rejected โ€” returns false); * - setting the current state again is a no-op (idempotent, no API call); * - a transition between states sharing an emoji makes NO API call * (rank bookkeeping without flicker); * - react() failures are swallowed โ€” reactions are decoration, they must * never break the flow that set them; * - state is keyed by chat:message, so one policy instance serves every * message concurrently, and is bounded (reactions are transient * request-lifetime coordination, not durable state). */ export interface ReactionStateSpec { /** The Telegram reaction emoji this state renders as. */ emoji: string; /** Arbitration rank: a state only applies over an equal-or-lower rank. */ rank: number; } /** The context surface read off the ctx: the message identity plus gramio's react(). * `for()` takes `unknown` and narrows structurally (gramio's react() param is an * emoji-literal union, which no portable signature satisfies in strict variance) โ€” * a ctx without a callable react() screams instead of silently not reacting. */ export interface ReactionCtx { id?: number; chatId?: number; chat?: { id?: number; }; react(emoji: string): Promise; } export interface ReactionHandle { /** The current state on this message, or null if none was set. */ state(): S | null; /** * Request a state. Applies (and reacts, if the emoji changes) only when * not outranked by the current state. Returns whether the state holds * after the call (true also for an idempotent re-set). */ set(next: S): Promise; } /** Declare the reaction vocabulary once; get per-message arbitrated handles. */ export declare function reactionPolicy>(states: S): { /** The arbitrated handle for THIS ctx's message (keyed chat:message). */ for(context: unknown): ReactionHandle; }; //# sourceMappingURL=reactions.d.ts.map