mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-10-11 03:13:50 +00:00
feat: admin-scoped Telegram menu
setMyCommands now runs at default scope (user commands only) plus once
per ADMIN_ID with scope:{type:'chat'} carrying the full set, so admin
commands (/addgroup, /delgroup, /listgroup) are hidden from non-admins'
menu. requireAdminUser remains the actual auth gate; this is UI cleanup.
parseAdminIds extracted to src/util/parse-admin-ids.js — shared between
config loading and the register script. ADMIN_IDS now required by
register-webhook.js (.env.deploy.example updated). Per-admin call uses
allowFail to gracefully skip admins who haven't DMed the bot yet.
This commit is contained in:
1 parent
34354a93a7
commit
1ed82578c0
4 files changed
+60
-30
No files matched your search
@@ -6,6 +6,10 @@
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_WEBHOOK_SECRET=
|
||||
|
||||
# Comma-separated Telegram user IDs (must match Vercel ADMIN_IDS).
|
||||
# register-webhook.js sets a chat-scoped menu (with admin commands) per ID.
|
||||
ADMIN_IDS=
|
||||
|
||||
# Where Telegram should send webhook updates.
|
||||
# Format: https://<your-vercel-url>/api/webhook
|
||||
WORKER_URL=https://store-scraper-bot.vercel.app/api/webhook
|
||||
+45
-21
@@ -1,17 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
// Post-deploy registration: setWebhook (with secret_token) + setMyCommands.
|
||||
// Run via: npm run register (reads .env.deploy)
|
||||
// Post-deploy registration:
|
||||
// - setWebhook (with secret_token)
|
||||
// - setMyCommands at default scope (user-only commands)
|
||||
// - setMyCommands at chat scope per ADMIN_ID (full set, including admin commands)
|
||||
// Run via: npm run register (reads .env.deploy)
|
||||
// Dry run via: npm run register:dry
|
||||
|
||||
import {
|
||||
TELEGRAM_USER_COMMANDS,
|
||||
TELEGRAM_ADMIN_COMMANDS,
|
||||
} from '../src/bot/commands/index.js';
|
||||
import { parseAdminIds } from '../src/util/parse-admin-ids.js';
|
||||
|
||||
const TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const SECRET = process.env.TELEGRAM_WEBHOOK_SECRET;
|
||||
const URL_ = process.env.WORKER_URL;
|
||||
const ADMIN_IDS_RAW = process.env.ADMIN_IDS;
|
||||
const DRY = process.argv.includes('--dry-run');
|
||||
|
||||
for (const [k, v] of Object.entries({
|
||||
TELEGRAM_BOT_TOKEN: TOKEN,
|
||||
TELEGRAM_WEBHOOK_SECRET: SECRET,
|
||||
WORKER_URL: URL_,
|
||||
ADMIN_IDS: ADMIN_IDS_RAW,
|
||||
})) {
|
||||
if (!v) {
|
||||
console.error(`${k} is required`);
|
||||
@@ -19,25 +30,13 @@ for (const [k, v] of Object.entries({
|
||||
}
|
||||
}
|
||||
|
||||
const COMMANDS = [
|
||||
{ command: 'info', description: 'Show this group ID' },
|
||||
{ command: 'addgroup', description: '[admin] Authorize a group' },
|
||||
{ command: 'delgroup', description: '[admin] Deauthorize a group' },
|
||||
{ command: 'listgroup', description: '[admin] List authorized groups' },
|
||||
{ command: 'addapple', description: 'Track an Apple App Store app' },
|
||||
{ command: 'delapple', description: 'Stop tracking an Apple app' },
|
||||
{ command: 'addgoogle', description: 'Track a Google Play app' },
|
||||
{ command: 'delgoogle', description: 'Stop tracking a Google app' },
|
||||
{ command: 'listapp', description: 'List tracked apps in this group' },
|
||||
{ command: 'checkapp', description: 'Check update status of tracked apps' },
|
||||
{ command: 'checkappscore', description: 'Check scores + ratings of tracked apps' },
|
||||
{ command: 'rawappleapp', description: 'Dump raw Apple API JSON for an app' },
|
||||
{ command: 'rawgoogleapp', description: 'Dump raw Google API JSON for an app' },
|
||||
{ command: 'settings', description: "Show this group's settings" },
|
||||
{ command: 'setdayswarning', description: 'Set warning threshold (days, 0 = default)' },
|
||||
];
|
||||
const adminIds = parseAdminIds(ADMIN_IDS_RAW);
|
||||
if (adminIds.length === 0) {
|
||||
console.error('ADMIN_IDS must contain at least one numeric Telegram user ID');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function tg(method, payload) {
|
||||
async function tg(method, payload, { allowFail = false } = {}) {
|
||||
if (DRY) {
|
||||
console.log(`[dry-run] ${method}`, JSON.stringify(payload, null, 2));
|
||||
return { ok: true, result: '(dry)' };
|
||||
@@ -49,6 +48,7 @@ async function tg(method, payload) {
|
||||
});
|
||||
const body = await res.json();
|
||||
if (!body.ok) {
|
||||
if (allowFail) return body;
|
||||
console.error(`${method} failed`, body);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -60,6 +60,30 @@ await tg('setWebhook', {
|
||||
secret_token: SECRET,
|
||||
allowed_updates: ['message'],
|
||||
});
|
||||
await tg('setMyCommands', { commands: COMMANDS });
|
||||
|
||||
// Default scope: every chat sees these unless a more specific scope overrides.
|
||||
await tg('setMyCommands', { commands: TELEGRAM_USER_COMMANDS });
|
||||
|
||||
// Per-admin chat scope: full set including admin commands.
|
||||
// Telegram requires the admin has DMed the bot at least once for `chat` scope
|
||||
// to resolve. If they haven't, the call returns "chat not found" — log + skip
|
||||
// rather than aborting the whole register flow.
|
||||
for (const adminId of adminIds) {
|
||||
const result = await tg(
|
||||
'setMyCommands',
|
||||
{
|
||||
commands: TELEGRAM_ADMIN_COMMANDS,
|
||||
scope: { type: 'chat', chat_id: adminId },
|
||||
},
|
||||
{ allowFail: true },
|
||||
);
|
||||
if (!result.ok) {
|
||||
console.warn(
|
||||
`[warn] setMyCommands for admin ${adminId} failed: ${result.description}. ` +
|
||||
`Admin must DM the bot at least once for chat-scoped menu.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const info = await tg('getWebhookInfo', {});
|
||||
console.log('Webhook state:', JSON.stringify(info.result, null, 2));
|
||||
+1
-9
@@ -1,13 +1,5 @@
|
||||
import { createLogger } from './logger.js';
|
||||
|
||||
function parseAdminIds(raw) {
|
||||
return raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
.map((s) => Number.parseInt(s, 10))
|
||||
.filter((n) => Number.isFinite(n));
|
||||
}
|
||||
import { parseAdminIds } from './util/parse-admin-ids.js';
|
||||
|
||||
// Builds config from a plain env dictionary (Vercel's `process.env` or any
|
||||
// dict-like). Called once per webhook / cron invocation; cheap.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Parses ADMIN_IDS env (comma-separated Telegram user IDs).
|
||||
// Shared between config loading and the register-webhook script.
|
||||
export function parseAdminIds(raw) {
|
||||
return (raw ?? '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
.map((s) => Number.parseInt(s, 10))
|
||||
.filter((n) => Number.isFinite(n));
|
||||
}
|
||||
Reference in new issue
Block a user