mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-10-11 03:13:50 +00:00
Refactors source to be Worker-shaped. No live deploy yet — sister deploy plan
runs Atlas provisioning + smoke later.
- wrangler.toml with nodejs_compat_v2, daily UTC 0 cron (= 7am Asia/Ho_Chi_Minh)
- package.json: drop node-telegram-bot-api, node-cron, dotenv, pino,
pino-pretty; add wrangler devDep; bump to 0.2.0
- src/bot/telegram-api.js: raw fetch wrapper for Telegram Bot API
- src/bot/dispatch.js: per-message dispatcher extracted from polling loop
- src/repository/mongodb.js: memoized MongoClient per warm isolate, typed
MongoUnavailable error, fast-fail timeouts
- src/repository/store.js: factory binding env once
- All 4 repositories converted to factory shape
- src/api/{apple,google}-scraper.js: take store instead of importing repos
- src/index.js: Worker entry exporting { fetch, scheduled }; webhook validates
X-Telegram-Bot-Api-Secret-Token; ack-then-waitUntil pattern
- src/scheduler/scheduler.js: trimmed; runDailyCheck only (no node-cron)
- src/config.js, src/logger.js: env-driven, console.log JSON output
- scripts/register-webhook.js: setWebhook + setMyCommands; --dry-run supported
- scripts/check-secret-leaks.js: lint blocks console.log(env.<SECRET>)
- plans/260426-2015-cloudflare-worker-code-port: this code port plan
- plans/260426-2327-cloudflare-deploy-and-smoke: sister deploy plan
Validated via node --check on all 32 source files; lint clean. Real deploy
gates (bundle size, cold-start CPU) run in deploy plan.
58 lines
1.9 KiB
JavaScript
58 lines
1.9 KiB
JavaScript
import { buildTable } from '../../util/table.js';
|
|
import { authorizeGroup, getCommandArguments, splitArgs } from './command-utils.js';
|
|
|
|
// /checkappscore — Java CheckAppScoreCommand. Reports score + ratings.
|
|
// Score rounded to 1 decimal (Java Precision.round(score, 1) parity).
|
|
export function createCheckAppScoresCommand(store, appleScraper, googleScraper) {
|
|
return async (msg, sender) => {
|
|
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
|
|
if (splitArgs(getCommandArguments(msg.text)).length !== 0) {
|
|
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
|
|
return;
|
|
}
|
|
try {
|
|
const group = await store.group.getGroup(msg.chat.id);
|
|
const headers = ['AppId', 'Score', 'Ratings'];
|
|
const appleRows = await scoreRowsFor(group.appleApps, appleScraper);
|
|
const googleRows = await scoreRowsFor(group.googleApps, googleScraper);
|
|
|
|
const out =
|
|
'<b>Apple Apps</b>\n' +
|
|
renderTable(appleRows, headers) +
|
|
'\n<b>Google Apps</b>\n' +
|
|
renderTable(googleRows, headers);
|
|
await sender.sendMessage(msg.chat.id, out);
|
|
} catch {
|
|
await sender.sendMessage(msg.chat.id, 'Internal server error');
|
|
}
|
|
};
|
|
}
|
|
|
|
async function scoreRowsFor(apps, scraper) {
|
|
const rows = [];
|
|
for (const a of apps) {
|
|
try {
|
|
const resp = await scraper.getApp(a.appId, a.country);
|
|
if (!resp) {
|
|
rows.push([a.appId, '?', '?']);
|
|
continue;
|
|
}
|
|
rows.push([a.appId, formatScore(resp.score), String(resp.ratings ?? 0)]);
|
|
} catch {
|
|
rows.push([a.appId, '?', '?']);
|
|
}
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
function renderTable(rows, headers) {
|
|
if (rows.length === 0) return '<i>(none)</i>\n';
|
|
return `<pre>${buildTable(headers, rows)}</pre>\n`;
|
|
}
|
|
|
|
function formatScore(score) {
|
|
const v = Number(score);
|
|
if (!Number.isFinite(v)) return '?';
|
|
return (Math.round(v * 10) / 10).toFixed(1);
|
|
}
|