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.
117 lines
3.9 KiB
JavaScript
117 lines
3.9 KiB
JavaScript
import { buildTable, formatNumber, truncateString } from '../util/table.js';
|
|
import { daysBetween, formatDateInTz, formatDateTimeInTz, weekdayInTz } from '../util/time.js';
|
|
|
|
// One-shot daily check, invoked from the Worker `scheduled` handler. The cron
|
|
// schedule lives in wrangler.toml ("0 0 * * *" UTC = 7am Asia/Ho_Chi_Minh).
|
|
export async function runDailyCheck(config, store, sender, appleScraper, googleScraper) {
|
|
const logger = config.logger;
|
|
const now = new Date();
|
|
const dow = weekdayInTz(now, config.timezone);
|
|
const silent = dow === 0 || dow === 6;
|
|
logger.info({ silent }, 'Running daily check job');
|
|
|
|
let groups;
|
|
try {
|
|
groups = await store.admin.getAllGroups();
|
|
} catch (err) {
|
|
logger.error({ err: err.message }, 'Failed to get groups');
|
|
return;
|
|
}
|
|
|
|
for (const gid of groups) {
|
|
try {
|
|
await checkGroup(gid, silent, now, config, store, sender, appleScraper, googleScraper);
|
|
} catch (err) {
|
|
logger.error({ err: err.message, groupId: gid }, 'check group failed');
|
|
}
|
|
}
|
|
logger.info({ groupsChecked: groups.length }, 'Daily check job completed');
|
|
}
|
|
|
|
async function checkGroup(groupId, silent, now, config, store, sender, appleScraper, googleScraper) {
|
|
const logger = config.logger;
|
|
const group = await store.group.getGroup(groupId);
|
|
if (group.appleApps.length === 0 && group.googleApps.length === 0) {
|
|
logger.info({ groupId }, 'Group has no apps, skipping');
|
|
return;
|
|
}
|
|
|
|
const threshold = config.numDaysWarningNotUpdated;
|
|
const stale = [];
|
|
|
|
for (const info of group.appleApps) {
|
|
try {
|
|
const app = await appleScraper.getApp(info.appId, info.country);
|
|
if (!app) continue;
|
|
const updatedMs = Date.parse(app.updated);
|
|
if (Number.isNaN(updatedMs)) continue;
|
|
const days = daysBetween(updatedMs, now.getTime());
|
|
if (days > threshold) {
|
|
stale.push({
|
|
appId: info.appId,
|
|
title: app.title,
|
|
days,
|
|
updated: formatDateInTz(new Date(updatedMs), config.timezone),
|
|
score: app.score,
|
|
reviews: Number(app.reviews ?? 0),
|
|
ratings: Number(app.ratings ?? 0),
|
|
isApple: true,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logger.error({ err: err.message, appId: info.appId }, 'Apple fetch failed');
|
|
}
|
|
}
|
|
|
|
for (const info of group.googleApps) {
|
|
try {
|
|
const app = await googleScraper.getApp(info.appId, info.country);
|
|
if (!app) continue;
|
|
const updatedMs = Number(app.updated);
|
|
const days = daysBetween(updatedMs, now.getTime());
|
|
if (days > threshold) {
|
|
stale.push({
|
|
appId: info.appId,
|
|
title: app.title,
|
|
days,
|
|
updated: formatDateInTz(new Date(updatedMs), config.timezone),
|
|
score: app.score,
|
|
reviews: Number(app.reviews ?? 0),
|
|
ratings: Number(app.ratings ?? 0),
|
|
isApple: false,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logger.error({ err: err.message, appId: info.appId }, 'Google fetch failed');
|
|
}
|
|
}
|
|
|
|
if (stale.length === 0) {
|
|
logger.info({ groupId }, 'All apps up-to-date');
|
|
return;
|
|
}
|
|
const message = buildReport(groupId, stale, now, config);
|
|
if (silent) await sender.sendMessageSilent(groupId, message);
|
|
else await sender.sendMessage(groupId, message);
|
|
}
|
|
|
|
function buildReport(groupId, apps, now, config) {
|
|
const headers = ['App', 'Store', 'Days', 'Updated', 'Score', 'Reviews', 'Ratings'];
|
|
const rows = apps.map((a) => [
|
|
truncateString(a.title || '', 30),
|
|
a.isApple ? 'Apple' : 'Google',
|
|
String(a.days),
|
|
a.updated,
|
|
Number(a.score ?? 0).toFixed(1),
|
|
String(a.reviews),
|
|
formatNumber(a.ratings),
|
|
]);
|
|
return (
|
|
`<b>Daily App Check Report</b>\n` +
|
|
`Date: ${formatDateTimeInTz(now, config.timezone)}\n` +
|
|
`Group: <code>${groupId}</code>\n` +
|
|
`Apps not updated in >${config.numDaysWarningNotUpdated} days: <b>${apps.length}</b>\n\n` +
|
|
`<pre>${buildTable(headers, rows)}</pre>`
|
|
);
|
|
}
|