mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-10-11 03:13:50 +00:00
feat: /setappttl admin command for runtime cache TTL override
New admin-only command persists APP_CACHE_SECONDS override in admin doc (admin.appCacheSeconds), bounds [60, 86400], 0/default clears. Fallback to env when unset. app-cache-repository now takes a lazy memoized TTL getter — admin doc read at most once per request, only on cache write paths. app-builder wires (admin.getAppCacheSeconds() ?? config.appCacheSeconds). /settings now displays effective TTL with override status alongside the existing per-group settings. Also fix /setdayswarning + /setappttl arg parsing: gate reset on string '0' instead of parseInt === 0, so '/cmd 0.5', '/cmd 0xff', '/cmd 0abc' no longer silently trigger a reset.
This commit is contained in:
1 parent
1ed82578c0
commit
675f802746
7 files changed
+100
-11
No files matched your search
+7
-2
@@ -18,8 +18,13 @@ export function buildApp(env) {
|
||||
admin: createAdminRepository(handle),
|
||||
group: createGroupRepository(handle),
|
||||
};
|
||||
const appleCache = createAppCacheRepository(handle, 'apple', config.appCacheSeconds);
|
||||
const googleCache = createAppCacheRepository(handle, 'google', config.appCacheSeconds);
|
||||
// Resolves to admin override if set, else env default. Per-repo memoization
|
||||
// means each cache repo reads the admin doc at most once per request, and
|
||||
// only on the first cache write — read-only paths cost zero extra Redis ops.
|
||||
const ttlGetter = async () =>
|
||||
(await store.admin.getAppCacheSeconds()) ?? config.appCacheSeconds;
|
||||
const appleCache = createAppCacheRepository(handle, 'apple', ttlGetter);
|
||||
const googleCache = createAppCacheRepository(handle, 'google', ttlGetter);
|
||||
const appleScraper = createAppleScraper(config, appleCache);
|
||||
const googleScraper = createGoogleScraper(config, googleCache);
|
||||
const { sender, commands } = createBot(config, store, appleScraper, googleScraper);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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.
|
||||
// /settings — table of per-group setting overrides + their env defaults,
|
||||
// plus admin-global runtime overrides (cache TTL).
|
||||
export function createGetSettingsCommand(config, store) {
|
||||
return async (msg, sender, args) => {
|
||||
if (!(await authorizeGroup(msg.chat.id, store, sender))) return;
|
||||
@@ -12,14 +12,22 @@ export function createGetSettingsCommand(config, store) {
|
||||
}
|
||||
const group = await store.group.getGroup(msg.chat.id);
|
||||
const s = group?.settings ?? {};
|
||||
const adminTtl = await store.admin.getAppCacheSeconds();
|
||||
const rows = [
|
||||
[
|
||||
'numDaysWarningNotUpdated',
|
||||
formatValue(s.numDaysWarningNotUpdated),
|
||||
String(config.numDaysWarningNotUpdated),
|
||||
],
|
||||
[
|
||||
'appCacheSeconds (admin)',
|
||||
adminTtl === undefined ? '(unset)' : `${adminTtl}`,
|
||||
String(config.appCacheSeconds),
|
||||
],
|
||||
];
|
||||
const out = '<b>Group Settings</b>\n' + `<pre>${buildTable(['Setting', 'Value', 'Default'], rows)}</pre>`;
|
||||
const out =
|
||||
'<b>Group Settings</b>\n' +
|
||||
`<pre>${buildTable(['Setting', 'Value', 'Default'], rows)}</pre>`;
|
||||
await sender.sendMessage(msg.chat.id, out);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { createRawAppleAppCommand } from './raw-apple-app.js';
|
||||
import { createRawGoogleAppCommand } from './raw-google-app.js';
|
||||
import { createGetSettingsCommand } from './get-settings.js';
|
||||
import { createSetDaysWarningCommand } from './set-days-warning.js';
|
||||
import { createSetAppTtlCommand } from './set-app-ttl.js';
|
||||
|
||||
export const COMMAND_CATALOG = [
|
||||
{ name: 'info', description: 'Show this group ID', adminOnly: false, build: () => createInfoCommand() },
|
||||
@@ -39,6 +40,7 @@ export const COMMAND_CATALOG = [
|
||||
{ name: 'rawgoogleapp', description: 'Dump raw Google API JSON for an app', adminOnly: false, build: (c) => createRawGoogleAppCommand(c.store, c.googleScraper) },
|
||||
{ name: 'settings', description: "Show this group's settings", adminOnly: false, build: (c) => createGetSettingsCommand(c.config, c.store) },
|
||||
{ name: 'setdayswarning', description: 'Set warning threshold (days, 0 = default)', adminOnly: false, build: (c) => createSetDaysWarningCommand(c.config, c.store) },
|
||||
{ name: 'setappttl', description: '[admin] Set cache TTL (seconds, 0 = default)', adminOnly: true, build: (c) => createSetAppTtlCommand(c.config, c.store) },
|
||||
];
|
||||
|
||||
// Telegram menu projections.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { requireAdminUser } from './command-utils.js';
|
||||
|
||||
const MIN_TTL = 60;
|
||||
const MAX_TTL = 86400;
|
||||
|
||||
// /setappttl <n> — admin-only. Overrides upstream cache TTL (seconds).
|
||||
// `0` or `default` clears the override (falls back to APP_CACHE_SECONDS env).
|
||||
export function createSetAppTtlCommand(config, store) {
|
||||
return async (msg, sender, args) => {
|
||||
if (!(await requireAdminUser(msg.from.id, msg.chat.id, config, sender))) return;
|
||||
if (args.length !== 1) {
|
||||
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
|
||||
return;
|
||||
}
|
||||
const arg = args[0];
|
||||
|
||||
if (arg === 'default' || arg === '0') {
|
||||
await store.admin.setAppCacheSeconds(undefined);
|
||||
await sender.sendMessage(
|
||||
msg.chat.id,
|
||||
`Reset to default (${config.appCacheSeconds}s)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(arg, 10);
|
||||
if (
|
||||
!Number.isFinite(parsed) ||
|
||||
String(parsed) !== arg ||
|
||||
parsed < MIN_TTL ||
|
||||
parsed > MAX_TTL
|
||||
) {
|
||||
await sender.sendMessage(
|
||||
msg.chat.id,
|
||||
`Invalid arguments (allowed range: ${MIN_TTL}-${MAX_TTL}s)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await store.admin.setAppCacheSeconds(parsed);
|
||||
await sender.sendMessage(msg.chat.id, `App cache TTL set to ${parsed}s`);
|
||||
};
|
||||
}
|
||||
@@ -12,9 +12,8 @@ export function createSetDaysWarningCommand(config, store) {
|
||||
return;
|
||||
}
|
||||
const arg = args[0];
|
||||
const parsed = Number.parseInt(arg, 10);
|
||||
|
||||
if (arg === 'default' || parsed === 0) {
|
||||
if (arg === 'default' || arg === '0') {
|
||||
await store.group.setSetting(msg.chat.id, 'numDaysWarningNotUpdated', undefined);
|
||||
await sender.sendMessage(
|
||||
msg.chat.id,
|
||||
@@ -23,6 +22,7 @@ export function createSetDaysWarningCommand(config, store) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(arg, 10);
|
||||
if (!Number.isFinite(parsed) || String(parsed) !== arg || parsed < 1 || parsed > MAX_DAYS) {
|
||||
await sender.sendMessage(msg.chat.id, 'Invalid arguments');
|
||||
return;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { getJson, putJson } from './upstash.js';
|
||||
|
||||
const ADMIN_KEY = 'admin';
|
||||
|
||||
// Upstash-backed admin singleton. Holds the authorized chat ID allowlist.
|
||||
// Upstash-backed admin singleton. Holds the authorized chat ID allowlist
|
||||
// plus optional admin-global runtime settings (e.g. cache TTL override).
|
||||
export function createAdminRepository(handle) {
|
||||
async function load() {
|
||||
return (await getJson(handle, ADMIN_KEY)) ?? { groups: [] };
|
||||
@@ -38,5 +39,25 @@ export function createAdminRepository(handle) {
|
||||
return (await load()).groups;
|
||||
}
|
||||
|
||||
return { addGroup, removeGroup, hasGroup, getAllGroups };
|
||||
// undefined when no override set — caller falls back to env default.
|
||||
async function getAppCacheSeconds() {
|
||||
return (await load()).appCacheSeconds;
|
||||
}
|
||||
|
||||
// Pass `undefined` to clear the override.
|
||||
async function setAppCacheSeconds(seconds) {
|
||||
const admin = await load();
|
||||
if (seconds === undefined) delete admin.appCacheSeconds;
|
||||
else admin.appCacheSeconds = seconds;
|
||||
await save(admin);
|
||||
}
|
||||
|
||||
return {
|
||||
addGroup,
|
||||
removeGroup,
|
||||
hasGroup,
|
||||
getAllGroups,
|
||||
getAppCacheSeconds,
|
||||
setAppCacheSeconds,
|
||||
};
|
||||
}
|
||||
@@ -3,11 +3,21 @@ import { getJson, putJson } from './upstash.js';
|
||||
// Per-store cache for upstream app responses. TTL via Redis EX —
|
||||
// expired keys are deleted, so a null read is the cache miss.
|
||||
// `prefix` is the logical key namespace ('apple' or 'google').
|
||||
export function createAppCacheRepository(handle, prefix, appCacheSeconds) {
|
||||
// `getTtl` is an async function returning the current TTL in seconds; the
|
||||
// resolved value is memoized for the lifetime of this repo instance, so the
|
||||
// admin override doc is read at most once per request even with multiple
|
||||
// cache writes.
|
||||
export function createAppCacheRepository(handle, prefix, getTtl) {
|
||||
function key(appId) {
|
||||
return `${prefix}:${appId}`;
|
||||
}
|
||||
|
||||
let cachedTtl;
|
||||
async function ttl() {
|
||||
if (cachedTtl === undefined) cachedTtl = await getTtl();
|
||||
return cachedTtl;
|
||||
}
|
||||
|
||||
return {
|
||||
async getCached(appId) {
|
||||
return getJson(handle, key(appId));
|
||||
@@ -17,7 +27,7 @@ export function createAppCacheRepository(handle, prefix, appCacheSeconds) {
|
||||
handle,
|
||||
key(appId),
|
||||
{ app: response, millis: Date.now() },
|
||||
{ expirationTtl: appCacheSeconds },
|
||||
{ expirationTtl: await ttl() },
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user