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 ( `Daily App Check Report\n` + `Date: ${formatDateTimeInTz(now, config.timezone)}\n` + `Group: ${groupId}\n` + `Apps not updated in >${config.numDaysWarningNotUpdated} days: ${apps.length}\n\n` + `
${buildTable(headers, rows)}
` ); }