refactor: extract bot command catalog as single source of truth

src/bot/commands/index.js owns the canonical catalog (name, description,
adminOnly, build factory). bot.js builds dispatch from it; future menu
registration reads it. Drops the 14 explicit factory imports + the inline
/info handler from bot.js. Prevents the dispatch-vs-menu drift that bit
us with /setdayswarning (commit 013120649726f1 backfill).
This commit is contained in:
2026-05-10 02:45:50 +07:00
parent 6ed9c2dcb0
commit 34354a93a7
4 changed files with 73 additions and 42 deletions
+6 -5
View File
@@ -8,10 +8,7 @@ Runs on Vercel serverless functions with Upstash Redis as the data store.
- Upstash Redis schema mirrors the Java/Go Mongo layout: keys `admin`,
`group:{chatId}`, `apple:{appId}`, `google:{appId}` (last two TTL'd via Redis
`EX`). Multi-tenant isolation via `KEY_PREFIX` (default `store-scraper-bot:`).
- Telegram command identifiers match Java plus per-group settings:
`/info`, `/addgroup`, `/delgroup`, `/listgroup`, `/addapple`, `/delapple`,
`/addgoogle`, `/delgoogle`, `/listapp`, `/checkapp`, `/checkappscore`,
`/rawappleapp`, `/rawgoogleapp`, `/settings`, `/setdayswarning`.
- Command set defined in `src/bot/commands/index.js` (single source of truth — catalog drives both dispatch and the Telegram menu). Admin-only commands are hidden from the default menu and shown only in per-admin chat scope.
- HTML parse mode; weekend-silent daily report; configurable upstream cache (default 10 min).
- Per-group warning threshold override via `/setdayswarning` (falls back to `NUM_DAYS_WARNING_NOT_UPDATED` env default).
- Inlined `app-store-scraper` + `google-play-scraper` (no external scraper service).
@@ -58,7 +55,11 @@ Deploy:
npm run deploy # vercel deploy --prod && register webhook
```
`npm run register` re-points the Telegram webhook at the URL in `.env.deploy:WORKER_URL`.
`npm run register` re-points the Telegram webhook at the URL in `.env.deploy:WORKER_URL`,
and refreshes the menu: default scope = user commands only, plus a chat-scoped menu
(full set including admin commands) for every ID in `.env.deploy:ADMIN_IDS`. Re-run it
whenever `src/bot/commands/index.js` changes — Telegram caches the menu until
`setMyCommands` is called again. `npm run deploy` does this automatically.
`npm run describe` updates the bot's profile description / about-text (run once when copy changes).
## Operations
+5 -37
View File
@@ -1,18 +1,5 @@
import { createTelegramApi } from './telegram-api.js';
import { createAddGroupCommand } from './commands/add-group.js';
import { createDeleteGroupCommand } from './commands/delete-group.js';
import { createListGroupCommand } from './commands/list-group.js';
import { createAddAppleAppCommand } from './commands/add-apple-app.js';
import { createDeleteAppleAppCommand } from './commands/delete-apple-app.js';
import { createAddGoogleAppCommand } from './commands/add-google-app.js';
import { createDeleteGoogleAppCommand } from './commands/delete-google-app.js';
import { createListAppCommand } from './commands/list-app.js';
import { createCheckAppCommand } from './commands/check-app.js';
import { createCheckAppScoresCommand } from './commands/check-app-scores.js';
import { createRawAppleAppCommand } from './commands/raw-apple-app.js';
import { createRawGoogleAppCommand } from './commands/raw-google-app.js';
import { createGetSettingsCommand } from './commands/get-settings.js';
import { createSetDaysWarningCommand } from './commands/set-days-warning.js';
import { COMMAND_CATALOG } from './commands/index.js';
const PARSE_MODE = 'HTML';
@@ -51,29 +38,10 @@ export function createBot(config, store, appleScraper, googleScraper) {
},
};
const commands = {
info: async (msg, sender, args) => {
if (args.length !== 0) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
}
await sender.sendMessage(msg.chat.id, `Id của nhóm là <code>${msg.chat.id}</code>\n`);
},
addgroup: createAddGroupCommand(config, store),
delgroup: createDeleteGroupCommand(config, store),
listgroup: createListGroupCommand(config, store),
addapple: createAddAppleAppCommand(store, appleScraper),
delapple: createDeleteAppleAppCommand(store),
addgoogle: createAddGoogleAppCommand(store, googleScraper),
delgoogle: createDeleteGoogleAppCommand(store),
listapp: createListAppCommand(store),
checkapp: createCheckAppCommand(config, store, appleScraper, googleScraper),
checkappscore: createCheckAppScoresCommand(store, appleScraper, googleScraper),
rawappleapp: createRawAppleAppCommand(store, appleScraper),
rawgoogleapp: createRawGoogleAppCommand(store, googleScraper),
settings: createGetSettingsCommand(config, store),
setdayswarning: createSetDaysWarningCommand(config, store),
};
const ctx = { config, store, appleScraper, googleScraper };
const commands = Object.fromEntries(
COMMAND_CATALOG.map(({ name, build }) => [name, build(ctx)]),
);
return { sender, commands, api };
}
+52
View File
@@ -0,0 +1,52 @@
// Single source of truth for bot commands.
// Adding/removing/renaming a command here automatically updates dispatch (bot.js).
// Telegram menu only updates after `npm run register` (or `npm run deploy`).
//
// Catalog entry shape:
// { name, description, adminOnly, build(ctx) -> handler }
// `adminOnly: true` => hidden from the default Telegram menu, shown only in
// per-admin chat scope (see scripts/register-webhook.js).
import { createInfoCommand } from './info.js';
import { createAddGroupCommand } from './add-group.js';
import { createDeleteGroupCommand } from './delete-group.js';
import { createListGroupCommand } from './list-group.js';
import { createAddAppleAppCommand } from './add-apple-app.js';
import { createDeleteAppleAppCommand } from './delete-apple-app.js';
import { createAddGoogleAppCommand } from './add-google-app.js';
import { createDeleteGoogleAppCommand } from './delete-google-app.js';
import { createListAppCommand } from './list-app.js';
import { createCheckAppCommand } from './check-app.js';
import { createCheckAppScoresCommand } from './check-app-scores.js';
import { createRawAppleAppCommand } from './raw-apple-app.js';
import { createRawGoogleAppCommand } from './raw-google-app.js';
import { createGetSettingsCommand } from './get-settings.js';
import { createSetDaysWarningCommand } from './set-days-warning.js';
export const COMMAND_CATALOG = [
{ name: 'info', description: 'Show this group ID', adminOnly: false, build: () => createInfoCommand() },
{ name: 'addgroup', description: '[admin] Authorize a group', adminOnly: true, build: (c) => createAddGroupCommand(c.config, c.store) },
{ name: 'delgroup', description: '[admin] Deauthorize a group', adminOnly: true, build: (c) => createDeleteGroupCommand(c.config, c.store) },
{ name: 'listgroup', description: '[admin] List authorized groups', adminOnly: true, build: (c) => createListGroupCommand(c.config, c.store) },
{ name: 'addapple', description: 'Track an Apple App Store app', adminOnly: false, build: (c) => createAddAppleAppCommand(c.store, c.appleScraper) },
{ name: 'delapple', description: 'Stop tracking an Apple app', adminOnly: false, build: (c) => createDeleteAppleAppCommand(c.store) },
{ name: 'addgoogle', description: 'Track a Google Play app', adminOnly: false, build: (c) => createAddGoogleAppCommand(c.store, c.googleScraper) },
{ name: 'delgoogle', description: 'Stop tracking a Google app', adminOnly: false, build: (c) => createDeleteGoogleAppCommand(c.store) },
{ name: 'listapp', description: 'List tracked apps in this group', adminOnly: false, build: (c) => createListAppCommand(c.store) },
{ name: 'checkapp', description: 'Check update status of tracked apps', adminOnly: false, build: (c) => createCheckAppCommand(c.config, c.store, c.appleScraper, c.googleScraper) },
{ name: 'checkappscore', description: 'Check scores + ratings of tracked apps', adminOnly: false, build: (c) => createCheckAppScoresCommand(c.store, c.appleScraper, c.googleScraper) },
{ name: 'rawappleapp', description: 'Dump raw Apple API JSON for an app', adminOnly: false, build: (c) => createRawAppleAppCommand(c.store, c.appleScraper) },
{ name: 'rawgoogleapp', description: 'Dump raw Google API JSON for an app', adminOnly: false, build: (c) => createRawGoogleAppCommand(c.store, c.googleScraper) },
{ name: 'settings', description: "Show this group's settings", adminOnly: false, build: (c) => createGetSettingsCommand(c.config, c.store) },
{ name: 'setdayswarning', description: 'Set warning threshold (days, 0 = default)', adminOnly: false, build: (c) => createSetDaysWarningCommand(c.config, c.store) },
];
// Telegram menu projections.
// Default scope: user commands only — keeps admin entries out of every group's menu.
// Admin chat scope: full set including admin commands.
export const TELEGRAM_USER_COMMANDS = COMMAND_CATALOG
.filter((e) => !e.adminOnly)
.map(({ name, description }) => ({ command: name, description }));
export const TELEGRAM_ADMIN_COMMANDS = COMMAND_CATALOG
.map(({ name, description }) => ({ command: name, description }));
+10
View File
@@ -0,0 +1,10 @@
// /info — replies with the current chat ID (used to obtain the group ID for /addgroup).
export function createInfoCommand() {
return async (msg, sender, args) => {
if (args.length !== 0) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
}
await sender.sendMessage(msg.chat.id, `Id của nhóm là <code>${msg.chat.id}</code>\n`);
};
}