Files
arah2/bot/scripts/register-commands.mjs
T
araharah-executor 141d6d2a88 fix(07-06): root-cause the /cari menu defect — register-commands.mjs sent the wrong Bot API field name
- the Bot API parameter is 'scope' (verified against
  core.telegram.org/bots/api/methods/setmycommands); the script's
  'bot_command_scope' was an unknown field Telegram silently ignores,
  so every call hit the default scope and the two clear calls wiped the
  default set entirely — the ok:true lines proved nothing about scope
- registration re-run: default (agnostic/en/ms) + all_private_chats
  (agnostic/ms) read back help, privacy, cari via getMyCommands with
  the correct 'scope' param; group scopes verified cleared
- docs/KNOWN-LIMITATIONS.md: the 'Telegram-side defect' attribution
  corrected to the actual script bug; residual client menu-caching
  caveat + type-the-command workaround kept
- bot/README.md command row updated to the real scope set
2026-10-25 05:29:38 +08:00

115 lines
5.0 KiB
JavaScript

#!/usr/bin/env node
/*
* register-commands.mjs — one-time setMyCommands registration (D-03, D-04, D-17).
* -------------------------------------------------------------------------------
* Registers the bot's persistent command menu in two scopes:
* 1. default scope — English /help and /privacy descriptions
* 2. default + language_code "ms" — Malay descriptions for ms users
*
* D-03: the menu lists ONLY commands that exist in this phase (/help,
* /privacy, /cari) — no stub entries reserved for future commands (the
* Phase-5 navigation command stays reserved and unregistered). There are
* exactly two fetch calls below, one per scope.
*
* D-05 (single copy source): the descriptions are not inline here — they
* are derived from src/i18n/strings.ts `helpMenu` lines ("/cmd — text"),
* the same text /help replies with, so reply and registered menu cannot
* drift apart.
*
* Secrets stay secret (T-02-04): BOT_TOKEN is read from bot/.dev.vars
* (never committed, D-15) and is never echoed — only the API response's
* ok/result fields are printed.
*/
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { strings } from "../src/i18n/strings.ts";
/* ————— configuration (D-15: local secrets from gitignored .dev.vars) ————— */
function readDevVars() {
const devVarsPath = path.join(fileURLToPath(new URL(import.meta.url)), "..", "..", ".dev.vars");
let raw = "";
try {
raw = readFileSync(devVarsPath, "utf8");
} catch {
return {};
}
const vars = {};
for (const line of raw.split("\n")) {
const trimmed = line.trim();
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq === -1) continue;
vars[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
}
return vars;
}
const token = process.env.BOT_TOKEN ?? readDevVars().BOT_TOKEN;
if (!token) {
console.error(
"BOT_TOKEN missing — copy bot/.dev.vars.example to bot/.dev.vars (gitignored) and paste your @BotFather token.",
);
process.exit(1);
}
/* ————— menu derived from the strings module (D-03/D-05, gap-free) ————— */
/** Extract the `/cmd — description` entries from a locale's helpMenu. */
function menuFor(locale) {
const lines = strings[locale].helpMenu.split("\n");
return ["help", "privacy", "cari"].map((command) => {
const marker = `/${command} — `;
const line = lines.find((l) => l.startsWith(marker));
if (line === undefined) {
throw new Error(`helpMenu for "${locale}" is missing its /${command} line (D-03 copy drift)`);
}
return { command, description: line.slice(marker.length).trim() };
});
}
/* ————— scope set (device-gate fix 2026-10-24): Telegram resolves a chat's
* command menu from the NARROWEST scope that has ANY commands set — a stale
* all_private_chats set shadows the default scope entirely (the /cari-missing-
* from-menu incident). The script therefore arms every scope it may shadow:
* default (en) · default+ms · all_private_chats (en) · all_private_chats+ms
* and CLEARS the group scopes (leftover phase-2 experiments) so no junk set
* shadows anything elsewhere.
*
* GATE-BUG FIX (07-06 canary, 2026-10-25): the Bot API parameter is `scope`
* (https://core.telegram.org/bots/api/methods/setmycommands) — this script
* previously sent `bot_command_scope`, an unknown field Telegram SILENTLY
* ignores, so every call actually hit the default scope and the two "clear"
* calls wiped the default set entirely (the true root cause of the /cari
* menu defect; the setMyCommands ok:true lines proved nothing about scope).
* ————— */
const API = `https://api.telegram.org/bot${token}/setMyCommands`;
const JSON_HEADERS = { "content-type": "application/json" };
const CALLS = [
{ label: "default (en)", body: { commands: menuFor("en"), scope: { type: "default" } } },
{ label: "default (ms)", body: { commands: menuFor("ms"), scope: { type: "default" }, language_code: "ms" } },
{ label: "all_private_chats (en)", body: { commands: menuFor("en"), scope: { type: "all_private_chats" } } },
{ label: "all_private_chats (ms)", body: { commands: menuFor("ms"), scope: { type: "all_private_chats" }, language_code: "ms" } },
{ label: "clear all_group_chats", body: { commands: [], scope: { type: "all_group_chats" } } },
{ label: "clear all_chat_administrators", body: { commands: [], scope: { type: "all_chat_administrators" } } },
];
let failed = null;
for (const call of CALLS) {
const res = await fetch(API, { method: "POST", headers: JSON_HEADERS, body: JSON.stringify(call.body) });
const body = await res.json();
console.log(`${call.label.padEnd(30)} -> ok:${body.ok} ${body.ok ? "" : JSON.stringify(body)}`);
if (!body.ok && failed === null) failed = body;
}
if (!failed) {
console.log("all command scopes armed");
} else {
const firstError = failed;
console.error(`setMyCommands failed: ${firstError.error_code ?? "?"} ${firstError.description ?? "unknown error"}`);
process.exit(1);
}