mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-10-13 12:15:05 +00:00
feat: per-group days-to-warning override + /settings + /setdayswarning
Adds optional group.settings.numDaysWarningNotUpdated, resolved per-group in scheduler and /checkapp with fallback to env default. New commands /settings (read) and /setdayswarning <n|0|default> (write).
This commit is contained in:
1 parent
c32688f41b
commit
01312065c5
11 files changed
+353
-5
No files matched your search
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
phase: 1
|
||||||
|
title: Schema + threshold resolver
|
||||||
|
status: completed
|
||||||
|
priority: P2
|
||||||
|
effort: 30m
|
||||||
|
dependencies: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 1: Schema + threshold resolver
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Extend the per-group state with an optional `settings` object and add one helper that resolves the effective warning threshold. Replace direct `config.numDaysWarningNotUpdated` reads at all call sites.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Storage shape (Upstash key `group:{chatId}`)**
|
||||||
|
```js
|
||||||
|
{ appleApps: [...], googleApps: [...], settings: { numDaysWarningNotUpdated?: number } }
|
||||||
|
```
|
||||||
|
- `settings` is optional. Missing → behaves identically to today.
|
||||||
|
- Additive change; no migration needed.
|
||||||
|
|
||||||
|
**Resolver (single source of truth)**
|
||||||
|
```js
|
||||||
|
// src/util/group-settings.js
|
||||||
|
export function resolveDaysWarning(group, config) {
|
||||||
|
const v = group?.settings?.numDaysWarningNotUpdated;
|
||||||
|
return Number.isFinite(v) && v > 0 ? v : config.numDaysWarningNotUpdated;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Repository surface**
|
||||||
|
Add to `createGroupRepository`:
|
||||||
|
```js
|
||||||
|
async function setSetting(groupId, key, value) {
|
||||||
|
return mutateAndSave(groupId, (g) => {
|
||||||
|
g.settings ??= {};
|
||||||
|
if (value === undefined || value === null) delete g.settings[key];
|
||||||
|
else g.settings[key] = value;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Generic so `/setdayswarning` and any future setter share one path.
|
||||||
|
- `emptyGroup()` does NOT add `settings: {}` — keep payload minimal; resolver tolerates absence.
|
||||||
|
|
||||||
|
## Related Code Files
|
||||||
|
|
||||||
|
**Create**
|
||||||
|
- `src/util/group-settings.js` — `resolveDaysWarning(group, config)`
|
||||||
|
|
||||||
|
**Modify**
|
||||||
|
- `src/repository/group-repository.js` — add `setSetting` to public surface
|
||||||
|
- `src/scheduler/scheduler.js` — replace `config.numDaysWarningNotUpdated` reads (lines 39, 114) with `resolveDaysWarning(group, config)`
|
||||||
|
- `src/bot/commands/check-app.js` — replace `config.numDaysWarningNotUpdated` (line 15) with resolver
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. Create `src/util/group-settings.js` with `resolveDaysWarning`.
|
||||||
|
2. In `group-repository.js`, add `setSetting` mutator + export it.
|
||||||
|
3. In `scheduler.js#checkGroup`, after `await store.group.getGroup(groupId)`, compute `const threshold = resolveDaysWarning(group, config);` — drop the `config.numDaysWarningNotUpdated` ref.
|
||||||
|
4. In `scheduler.js#buildReport`, accept the resolved threshold (pass it through) and use it in the header string instead of `config.numDaysWarningNotUpdated`.
|
||||||
|
5. In `check-app.js`, swap line 15 to `const threshold = resolveDaysWarning(group, config);`.
|
||||||
|
6. `node --check` all four touched files.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] Resolver returns env default when `group.settings` absent
|
||||||
|
- [ ] Resolver returns group override when valid integer > 0 stored
|
||||||
|
- [ ] Resolver ignores zero / negative / non-finite stored values (falls back)
|
||||||
|
- [ ] Scheduler + `/checkapp` use resolved value at every call site
|
||||||
|
- [ ] Daily report header shows the per-group threshold, not the global default
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
- **Header drift:** scheduler builds report header with global default today. If we forget to thread the resolved value into `buildReport`, groups with overrides see a misleading header. Mitigation: pass the resolved threshold as a fn arg, not a closure.
|
||||||
|
- **Bad stored values:** if a future bug writes a string/null, resolver must not crash. Mitigation: `Number.isFinite && > 0` guard.
|
||||||
|
- **Schema rot:** `emptyGroup()` not adding `settings` keeps payload minimal but means every reader must tolerate undefined. Mitigation: only the resolver reads it; `setSetting` lazily inits.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
phase: 2
|
||||||
|
title: Commands + wiring
|
||||||
|
status: completed
|
||||||
|
priority: P2
|
||||||
|
effort: 45m
|
||||||
|
dependencies:
|
||||||
|
- 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 2: Commands + wiring
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add `/settings` (read) and `/setdayswarning` (write) commands. Wire both into the dispatcher map in `bot.js`. Both gate on `authorizeGroup` like every other command except `/info`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Command names**
|
||||||
|
- `/settings` — generic reader, prints all settings + their effective values. Future settings drop in here.
|
||||||
|
- `/setdayswarning <n>` — single-purpose setter. Pattern: one setter command per setting key. Easier UX than a generic `/set <key> <value>`.
|
||||||
|
|
||||||
|
**Reset semantics for `/setdayswarning`**
|
||||||
|
- `<n>` parsed as integer
|
||||||
|
- `n >= 1 && n <= 3650` → store
|
||||||
|
- `n == 0` or arg `default` → reset (delete the setting key, fall back to env default)
|
||||||
|
- Anything else → "Invalid arguments"
|
||||||
|
|
||||||
|
**`/settings` output**
|
||||||
|
```
|
||||||
|
<b>Group Settings</b>
|
||||||
|
<pre>┌──────────────────────────┬───────┬─────────┐
|
||||||
|
│ Setting │ Value │ Default │
|
||||||
|
├──────────────────────────┼───────┼─────────┤
|
||||||
|
│ numDaysWarningNotUpdated │ 45 │ 30 │
|
||||||
|
└──────────────────────────┴───────┴─────────┘</pre>
|
||||||
|
```
|
||||||
|
Use existing `buildTable` from `src/util/table.js`. Show env default in a separate column so user understands what `0`/reset means.
|
||||||
|
|
||||||
|
## Related Code Files
|
||||||
|
|
||||||
|
**Create**
|
||||||
|
- `src/bot/commands/get-settings.js` — `createGetSettingsCommand(config, store)` returns handler
|
||||||
|
- `src/bot/commands/set-days-warning.js` — `createSetDaysWarningCommand(config, store)` returns handler
|
||||||
|
|
||||||
|
**Modify**
|
||||||
|
- `src/bot/bot.js` — import + register both:
|
||||||
|
```js
|
||||||
|
settings: createGetSettingsCommand(config, store),
|
||||||
|
setdayswarning: createSetDaysWarningCommand(config, store),
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. Create `src/bot/commands/get-settings.js`:
|
||||||
|
- Signature `(msg, sender, args) => …`
|
||||||
|
- First line: `if (!(await authorizeGroup(msg.chat.id, store, sender))) return;`
|
||||||
|
- Reject if `args.length !== 0`
|
||||||
|
- Read group via `store.group.getGroup(msg.chat.id)`
|
||||||
|
- Build settings rows: known keys list `[['numDaysWarningNotUpdated', g?.settings?.numDaysWarningNotUpdated, config.numDaysWarningNotUpdated]]`
|
||||||
|
- Render table with `buildTable`
|
||||||
|
- Send via `sender.sendMessage`
|
||||||
|
|
||||||
|
2. Create `src/bot/commands/set-days-warning.js`:
|
||||||
|
- Same auth guard
|
||||||
|
- Reject if `args.length !== 1`
|
||||||
|
- Parse arg: `Number.parseInt`
|
||||||
|
- If `args[0] === 'default'` or parsed `=== 0` → call `store.group.setSetting(chatId, 'numDaysWarningNotUpdated', undefined)`, reply "Reset to default (Nd)"
|
||||||
|
- If parsed valid (`Number.isFinite && >= 1 && <= 3650`) → `setSetting(chatId, 'numDaysWarningNotUpdated', parsed)`, reply "Days-to-warning set to N"
|
||||||
|
- Otherwise `sender.sendMessage(chatId, 'Invalid arguments')` and return
|
||||||
|
|
||||||
|
3. In `bot.js`:
|
||||||
|
- Add two imports near the other command imports
|
||||||
|
- Add two map entries in the `commands` object (preserve alphabetical-ish grouping or stick at end with raw\* commands — match existing convention)
|
||||||
|
|
||||||
|
4. `node --check` on the three modified/new files.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] `/settings` from authorized group prints table with current value + env default
|
||||||
|
- [ ] `/settings` from unauthorized group returns "Group is not allowed to use bot"
|
||||||
|
- [ ] `/setdayswarning 45` persists to Upstash; subsequent `/settings` shows 45
|
||||||
|
- [ ] `/setdayswarning 0` (or `default`) deletes the setting; resolver falls back to env default
|
||||||
|
- [ ] `/setdayswarning abc` / `/setdayswarning -5` / `/setdayswarning 9999` → "Invalid arguments"
|
||||||
|
- [ ] After `/setdayswarning 45`, daily cron + `/checkapp` use 45 for that group only
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
- **Command discoverability:** users may not know the command exists. Mitigation: out-of-scope (no help command exists in either Java or JS bot). Consider a future `/help` command.
|
||||||
|
- **Race on concurrent writes:** `mutateAndSave` is read-modify-write without a transaction. Two simultaneous `/setdayswarning` calls in the same group could clobber each other. Acceptable — low likelihood, low blast radius (last write wins on the same key).
|
||||||
|
- **No admin gate:** any group member can change the threshold. Matches existing model (any member can `/addapple`). If product wants admin-only later, add `requireAdminUser` check — pattern already exists in `command-utils.js`.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
phase: 3
|
||||||
|
title: Smoke test
|
||||||
|
status: completed
|
||||||
|
priority: P3
|
||||||
|
effort: 15m
|
||||||
|
dependencies:
|
||||||
|
- 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 3: Smoke test
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
No unit-test suite exists. Validate via syntax checks, the existing secret-leak lint, and a manual Telegram run-through against the deployed bot.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. `node --check` on every modified/new file:
|
||||||
|
- `src/util/group-settings.js`
|
||||||
|
- `src/repository/group-repository.js`
|
||||||
|
- `src/scheduler/scheduler.js`
|
||||||
|
- `src/bot/commands/check-app.js`
|
||||||
|
- `src/bot/commands/get-settings.js`
|
||||||
|
- `src/bot/commands/set-days-warning.js`
|
||||||
|
- `src/bot/bot.js`
|
||||||
|
2. `npm run lint` (secret-leak scan, must pass).
|
||||||
|
3. Deploy preview to Vercel (or merge to `main` if direct-deploy is normal).
|
||||||
|
4. Manual Telegram smoke (in an authorized test group):
|
||||||
|
- `/settings` → table shows `numDaysWarningNotUpdated = (unset) / default 30` (or whatever env value is)
|
||||||
|
- `/setdayswarning 5` → "Days-to-warning set to 5"
|
||||||
|
- `/settings` → shows `5 / 30`
|
||||||
|
- `/checkapp` → header reads `>5 days`, results reflect 5-day threshold
|
||||||
|
- `/setdayswarning 0` → "Reset to default (30d)"
|
||||||
|
- `/settings` → back to unset / 30
|
||||||
|
- `/setdayswarning abc` → "Invalid arguments"
|
||||||
|
- `/setdayswarning 9999` → "Invalid arguments"
|
||||||
|
- From an unauthorized chat: `/settings` → "Group is not allowed to use bot"
|
||||||
|
5. Wait for next daily cron OR force-trigger via Vercel cron-now panel; confirm authoritized group with override sees correct threshold in report header.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] All `node --check` pass
|
||||||
|
- [ ] `npm run lint` passes
|
||||||
|
- [ ] Manual Telegram run-through hits every bullet above
|
||||||
|
- [ ] Daily cron report header reflects per-group override (or shows env default for groups with no override)
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
- **No automated regression:** future refactors could silently break the resolver. Acceptable for now — repo has no tests at all. Document the resolver invariants in the file's leading comment.
|
||||||
|
- **Cron timing:** waiting up to 24h for natural cron firing slows verification. Mitigation: hit `/api/cron` directly with `Authorization: Bearer $CRON_SECRET` to force a run.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: per-group days-to-warning setting + settings commands
|
||||||
|
description: >-
|
||||||
|
Allow each Telegram group to override numDaysWarningNotUpdated. Add /settings
|
||||||
|
(read) and /setdayswarning (write).
|
||||||
|
status: completed
|
||||||
|
priority: P2
|
||||||
|
created: 2026-05-09T00:00:00.000Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# per-group days-to-warning setting + settings commands
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Today `numDaysWarningNotUpdated` is global env config (default 30). One threshold for all groups. Goal: let each group set its own threshold, falling back to env default. Also expose generic `/settings` reader so future per-group settings (timezone, silent days, etc.) drop in without new commands.
|
||||||
|
|
||||||
|
## Goals & Non-Goals
|
||||||
|
|
||||||
|
**Goals**
|
||||||
|
- `group.settings.numDaysWarningNotUpdated` optional override, persisted in Upstash with the rest of group state
|
||||||
|
- `/settings` — show all settings for the calling group (with effective + default values)
|
||||||
|
- `/setdayswarning <n>` — set the threshold (1..3650 integer); `0` or empty resets to default
|
||||||
|
- Scheduler + `/checkapp` use the per-group resolved threshold
|
||||||
|
|
||||||
|
**Non-Goals**
|
||||||
|
- Other settings (timezone, silent weekend, language) — schema is extensible but no values added yet
|
||||||
|
- Admin-only restriction — `authorizeGroup` only (matches sibling commands like `/addapple`)
|
||||||
|
- Migration of existing groups — schema is additive; missing `settings` reads as `{}`
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
| Phase | Name | Status |
|
||||||
|
|-------|------|--------|
|
||||||
|
| 1 | [Schema + threshold resolver](./phase-01-schema-threshold-resolver.md) | Completed |
|
||||||
|
| 2 | [Commands + wiring](./phase-02-commands-wiring.md) | Completed |
|
||||||
|
| 3 | [Smoke test](./phase-03-smoke-test.md) | Completed |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
None. Self-contained change. Depends only on existing Upstash group repository + command pattern.
|
||||||
@@ -11,6 +11,8 @@ import { createCheckAppCommand } from './commands/check-app.js';
|
|||||||
import { createCheckAppScoresCommand } from './commands/check-app-scores.js';
|
import { createCheckAppScoresCommand } from './commands/check-app-scores.js';
|
||||||
import { createRawAppleAppCommand } from './commands/raw-apple-app.js';
|
import { createRawAppleAppCommand } from './commands/raw-apple-app.js';
|
||||||
import { createRawGoogleAppCommand } from './commands/raw-google-app.js';
|
import { createRawGoogleAppCommand } from './commands/raw-google-app.js';
|
||||||
|
import { createGetSettingsCommand } from './commands/get-settings.js';
|
||||||
|
import { createSetDaysWarningCommand } from './commands/set-days-warning.js';
|
||||||
|
|
||||||
const PARSE_MODE = 'HTML';
|
const PARSE_MODE = 'HTML';
|
||||||
|
|
||||||
@@ -69,6 +71,8 @@ export function createBot(config, store, appleScraper, googleScraper) {
|
|||||||
checkappscore: createCheckAppScoresCommand(store, appleScraper, googleScraper),
|
checkappscore: createCheckAppScoresCommand(store, appleScraper, googleScraper),
|
||||||
rawappleapp: createRawAppleAppCommand(store, appleScraper),
|
rawappleapp: createRawAppleAppCommand(store, appleScraper),
|
||||||
rawgoogleapp: createRawGoogleAppCommand(store, googleScraper),
|
rawgoogleapp: createRawGoogleAppCommand(store, googleScraper),
|
||||||
|
settings: createGetSettingsCommand(config, store),
|
||||||
|
setdayswarning: createSetDaysWarningCommand(config, store),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { sender, commands, api };
|
return { sender, commands, api };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { buildTable } from '../../util/table.js';
|
import { buildTable } from '../../util/table.js';
|
||||||
import { daysBetween, formatDateInTz } from '../../util/time.js';
|
import { daysBetween, formatDateInTz } from '../../util/time.js';
|
||||||
|
import { resolveDaysWarning } from '../../util/group-settings.js';
|
||||||
import { authorizeGroup } from './command-utils.js';
|
import { authorizeGroup } from './command-utils.js';
|
||||||
|
|
||||||
// /checkapp — reports update status per app, per store.
|
// /checkapp — reports update status per app, per store.
|
||||||
@@ -12,7 +13,7 @@ export function createCheckAppCommand(config, store, appleScraper, googleScraper
|
|||||||
}
|
}
|
||||||
const group = await store.group.getGroup(msg.chat.id);
|
const group = await store.group.getGroup(msg.chat.id);
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
const threshold = config.numDaysWarningNotUpdated;
|
const threshold = resolveDaysWarning(group, config);
|
||||||
const headers = ['AppId', 'Updated', 'Days', 'OK'];
|
const headers = ['AppId', 'Updated', 'Days', 'OK'];
|
||||||
|
|
||||||
const appleRows = await appleRowsFor(group.appleApps, appleScraper, nowMs, threshold, config.timezone);
|
const appleRows = await appleRowsFor(group.appleApps, appleScraper, nowMs, threshold, config.timezone);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { buildTable } from '../../util/table.js';
|
||||||
|
import { authorizeGroup } from './command-utils.js';
|
||||||
|
|
||||||
|
// /settings — table of per-group setting overrides + their env defaults.
|
||||||
|
// Add new rows here when introducing new per-group settings.
|
||||||
|
export function createGetSettingsCommand(config, store) {
|
||||||
|
return async (msg, sender, args) => {
|
||||||
|
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
|
||||||
|
if (args.length !== 0) {
|
||||||
|
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = await store.group.getGroup(msg.chat.id);
|
||||||
|
const s = group?.settings ?? {};
|
||||||
|
const rows = [
|
||||||
|
[
|
||||||
|
'numDaysWarningNotUpdated',
|
||||||
|
formatValue(s.numDaysWarningNotUpdated),
|
||||||
|
String(config.numDaysWarningNotUpdated),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
const out = '<b>Group Settings</b>\n' + `<pre>${buildTable(['Setting', 'Value', 'Default'], rows)}</pre>`;
|
||||||
|
await sender.sendMessage(msg.chat.id, out);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValue(v) {
|
||||||
|
return v === undefined || v === null ? '(unset)' : String(v);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { authorizeGroup } from './command-utils.js';
|
||||||
|
|
||||||
|
const MAX_DAYS = 3650;
|
||||||
|
|
||||||
|
// /setdayswarning <n> — sets the per-group warning threshold.
|
||||||
|
// `0` or `default` resets to the env-config default.
|
||||||
|
export function createSetDaysWarningCommand(config, store) {
|
||||||
|
return async (msg, sender, args) => {
|
||||||
|
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
|
||||||
|
if (args.length !== 1) {
|
||||||
|
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const arg = args[0];
|
||||||
|
const parsed = Number.parseInt(arg, 10);
|
||||||
|
|
||||||
|
if (arg === 'default' || parsed === 0) {
|
||||||
|
await store.group.setSetting(msg.chat.id, 'numDaysWarningNotUpdated', undefined);
|
||||||
|
await sender.sendMessage(
|
||||||
|
msg.chat.id,
|
||||||
|
`Reset to default (${config.numDaysWarningNotUpdated}d)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(parsed) || String(parsed) !== arg || parsed < 1 || parsed > MAX_DAYS) {
|
||||||
|
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.group.setSetting(msg.chat.id, 'numDaysWarningNotUpdated', parsed);
|
||||||
|
await sender.sendMessage(msg.chat.id, `Days-to-warning set to ${parsed}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -34,6 +34,15 @@ export function createGroupRepository(handle) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setSetting(groupId, key, value) {
|
||||||
|
return mutateAndSave(groupId, (g) => {
|
||||||
|
g.settings ??= {};
|
||||||
|
if (value === undefined || value === null) delete g.settings[key];
|
||||||
|
else g.settings[key] = value;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addApp(list, appId, country) {
|
function addApp(list, appId, country) {
|
||||||
if (list.some((a) => a.appId === appId)) return false;
|
if (list.some((a) => a.appId === appId)) return false;
|
||||||
list.push({ appId, country });
|
list.push({ appId, country });
|
||||||
@@ -51,6 +60,7 @@ export function createGroupRepository(handle) {
|
|||||||
getGroup,
|
getGroup,
|
||||||
initGroup,
|
initGroup,
|
||||||
deleteGroup,
|
deleteGroup,
|
||||||
|
setSetting,
|
||||||
addAppleApp: (groupId, appId, country) =>
|
addAppleApp: (groupId, appId, country) =>
|
||||||
mutateAndSave(groupId, (g) => addApp(g.appleApps, appId, country)),
|
mutateAndSave(groupId, (g) => addApp(g.appleApps, appId, country)),
|
||||||
removeAppleApp: (groupId, appId) =>
|
removeAppleApp: (groupId, appId) =>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { buildTable, formatNumber, truncateString } from '../util/table.js';
|
import { buildTable, formatNumber, truncateString } from '../util/table.js';
|
||||||
import { daysBetween, formatDateInTz, formatDateTimeInTz, weekdayInTz } from '../util/time.js';
|
import { daysBetween, formatDateInTz, formatDateTimeInTz, weekdayInTz } from '../util/time.js';
|
||||||
|
import { resolveDaysWarning } from '../util/group-settings.js';
|
||||||
|
|
||||||
// One-shot daily check, invoked from api/cron.js. The cron schedule lives in
|
// One-shot daily check, invoked from api/cron.js. The cron schedule lives in
|
||||||
// vercel.json ("0 0 * * *" UTC = 7am Asia/Ho_Chi_Minh).
|
// vercel.json ("0 0 * * *" UTC = 7am Asia/Ho_Chi_Minh).
|
||||||
@@ -36,7 +37,7 @@ async function checkGroup(groupId, silent, now, config, store, sender, appleScra
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const threshold = config.numDaysWarningNotUpdated;
|
const threshold = resolveDaysWarning(group, config);
|
||||||
const stale = [];
|
const stale = [];
|
||||||
|
|
||||||
for (const info of group.appleApps) {
|
for (const info of group.appleApps) {
|
||||||
@@ -91,12 +92,12 @@ async function checkGroup(groupId, silent, now, config, store, sender, appleScra
|
|||||||
logger.info({ groupId }, 'All apps up-to-date');
|
logger.info({ groupId }, 'All apps up-to-date');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const message = buildReport(groupId, stale, now, config);
|
const message = buildReport(groupId, stale, now, config, threshold);
|
||||||
if (silent) await sender.sendMessageSilent(groupId, message);
|
if (silent) await sender.sendMessageSilent(groupId, message);
|
||||||
else await sender.sendMessage(groupId, message);
|
else await sender.sendMessage(groupId, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReport(groupId, apps, now, config) {
|
function buildReport(groupId, apps, now, config, threshold) {
|
||||||
const headers = ['App', 'Store', 'Days', 'Updated', 'Score', 'Reviews', 'Ratings'];
|
const headers = ['App', 'Store', 'Days', 'Updated', 'Score', 'Reviews', 'Ratings'];
|
||||||
const rows = apps.map((a) => [
|
const rows = apps.map((a) => [
|
||||||
truncateString(a.title || '', 30),
|
truncateString(a.title || '', 30),
|
||||||
@@ -111,7 +112,7 @@ function buildReport(groupId, apps, now, config) {
|
|||||||
`<b>Daily App Check Report</b>\n` +
|
`<b>Daily App Check Report</b>\n` +
|
||||||
`Date: ${formatDateTimeInTz(now, config.timezone)}\n` +
|
`Date: ${formatDateTimeInTz(now, config.timezone)}\n` +
|
||||||
`Group: <code>${groupId}</code>\n` +
|
`Group: <code>${groupId}</code>\n` +
|
||||||
`Apps not updated in >${config.numDaysWarningNotUpdated} days: <b>${apps.length}</b>\n\n` +
|
`Apps not updated in >${threshold} days: <b>${apps.length}</b>\n\n` +
|
||||||
`<pre>${buildTable(headers, rows)}</pre>`
|
`<pre>${buildTable(headers, rows)}</pre>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Resolves per-group setting overrides to effective values, falling back to
|
||||||
|
// global config defaults when a group has no override (or stored a bad value).
|
||||||
|
|
||||||
|
export function resolveDaysWarning(group, config) {
|
||||||
|
const v = group?.settings?.numDaysWarningNotUpdated;
|
||||||
|
return Number.isFinite(v) && v > 0 ? v : config.numDaysWarningNotUpdated;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user