mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-10-11 03:13:50 +00:00
refactor: drop dead repo + telegram-api exports, inline createStore
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.
This commit is contained in:
1 parent
32b14217cc
commit
9bcdaa9130
6 files changed
+43
-117
No files matched your search
+10
-2
@@ -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);
|
||||
|
||||
+4
-17
@@ -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 = {}) =>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user