mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-09-02 16:20:39 +00:00
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 (commit0131206→49726f1backfill).
This commit is contained in:
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 }));
|
||||
@@ -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`);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user