From 9bcdaa91303fa20b589313af95c101197ed7e9e7 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 9 May 2026 22:32:12 +0700 Subject: [PATCH] refactor: drop dead repo + telegram-api exports, inline createStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete repository/store.js (one-line aggregator) — wiring now inline in app-builder.js. Drop unused exports: scan + UpstashUnavailable from upstash.js, getMe + TelegramApiError class from telegram-api.js, init/getAdmin/save from admin-repository.js, exists/saveGroup from group-repository.js. Generic Error replaces the named error classes. --- src/app-builder.js | 12 +++++-- src/bot/telegram-api.js | 21 +++--------- src/repository/admin-repository.js | 22 ++++--------- src/repository/group-repository.js | 38 +++++++++------------- src/repository/store.js | 16 ---------- src/repository/upstash.js | 51 ++++-------------------------- 6 files changed, 43 insertions(+), 117 deletions(-) delete mode 100644 src/repository/store.js diff --git a/src/app-builder.js b/src/app-builder.js index ebf5676..c078e42 100644 --- a/src/app-builder.js +++ b/src/app-builder.js @@ -4,7 +4,10 @@ import { loadConfig } from './config.js'; import { createUpstashClient } from './repository/upstash.js'; -import { createStore } from './repository/store.js'; +import { createAdminRepository } from './repository/admin-repository.js'; +import { createGroupRepository } from './repository/group-repository.js'; +import { createAppleAppRepository } from './repository/apple-app-repository.js'; +import { createGoogleAppRepository } from './repository/google-app-repository.js'; import { createAppleScraper } from './api/apple-scraper.js'; import { createGoogleScraper } from './api/google-scraper.js'; import { createBot } from './bot/bot.js'; @@ -12,7 +15,12 @@ import { createBot } from './bot/bot.js'; export function buildApp(env) { const config = loadConfig(env); const handle = createUpstashClient(env); - const store = createStore(handle, config.appCacheSeconds); + const store = { + admin: createAdminRepository(handle), + group: createGroupRepository(handle), + appleApp: createAppleAppRepository(handle, config.appCacheSeconds), + googleApp: createGoogleAppRepository(handle, config.appCacheSeconds), + }; const appleScraper = createAppleScraper(config, store); const googleScraper = createGoogleScraper(config, store); const { sender, commands } = createBot(config, store, appleScraper, googleScraper); diff --git a/src/bot/telegram-api.js b/src/bot/telegram-api.js index 6389958..01028ec 100644 --- a/src/bot/telegram-api.js +++ b/src/bot/telegram-api.js @@ -1,18 +1,7 @@ -// Raw fetch wrapper for the Telegram Bot API. Replaces node-telegram-bot-api -// (which uses Node-only request/streams and bloats the Worker bundle). +// Raw fetch wrapper for the Telegram Bot API. const TELEGRAM_BASE = 'https://api.telegram.org'; -export class TelegramApiError extends Error { - constructor(method, status, body) { - super(`telegram ${method} failed: ${status} ${body}`); - this.name = 'TelegramApiError'; - this.method = method; - this.status = status; - this.body = body; - } -} - export function createTelegramApi(token) { const base = `${TELEGRAM_BASE}/bot${token}`; @@ -23,12 +12,11 @@ export function createTelegramApi(token) { body: JSON.stringify(payload), }); const text = await res.text(); - if (!res.ok) throw new TelegramApiError(method, res.status, text); + if (!res.ok) throw new Error(`telegram ${method} failed: ${res.status} ${text}`); return JSON.parse(text); } - // multipart/form-data — for sendDocument. WHATWG FormData/Blob is native to - // Workers; no `form-data` npm dep needed. + // multipart/form-data — for sendDocument. WHATWG FormData/Blob is native. async function callMultipart(method, fields, file) { const form = new FormData(); for (const [k, v] of Object.entries(fields)) form.set(k, String(v)); @@ -41,12 +29,11 @@ export function createTelegramApi(token) { } const res = await fetch(`${base}/${method}`, { method: 'POST', body: form }); const text = await res.text(); - if (!res.ok) throw new TelegramApiError(method, res.status, text); + if (!res.ok) throw new Error(`telegram ${method} failed: ${res.status} ${text}`); return JSON.parse(text); } return { - getMe: () => callJson('getMe', {}), sendMessage: (chatId, text, opts = {}) => callJson('sendMessage', { chat_id: chatId, text, ...opts }), sendDocument: (chatId, filename, body, opts = {}) => diff --git a/src/repository/admin-repository.js b/src/repository/admin-repository.js index 804fc48..bde7109 100644 --- a/src/repository/admin-repository.js +++ b/src/repository/admin-repository.js @@ -4,15 +4,8 @@ const ADMIN_KEY = 'admin'; // Upstash-backed admin singleton. Holds the authorized chat ID allowlist. export function createAdminRepository(handle) { - async function init() { - const existing = await getJson(handle, ADMIN_KEY); - if (existing) return; - await save({ _id: ADMIN_KEY, groups: [] }); - } - - async function getAdmin() { - const doc = await getJson(handle, ADMIN_KEY); - return doc ?? { _id: ADMIN_KEY, groups: [] }; + async function load() { + return (await getJson(handle, ADMIN_KEY)) ?? { groups: [] }; } async function save(admin) { @@ -20,7 +13,7 @@ export function createAdminRepository(handle) { } async function addGroup(groupId) { - const admin = await getAdmin(); + const admin = await load(); if (admin.groups.includes(groupId)) return false; admin.groups.push(groupId); await save(admin); @@ -28,7 +21,7 @@ export function createAdminRepository(handle) { } async function removeGroup(groupId) { - const admin = await getAdmin(); + const admin = await load(); const i = admin.groups.indexOf(groupId); if (i < 0) return false; admin.groups.splice(i, 1); @@ -37,14 +30,13 @@ export function createAdminRepository(handle) { } async function hasGroup(groupId) { - const admin = await getAdmin(); + const admin = await load(); return admin.groups.includes(groupId); } async function getAllGroups() { - const admin = await getAdmin(); - return admin.groups; + return (await load()).groups; } - return { init, getAdmin, save, addGroup, removeGroup, hasGroup, getAllGroups }; + return { addGroup, removeGroup, hasGroup, getAllGroups }; } diff --git a/src/repository/group-repository.js b/src/repository/group-repository.js index 097a6f3..571dbd7 100644 --- a/src/repository/group-repository.js +++ b/src/repository/group-repository.js @@ -3,36 +3,37 @@ import { del, getJson, putJson } from './upstash.js'; // Upstash-backed per-group state. Logical key shape: `group:{chatId}`. export function createGroupRepository(handle) { function key(groupId) { - return `group:${String(groupId)}`; + return `group:${groupId}`; } - function newGroup(groupId) { - return { _id: String(groupId), appleApps: [], googleApps: [] }; - } - - async function exists(groupId) { - const doc = await getJson(handle, key(groupId)); - return doc !== null; + function emptyGroup() { + return { appleApps: [], googleApps: [] }; } async function getGroup(groupId) { - const doc = await getJson(handle, key(groupId)); - return doc ?? newGroup(groupId); + return (await getJson(handle, key(groupId))) ?? emptyGroup(); } - async function saveGroup(group) { - await putJson(handle, key(group._id), group); + async function save(groupId, group) { + await putJson(handle, key(groupId), group); } async function initGroup(groupId) { - if (await exists(groupId)) return; - await saveGroup(newGroup(groupId)); + if (await getJson(handle, key(groupId))) return; + await save(groupId, emptyGroup()); } async function deleteGroup(groupId) { await del(handle, key(groupId)); } + async function mutateAndSave(groupId, mutator) { + const group = await getGroup(groupId); + if (!mutator(group)) return false; + await save(groupId, group); + return true; + } + function addApp(list, appId, country) { if (list.some((a) => a.appId === appId)) return false; list.push({ appId, country }); @@ -46,17 +47,8 @@ export function createGroupRepository(handle) { return true; } - async function mutateAndSave(groupId, mutator) { - const group = await getGroup(groupId); - if (!mutator(group)) return false; - await saveGroup(group); - return true; - } - return { - exists, getGroup, - saveGroup, initGroup, deleteGroup, addAppleApp: (groupId, appId, country) => diff --git a/src/repository/store.js b/src/repository/store.js deleted file mode 100644 index ef2847b..0000000 --- a/src/repository/store.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createAdminRepository } from './admin-repository.js'; -import { createGroupRepository } from './group-repository.js'; -import { createAppleAppRepository } from './apple-app-repository.js'; -import { createGoogleAppRepository } from './google-app-repository.js'; - -// Single binding point for all repositories. Threads the Upstash handle -// (client + key prefix) once so command handlers don't need to know about -// process.env or the Redis client construction. -export function createStore(handle, appCacheSeconds) { - return { - admin: createAdminRepository(handle), - group: createGroupRepository(handle), - appleApp: createAppleAppRepository(handle, appCacheSeconds), - googleApp: createGoogleAppRepository(handle, appCacheSeconds), - }; -} diff --git a/src/repository/upstash.js b/src/repository/upstash.js index f4935a5..65958f5 100644 --- a/src/repository/upstash.js +++ b/src/repository/upstash.js @@ -8,38 +8,23 @@ // // Multi-tenancy: every physical Redis key carries a configurable prefix // (env.KEY_PREFIX, default 'store-scraper-bot:') so this bot can safely share -// an Upstash database with other Vercel projects without collision. Repository -// callers pass logical keys; the adapter applies the prefix transparently. +// an Upstash database with other Vercel projects without collision. // -// 60s minimum TTL clamp is preserved from the KV days for parity safety, -// even though Redis would accept lower values. +// 60s minimum TTL clamp is preserved from the KV days for parity safety. import { Redis } from '@upstash/redis'; const MIN_TTL_SECONDS = 60; const DEFAULT_KEY_PREFIX = 'store-scraper-bot:'; -export class UpstashUnavailable extends Error { - constructor(missing) { - super(`Upstash env var missing: ${missing}`); - this.name = 'UpstashUnavailable'; - } -} - -// Build a handle bundling the Redis client and the key prefix together. -// The handle is what callers pass into getJson/putJson/del/scan — it stays -// opaque so repositories never need to know about prefixing themselves. -// // Accepts both env var naming conventions: -// - UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN (vanilla Upstash signup) -// - KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Marketplace integration) -// so the operator doesn't have to duplicate vars when the bot shares an -// Upstash DB provisioned via Vercel. +// UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN (vanilla Upstash signup) +// KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Marketplace integration) export function createUpstashClient(env) { const url = env?.UPSTASH_REDIS_REST_URL ?? env?.KV_REST_API_URL; const token = env?.UPSTASH_REDIS_REST_TOKEN ?? env?.KV_REST_API_TOKEN; - if (!url) throw new UpstashUnavailable('UPSTASH_REDIS_REST_URL or KV_REST_API_URL'); - if (!token) throw new UpstashUnavailable('UPSTASH_REDIS_REST_TOKEN or KV_REST_API_TOKEN'); + if (!url) throw new Error('UPSTASH_REDIS_REST_URL or KV_REST_API_URL is required'); + if (!token) throw new Error('UPSTASH_REDIS_REST_TOKEN or KV_REST_API_TOKEN is required'); const client = new Redis({ url, token }); const prefix = env.KEY_PREFIX ?? DEFAULT_KEY_PREFIX; return { client, prefix }; @@ -49,14 +34,10 @@ function physicalKey(handle, key) { return `${handle.prefix}${key}`; } -// Upstash auto-deserializes values that look like JSON. We always store via -// JSON.stringify, so reads can return the parsed object directly. Returns null -// on missing key, matching the prior KV semantics. export async function getJson(handle, key) { const value = await handle.client.get(physicalKey(handle, key)); if (value == null) return null; - // Some SDK versions return strings, others return parsed objects depending - // on content. Normalize: if string, parse; if object, pass through. + // Some SDK versions return strings, others return parsed objects. return typeof value === 'string' ? JSON.parse(value) : value; } @@ -70,21 +51,3 @@ export async function putJson(handle, key, value, opts = {}) { export async function del(handle, key) { await handle.client.del(physicalKey(handle, key)); } - -// Suffix-based scan. Caller passes a logical match like 'group:*'; adapter -// prepends the key prefix so only this bot's keys are returned. -// Returns the list of *logical* keys (prefix stripped) so callers stay -// prefix-unaware. -export async function scan(handle, matchSuffix) { - const match = `${handle.prefix}${matchSuffix}`; - const out = []; - let cursor = '0'; - do { - const [next, batch] = await handle.client.scan(cursor, { match, count: 100 }); - cursor = next; - for (const physical of batch) { - out.push(physical.startsWith(handle.prefix) ? physical.slice(handle.prefix.length) : physical); - } - } while (cursor !== '0'); - return out; -}