mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
feat(util): add private /stickerid to capture bot-scoped sticker file_ids
Reply to any sticker with /stickerid to get its file_id and file_unique_id. Collects IDs for hard-coded sticker pools in other modules (upcoming loldle win/lose/giveup reactions). Private visibility keeps the command out of /help and the Telegram slash menu.
This commit is contained in:
1 parent
679bf8ec6f
commit
06a93c6c60
6 files changed
+106
-5
No files matched your search
@@ -19,7 +19,7 @@ Telegram bot on Cloudflare Workers with a plug-n-play module system. grammY hand
|
||||
|
||||
| Module | Status | Commands | Storage | Crons | Description |
|
||||
|--------|--------|----------|---------|-------|-------------|
|
||||
| `util` | Complete | `/info`, `/help` | — | — | Bot info and command help renderer |
|
||||
| `util` | Complete | `/info`, `/help`, `/stickerid` (private) | — | — | Bot info, command help renderer, and sticker file_id echo helper |
|
||||
| `trading` | Complete | `/trade_topup`, `/trade_buy`, `/trade_sell`, `/trade_convert`, `/trade_stats`, `/history` | D1 (trades) + KV (portfolio, symbol cache) | Daily 5PM trim | Paper trading — VN stocks with dynamic symbol resolution. Crypto/gold/forex coming soon. |
|
||||
| `wordle` | Complete | `/wordle`, `/wordle_new`, `/wordle_giveup`, `/wordle_stats` | KV (game, stats) | — | Classic 5-letter word game. 14,855-word dict sourced from [dracos's gist](https://gist.github.com/dracos/dd0668f281e685bad51479e5acaadb93). |
|
||||
| `loldle` | Complete | `/loldle`, `/loldle_giveup`, `/loldle_stats` | KV (game, stats) | — | Classic-mode LoL champion guesser (auto-starts a new round after solve/giveup). Champion data synced from `tiennm99/loldle-data`. |
|
||||
|
||||
@@ -8,11 +8,13 @@ Core bot utilities — informational commands that read framework metadata.
|
||||
|---------|-----------|-------------|
|
||||
| `/info` | public | Echoes chat id, thread id, sender id (debug helper) |
|
||||
| `/help` | public | Renders all public + protected commands grouped by module |
|
||||
| `/stickerid` | private | Reply to a sticker to get its bot-scoped `file_id` (used to collect sticker pools for other modules) |
|
||||
|
||||
## Architecture
|
||||
|
||||
- `/help` is a pure renderer over `getCurrentRegistry()` — it reads the registry's command maps and formats them as Telegram HTML. Modules with zero visible commands are omitted. Private commands are always excluded.
|
||||
- `/info` reads grammY context fields (`ctx.chat.id`, `ctx.message.message_thread_id`, `ctx.from.id`). No external state.
|
||||
- `/stickerid` reads `ctx.message.reply_to_message.sticker` and echoes `file_id` + `file_unique_id`. Private visibility keeps it out of `/help` and the Telegram menu. Used to collect sticker IDs for hard-coded pools in other modules (e.g., loldle win/lose/giveup stickers).
|
||||
- HTML injection is prevented via `escapeHtml()` on all user-influenced strings.
|
||||
|
||||
## Database
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
|
||||
import { helpCommand } from "./help-command.js";
|
||||
import { infoCommand } from "./info-command.js";
|
||||
import { stickerIdCommand } from "./stickerid-command.js";
|
||||
|
||||
/** @type {import("../registry.js").BotModule} */
|
||||
const utilModule = {
|
||||
name: "util",
|
||||
commands: [infoCommand, helpCommand],
|
||||
commands: [infoCommand, helpCommand, stickerIdCommand],
|
||||
};
|
||||
|
||||
export default utilModule;
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file /stickerid — dev helper that returns the file_id of a replied sticker.
|
||||
*
|
||||
* Telegram sticker file_ids are bot-scoped: a file_id obtained from any other
|
||||
* bot will not work with sendSticker for this bot. To collect IDs for use in
|
||||
* `/loldle` congrats/lose/giveup pools, send a sticker to the bot, reply to
|
||||
* it with `/stickerid`, then copy the returned file_id into code.
|
||||
*
|
||||
* Private visibility — hidden from the Telegram `/` menu and from `/help`.
|
||||
*/
|
||||
|
||||
import { escapeHtml } from "../../util/escape-html.js";
|
||||
|
||||
/** @type {import("../validate-command.js").ModuleCommand} */
|
||||
export const stickerIdCommand = {
|
||||
name: "stickerid",
|
||||
visibility: "private",
|
||||
description: "Reply to a sticker with this command to get its bot-scoped file_id",
|
||||
handler: async (ctx) => {
|
||||
const sticker = ctx.message?.reply_to_message?.sticker;
|
||||
if (!sticker) {
|
||||
return ctx.reply(
|
||||
"Reply to a sticker message with /stickerid to get its file_id.\n" +
|
||||
"Usage: send a sticker to me, then tap Reply on it and type /stickerid.",
|
||||
);
|
||||
}
|
||||
|
||||
const setName = sticker.set_name ?? "(no set)";
|
||||
const lines = [
|
||||
"<b>file_id</b>",
|
||||
`<code>${escapeHtml(sticker.file_id)}</code>`,
|
||||
"",
|
||||
"<b>file_unique_id</b>",
|
||||
`<code>${escapeHtml(sticker.file_unique_id)}</code>`,
|
||||
"",
|
||||
`set: ${escapeHtml(setName)} · emoji: ${escapeHtml(sticker.emoji ?? "—")}`,
|
||||
];
|
||||
await ctx.reply(lines.join("\n"), { parse_mode: "HTML" });
|
||||
},
|
||||
};
|
||||
|
||||
export default stickerIdCommand;
|
||||
@@ -24,9 +24,9 @@ describe("installDispatcher", () => {
|
||||
const env = { MODULES: "util,wordle,loldle,misc", KV: makeFakeKv() };
|
||||
const reg = await installDispatcher(bot, env);
|
||||
|
||||
// Expect 12 total commands (10 public + 1 protected + 1 private).
|
||||
expect(bot.commandCalls).toHaveLength(12);
|
||||
expect(reg.allCommands.size).toBe(12);
|
||||
// Expect 13 total commands (10 public + 1 protected + 2 private).
|
||||
expect(bot.commandCalls).toHaveLength(13);
|
||||
expect(reg.allCommands.size).toBe(13);
|
||||
|
||||
const registeredNames = bot.commandCalls.map((c) => c.name).sort();
|
||||
const expected = [...reg.allCommands.keys()].sort();
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { stickerIdCommand } from "../../../src/modules/util/stickerid-command.js";
|
||||
|
||||
function makeCtx({ sticker }) {
|
||||
const reply = vi.fn(async () => {});
|
||||
return {
|
||||
reply,
|
||||
message: sticker ? { reply_to_message: { sticker } } : {},
|
||||
};
|
||||
}
|
||||
|
||||
describe("/stickerid", () => {
|
||||
it("is a private command so it stays out of /help and the Telegram menu", () => {
|
||||
expect(stickerIdCommand.visibility).toBe("private");
|
||||
});
|
||||
|
||||
it("tells the user how to use it when no sticker is replied to", async () => {
|
||||
const ctx = makeCtx({ sticker: null });
|
||||
await stickerIdCommand.handler(ctx);
|
||||
const [text] = ctx.reply.mock.calls[0];
|
||||
expect(text).toMatch(/reply to a sticker/i);
|
||||
});
|
||||
|
||||
it("echoes file_id + file_unique_id when replying to a sticker", async () => {
|
||||
const ctx = makeCtx({
|
||||
sticker: {
|
||||
file_id: "CAACAgIAAxkBAAEFAKE_ID",
|
||||
file_unique_id: "AgADFAKEUNIQ",
|
||||
set_name: "HotCherry",
|
||||
emoji: "🔥",
|
||||
},
|
||||
});
|
||||
await stickerIdCommand.handler(ctx);
|
||||
const [text, opts] = ctx.reply.mock.calls[0];
|
||||
expect(opts).toEqual({ parse_mode: "HTML" });
|
||||
expect(text).toContain("CAACAgIAAxkBAAEFAKE_ID");
|
||||
expect(text).toContain("AgADFAKEUNIQ");
|
||||
expect(text).toContain("HotCherry");
|
||||
expect(text).toContain("🔥");
|
||||
});
|
||||
|
||||
it("HTML-escapes sticker set name so a malicious set label can't inject tags", async () => {
|
||||
const ctx = makeCtx({
|
||||
sticker: {
|
||||
file_id: "id1",
|
||||
file_unique_id: "uid1",
|
||||
set_name: "<script>",
|
||||
emoji: null,
|
||||
},
|
||||
});
|
||||
await stickerIdCommand.handler(ctx);
|
||||
const [text] = ctx.reply.mock.calls[0];
|
||||
expect(text).toContain("<script>");
|
||||
expect(text).not.toContain("<script>");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user