- commands.test.mjs: commandSources gains commands/cari.ts; CS4 documents the SECOND inline-keyboard carve (venue card allowed, reply keyboard still banned in commands/); CS5 menu becomes help/privacy/cari; CS5b's future-stub ban narrows to /pergi_ke (reserved for Phase 5, D-01); CS6 registers cari+search in the alias set - cari.test.mjs NEW: CR1-CR15 — payload shapes captured through the real createBot + fake baseFetch (getMe included), plus source-introspection gates with the stripComments helper: venue fields, OPEN URL = configured origin + plain slug id, NO-CRYPTO law (D-11), one-message alternates, callback_data discipline, honest refusal, 128-char cap, always-answer, category-only logging (D-15), bilingual copy, registration script, swap semantics, hostile-id drop - register-commands.mjs: menu list gains cari (two fetch calls unchanged) - [Rule 1] location-fallback.ts: the message handler now passes non-location messages through (next) — it matched EVERY message and silently starved all downstream message-stage handlers, so /cari never fired through the real factory; caught by the fake-transport suite - suites: bot 65/65 (15 CR + 9 CS + all prior), web 33/33, search 57/57, bot:typecheck green
93 lines
3.9 KiB
JavaScript
93 lines
3.9 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() };
|
|
});
|
|
}
|
|
|
|
/* ————— exactly two API calls: default scope, then the ms scope (D-03/D-04) ————— */
|
|
|
|
const API = `https://api.telegram.org/bot${token}/setMyCommands`;
|
|
const JSON_HEADERS = { "content-type": "application/json" };
|
|
|
|
const defaultPayload = { commands: menuFor("en"), bot_command_scope: { type: "default" } };
|
|
const msPayload = { commands: menuFor("ms"), bot_command_scope: { type: "default" }, language_code: "ms" };
|
|
|
|
const defaultRes = await fetch(API, { method: "POST", headers: JSON_HEADERS, body: JSON.stringify(defaultPayload) });
|
|
const defaultBody = await defaultRes.json();
|
|
console.log(`default scope -> ok:${defaultBody.ok} result:${JSON.stringify(defaultBody.result ?? null)}`);
|
|
|
|
const msRes = await fetch(API, { method: "POST", headers: JSON_HEADERS, body: JSON.stringify(msPayload) });
|
|
const msBody = await msRes.json();
|
|
console.log(`ms scope -> ok:${msBody.ok} result:${JSON.stringify(msBody.result ?? null)}`);
|
|
|
|
if (!defaultBody.ok || !msBody.ok) {
|
|
const firstError = !defaultBody.ok ? defaultBody : msBody;
|
|
console.error(`setMyCommands failed: ${firstError.error_code ?? "?"} ${firstError.description ?? "unknown error"}`);
|
|
process.exit(1);
|
|
}
|