refactor: dispatcher pre-parses args, drop arg helpers + info.js

The dispatcher now extracts both the command name and its arguments
once and passes the args array to handlers via a 3rd parameter. Each
command file drops its splitArgs(getCommandArguments(msg.text)) call
and the corresponding imports. command-utils.js loses the now-unused
arg helpers (only auth helpers remain). info.js — a 12-line file with
one sendMessage — folds into bot.js's commands map.
This commit is contained in:
2026-05-09 22:35:38 +07:00
parent ee491f9eb5
commit 3f586d1d9f
16 changed files with 66 additions and 92 deletions
+7 -4
View File
@@ -1,5 +1,4 @@
import { createTelegramApi } from './telegram-api.js';
import { createInfoCommand } from './commands/info.js';
import { createAddGroupCommand } from './commands/add-group.js';
import { createDeleteGroupCommand } from './commands/delete-group.js';
import { createListGroupCommand } from './commands/list-group.js';
@@ -13,7 +12,6 @@ import { createCheckAppScoresCommand } from './commands/check-app-scores.js';
import { createRawAppleAppCommand } from './commands/raw-apple-app.js';
import { createRawGoogleAppCommand } from './commands/raw-google-app.js';
// HTML parse mode for all messages (Java parity).
const PARSE_MODE = 'HTML';
export function createBot(config, store, appleScraper, googleScraper) {
@@ -51,9 +49,14 @@ export function createBot(config, store, appleScraper, googleScraper) {
},
};
// Java command identifiers — keep names matching exactly.
const commands = {
info: createInfoCommand(),
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),
+3 -4
View File
@@ -1,11 +1,10 @@
import { buildAppleRequestByBundleId, buildAppleRequestByTrackId } from '../../api/apple-scraper.js';
import { authorizeGroup, getCommandArguments, splitArgs } from './command-utils.js';
import { authorizeGroup } from './command-utils.js';
// /addapple <id|appId> [country=vn] — Java AddAppleAppCommand.
// /addapple <id|appId> [country=vn]
export function createAddAppleAppCommand(store, appleScraper) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
const args = splitArgs(getCommandArguments(msg.text));
if (args.length < 1 || args.length > 2) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
+3 -4
View File
@@ -1,11 +1,10 @@
import { buildGoogleRequest } from '../../api/google-scraper.js';
import { authorizeGroup, getCommandArguments, splitArgs } from './command-utils.js';
import { authorizeGroup } from './command-utils.js';
// /addgoogle <appId> [country=vn] — Java AddGoogleAppCommand.
// /addgoogle <appId> [country=vn]
export function createAddGoogleAppCommand(store, googleScraper) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
const args = splitArgs(getCommandArguments(msg.text));
if (args.length < 1 || args.length > 2) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
+3 -4
View File
@@ -1,10 +1,9 @@
import { getCommandArguments, requireAdminUser, splitArgs } from './command-utils.js';
import { requireAdminUser } from './command-utils.js';
// /addgroup [groupId] — Java AddGroupCommand. Admin-only.
// /addgroup [groupId] — admin-only. Defaults to current chat.
export function createAddGroupCommand(config, store) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await requireAdminUser(msg.from.id, msg.chat.id, config, sender))) return;
const args = splitArgs(getCommandArguments(msg.text));
if (args.length > 1) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
+4 -5
View File
@@ -1,12 +1,11 @@
import { buildTable } from '../../util/table.js';
import { authorizeGroup, getCommandArguments, splitArgs } from './command-utils.js';
import { authorizeGroup } from './command-utils.js';
// /checkappscore — Java CheckAppScoreCommand. Reports score + ratings.
// Score rounded to 1 decimal (Java Precision.round(score, 1) parity).
// /checkappscore — reports score + ratings. Score rounded to 1 decimal.
export function createCheckAppScoresCommand(store, appleScraper, googleScraper) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
if (splitArgs(getCommandArguments(msg.text)).length !== 0) {
if (args.length !== 0) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
}
+4 -4
View File
@@ -1,12 +1,12 @@
import { buildTable } from '../../util/table.js';
import { daysBetween, formatDateInTz } from '../../util/time.js';
import { authorizeGroup, getCommandArguments, splitArgs } from './command-utils.js';
import { authorizeGroup } from './command-utils.js';
// /checkapp — Java CheckAppCommand. Reports update status per app, per store.
// /checkapp — reports update status per app, per store.
export function createCheckAppCommand(config, store, appleScraper, googleScraper) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
if (splitArgs(getCommandArguments(msg.text)).length !== 0) {
if (args.length !== 0) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
}
-14
View File
@@ -1,17 +1,3 @@
export function splitArgs(text) {
if (!text) return [];
return text.trim().split(/\s+/).filter((s) => s.length > 0);
}
// Strips the "/<cmd>" or "/<cmd>@botname" prefix from message.text.
export function getCommandArguments(text) {
if (!text) return '';
const trimmed = text.trim();
const space = trimmed.indexOf(' ');
if (space < 0) return '';
return trimmed.slice(space + 1).trim();
}
export async function authorizeGroup(chatId, store, sender) {
try {
const ok = await store.admin.hasGroup(chatId);
+3 -4
View File
@@ -1,10 +1,9 @@
import { authorizeGroup, getCommandArguments, splitArgs } from './command-utils.js';
import { authorizeGroup } from './command-utils.js';
// /delapple <appId> — Java DeleteAppleAppCommand.
// /delapple <appId>
export function createDeleteAppleAppCommand(store) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
const args = splitArgs(getCommandArguments(msg.text));
if (args.length !== 1) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
+3 -4
View File
@@ -1,10 +1,9 @@
import { authorizeGroup, getCommandArguments, splitArgs } from './command-utils.js';
import { authorizeGroup } from './command-utils.js';
// /delgoogle <appId> — Java DeleteGoogleAppCommand.
// /delgoogle <appId>
export function createDeleteGoogleAppCommand(store) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
const args = splitArgs(getCommandArguments(msg.text));
if (args.length !== 1) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
+3 -4
View File
@@ -1,10 +1,9 @@
import { getCommandArguments, requireAdminUser, splitArgs } from './command-utils.js';
import { requireAdminUser } from './command-utils.js';
// /delgroup [groupId] — Java DeleteGroupCommand. Admin-only.
// /delgroup [groupId] — admin-only.
export function createDeleteGroupCommand(config, store) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await requireAdminUser(msg.from.id, msg.chat.id, config, sender))) return;
const args = splitArgs(getCommandArguments(msg.text));
if (args.length > 1) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
-12
View File
@@ -1,12 +0,0 @@
import { getCommandArguments, splitArgs } from './command-utils.js';
// /info — Java InfoCommand. Reports the chat (group) ID.
export function createInfoCommand() {
return async (msg, sender) => {
if (splitArgs(getCommandArguments(msg.text)).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`);
};
}
+4 -4
View File
@@ -1,11 +1,11 @@
import { buildTable } from '../../util/table.js';
import { authorizeGroup, getCommandArguments, splitArgs } from './command-utils.js';
import { authorizeGroup } from './command-utils.js';
// /listapp — Java ListAppCommand. Two tables (Apple / Google) of tracked apps.
// /listapp — two tables (Apple / Google) of tracked apps.
export function createListAppCommand(store) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
if (splitArgs(getCommandArguments(msg.text)).length !== 0) {
if (args.length !== 0) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
}
+4 -4
View File
@@ -1,10 +1,10 @@
import { getCommandArguments, requireAdminUser, splitArgs } from './command-utils.js';
import { requireAdminUser } from './command-utils.js';
// /listgroup — Java ListGroupCommand. Admin-only.
// /listgroup — admin-only.
export function createListGroupCommand(config, store) {
return async (msg, sender) => {
return async (msg, sender, args) => {
if (!(await requireAdminUser(msg.from.id, msg.chat.id, config, sender))) return;
if (splitArgs(getCommandArguments(msg.text)).length !== 0) {
if (args.length !== 0) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
}
+2 -5
View File
@@ -1,11 +1,8 @@
import { buildAppleRequestByBundleId, buildAppleRequestByTrackId } from '../../api/apple-scraper.js';
import { getCommandArguments, splitArgs } from './command-utils.js';
// /rawappleapp <id|appId> [country=vn] — Java RawAppleAppCommand.
// Sends raw upstream JSON as a Telegram document attachment.
// /rawappleapp <id|appId> [country=vn] — sends raw upstream JSON as a document.
export function createRawAppleAppCommand(appleScraper) {
return async (msg, sender) => {
const args = splitArgs(getCommandArguments(msg.text));
return async (msg, sender, args) => {
if (args.length < 1 || args.length > 2) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
+2 -4
View File
@@ -1,10 +1,8 @@
import { buildGoogleRequest } from '../../api/google-scraper.js';
import { getCommandArguments, splitArgs } from './command-utils.js';
// /rawgoogleapp <appId> [country=vn] — Java RawGoogleAppCommand.
// /rawgoogleapp <appId> [country=vn]
export function createRawGoogleAppCommand(googleScraper) {
return async (msg, sender) => {
const args = splitArgs(getCommandArguments(msg.text));
return async (msg, sender, args) => {
if (args.length < 1 || args.length > 2) {
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
return;
+21 -12
View File
@@ -4,8 +4,9 @@ export async function dispatch(message, deps) {
const { sender, commands, config, logger } = deps;
if (!message?.text || message.text[0] !== '/') return;
const name = parseCommandName(message.text, config.telegramBotUsername);
if (!name) return;
const parsed = parseCommand(message.text, config.telegramBotUsername);
if (!parsed) return;
const { name, args } = parsed;
const handler = commands[name];
if (!handler) {
@@ -18,7 +19,7 @@ export async function dispatch(message, deps) {
'Executing command',
);
try {
await handler(message, sender);
await handler(message, sender, args);
} catch (err) {
logger.error({ err: err.message, command: name }, 'command failed');
try {
@@ -29,14 +30,22 @@ export async function dispatch(message, deps) {
}
}
// Extracts "info" from "/info", "/info arg", "/info@bot", "/info@bot arg".
function parseCommandName(text, botUsername) {
const space = text.indexOf(' ');
const head = space < 0 ? text.slice(1) : text.slice(1, space);
// Extracts name + args from "/cmd", "/cmd arg", "/cmd@bot", "/cmd@bot arg".
// Returns null if the @bot suffix targets a different bot.
function parseCommand(text, botUsername) {
const trimmed = text.trim();
const space = trimmed.indexOf(' ');
const head = space < 0 ? trimmed.slice(1) : trimmed.slice(1, space);
const argText = space < 0 ? '' : trimmed.slice(space + 1).trim();
const at = head.indexOf('@');
if (at < 0) return head;
const cmd = head.slice(0, at);
const target = head.slice(at + 1);
if (botUsername && target && target.toLowerCase() !== botUsername.toLowerCase()) return null;
return cmd;
let name = head;
if (at >= 0) {
name = head.slice(0, at);
const target = head.slice(at + 1);
if (botUsername && target && target.toLowerCase() !== botUsername.toLowerCase()) return null;
}
const args = argText ? argText.split(/\s+/) : [];
return { name, args };
}