Spaces:
Running
Running
Download src/lib/monitor.ts from iq7se2/tg-tracker: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/spaces/iq7se2/tg-tracker/resolve/main/src/lib/monitor.ts
- Command line
-
hf download hf://spaces/iq7se2/tg-tracker/src/lib/monitor.ts
-
curl -L -o monitor.ts https://huggingface.co/spaces/iq7se2/tg-tracker/resolve/main/src/lib/monitor.ts
10.5 kB
| /** | |
| * Background monitor service. | |
| * | |
| * Each tick: fetches auctions once (shared), then for each ENABLED page checks | |
| * if the page should notify for that auction. Per-page notified tracking via | |
| * the PageNotified table ensures each page notifies independently — a new bid | |
| * triggers a new notification for every enabled page that wants it. | |
| * | |
| * The monitor interval runs if ANY page is enabled. Pages are started/stopped | |
| * individually via the dashboard (start clears the page's notified records for | |
| * a fresh start). | |
| */ | |
| import { db } from "@/lib/db"; | |
| import { getSettings, AppSettings, formatMessage } from "@/lib/settings"; | |
| import { listPages, pageShouldReceive, PageConfig } from "@/lib/pages"; | |
| import { fetchFragmentAuctions, fetchGetgemsAuctions, fetchMockAuctions, RawAuction } from "@/lib/fetchers"; | |
| import { compilePattern, matchesPattern } from "@/lib/pattern-engine"; | |
| import { formatCountdown } from "@/lib/image-gen"; | |
| export interface MonitorStatus { | |
| running: boolean; | |
| enabled: boolean; // any page enabled? | |
| lastRunAt: string | null; | |
| nextRunAt: string | null; | |
| lastRunDurationMs: number | null; | |
| lastError: string | null; | |
| lastRunStats: { fetched: number; matched: number; notified: number; errors: number } | null; | |
| intervalSeconds: number; | |
| enabledPageCount: number; | |
| } | |
| interface MonitorState { | |
| timer: NodeJS.Timeout | null; | |
| running: boolean; | |
| lastRunAt: Date | null; | |
| lastRunDurationMs: number | null; | |
| lastError: string | null; | |
| lastRunStats: { fetched: number; matched: number; notified: number; errors: number } | null; | |
| intervalSeconds: number; | |
| } | |
| const globalForMonitor = globalThis as unknown as { __fragmentMonitor?: MonitorState }; | |
| function getState(): MonitorState { | |
| if (!globalForMonitor.__fragmentMonitor) { | |
| globalForMonitor.__fragmentMonitor = { | |
| timer: null, | |
| running: false, | |
| lastRunAt: null, | |
| lastRunDurationMs: null, | |
| lastError: null, | |
| lastRunStats: null, | |
| intervalSeconds: 60, | |
| }; | |
| } | |
| return globalForMonitor.__fragmentMonitor; | |
| } | |
| export async function getMonitorStatus(): Promise<MonitorStatus> { | |
| const s = getState(); | |
| const pages = await listPages(); | |
| const enabledCount = pages.filter((p) => p.enabled).length; | |
| const nextRunAt = s.timer && s.lastRunAt ? new Date(s.lastRunAt.getTime() + s.intervalSeconds * 1000) : null; | |
| return { | |
| running: s.running, | |
| enabled: enabledCount > 0, | |
| lastRunAt: s.lastRunAt ? s.lastRunAt.toISOString() : null, | |
| nextRunAt: nextRunAt ? nextRunAt.toISOString() : null, | |
| lastRunDurationMs: s.lastRunDurationMs, | |
| lastError: s.lastError, | |
| lastRunStats: s.lastRunStats, | |
| intervalSeconds: s.intervalSeconds, | |
| enabledPageCount: enabledCount, | |
| }; | |
| } | |
| export async function runMonitorCycle(): Promise<{ | |
| fetched: number; | |
| matched: number; | |
| notified: number; | |
| errors: number; | |
| error?: string; | |
| }> { | |
| const state = getState(); | |
| if (state.running) { | |
| return { fetched: 0, matched: 0, notified: 0, errors: 0, error: "already running" }; | |
| } | |
| state.running = true; | |
| const startedAt = new Date(); | |
| let fetched = 0; | |
| let matched = 0; | |
| let notified = 0; | |
| let errors = 0; | |
| try { | |
| const settings = await getSettings(); | |
| const pages = await listPages(); | |
| const enabledPages = pages.filter((p) => p.enabled); | |
| if (enabledPages.length === 0) { | |
| state.lastRunAt = startedAt; | |
| state.lastRunDurationMs = Date.now() - startedAt.getTime(); | |
| state.lastError = null; | |
| state.lastRunStats = { fetched: 0, matched: 0, notified: 0, errors: 0 }; | |
| return { fetched: 0, matched: 0, notified: 0, errors: 0 }; | |
| } | |
| // Fetch auctions from enabled sources. | |
| const sources: Promise<RawAuction[]>[] = []; | |
| if (settings.sourceFragment) sources.push(fetchFragmentAuctions(settings.fetchSaleType).catch(() => [] as RawAuction[])); | |
| if (settings.sourceGetgems) sources.push(fetchGetgemsAuctions(100).catch(() => [] as RawAuction[])); | |
| if (settings.sourceMock) sources.push(fetchMockAuctions().catch(() => [] as RawAuction[])); | |
| const results = await Promise.all(sources); | |
| let allAuctions: RawAuction[] = results.flat(); | |
| if (settings.maxBidTon > 0) { | |
| allAuctions = allAuctions.filter((a) => a.bidAmount <= settings.maxBidTon); | |
| } | |
| fetched = allAuctions.length; | |
| // Load ALL patterns. | |
| const allPatterns = await db.pattern.findMany(); | |
| const compiledPatterns = allPatterns | |
| .map((p) => { | |
| try { | |
| return { pattern: p, compiled: compilePattern(p.expression) }; | |
| } catch { | |
| return null; | |
| } | |
| }) | |
| .filter((x): x is { pattern: (typeof allPatterns)[number]; compiled: ReturnType<typeof compilePattern> } => x !== null); | |
| const seenThisCycle = new Set<string>(); | |
| // Per-page notified counts. | |
| const pageNotifiedCount: Record<string, number> = {}; | |
| for (const p of enabledPages) pageNotifiedCount[p.id] = 0; | |
| for (const auction of allAuctions) { | |
| const key = `${auction.source}:${auction.saleType}:${auction.username}`; | |
| if (seenThisCycle.has(key)) continue; | |
| seenThisCycle.add(key); | |
| // Find first matching pattern (global — patterns shared across pages). | |
| let matchedPattern: { id: string; expression: string } | null = null; | |
| for (const entry of compiledPatterns) { | |
| if (matchesPattern(auction.username, entry.compiled)) { | |
| matchedPattern = { id: entry.pattern.id, expression: entry.pattern.expression }; | |
| break; | |
| } | |
| } | |
| const isMatch = matchedPattern !== null; | |
| if (isMatch) matched++; | |
| // For each enabled page, check if it should notify. | |
| for (const page of enabledPages) { | |
| if (!pageShouldReceive(page, auction, matchedPattern?.id ?? null, isMatch)) continue; | |
| // Check per-page notified record. | |
| const existing = await db.pageNotified.findUnique({ | |
| where: { | |
| pageId_username_source_saleType: { | |
| pageId: page.id, | |
| username: auction.username, | |
| source: auction.source, | |
| saleType: auction.saleType, | |
| }, | |
| }, | |
| }); | |
| const bidIncreased = !existing || auction.bidAmount > existing.bidAmount; | |
| if (!bidIncreased) continue; // already notified this bid to this page | |
| // Send notification. | |
| const res = await notifyPage(page, auction, matchedPattern, settings); | |
| notified += res.notified; | |
| errors += res.errors; | |
| if (res.notified > 0) { | |
| pageNotifiedCount[page.id] = (pageNotifiedCount[page.id] || 0) + 1; | |
| // Upsert the notified record. | |
| await db.pageNotified.upsert({ | |
| where: { | |
| pageId_username_source_saleType: { | |
| pageId: page.id, | |
| username: auction.username, | |
| source: auction.source, | |
| saleType: auction.saleType, | |
| }, | |
| }, | |
| create: { | |
| pageId: page.id, | |
| username: auction.username, | |
| source: auction.source, | |
| saleType: auction.saleType, | |
| bidAmount: auction.bidAmount, | |
| }, | |
| update: { bidAmount: auction.bidAmount, notifiedAt: new Date() }, | |
| }); | |
| } | |
| } | |
| } | |
| // Update page stats. | |
| for (const page of enabledPages) { | |
| await db.monitoringPage.update({ | |
| where: { id: page.id }, | |
| data: { lastRunAt: new Date(), lastNotified: pageNotifiedCount[page.id] || 0 }, | |
| }); | |
| } | |
| state.lastRunAt = startedAt; | |
| state.lastRunDurationMs = Date.now() - startedAt.getTime(); | |
| state.lastError = null; | |
| state.lastRunStats = { fetched, matched, notified, errors }; | |
| return { fetched, matched, notified, errors }; | |
| } catch (e: any) { | |
| state.lastError = e?.message ?? String(e); | |
| state.lastRunAt = startedAt; | |
| state.lastRunDurationMs = Date.now() - startedAt.getTime(); | |
| state.lastRunStats = { fetched, matched, notified, errors }; | |
| return { fetched, matched, notified, errors, error: state.lastError }; | |
| } finally { | |
| state.running = false; | |
| } | |
| } | |
| async function notifyPage( | |
| page: PageConfig, | |
| auction: RawAuction, | |
| matchedPattern: { id: string; expression: string } | null, | |
| settings: AppSettings | |
| ): Promise<{ notified: number; errors: number }> { | |
| const template = page.messageTemplate || settings.messageTemplate; | |
| const timeLeft = formatCountdown(auction.endTime); | |
| const text = formatMessage(template, { | |
| username: auction.username, | |
| bid: auction.bidAmount, | |
| source: auction.source, | |
| pattern: matchedPattern?.expression, | |
| url: auction.url, | |
| timeLeft, | |
| saleType: auction.saleType, | |
| }); | |
| // Web-only mode: log the notification to the database (no Telegram send) | |
| // The notification text is stored so it can be viewed in the Notifications tab. | |
| try { | |
| await db.notification.create({ | |
| data: { | |
| username: auction.username, | |
| pattern: matchedPattern?.expression ?? null, | |
| patternId: matchedPattern?.id ?? null, | |
| bidAmount: auction.bidAmount, | |
| source: auction.source, | |
| saleType: auction.saleType, | |
| recipient: page.recipient, | |
| pageId: page.id, | |
| pageName: page.name, | |
| status: "success", | |
| message: text, | |
| hasImage: false, | |
| error: null, | |
| }, | |
| }); | |
| return { notified: 1, errors: 0 }; | |
| } catch (e: any) { | |
| console.error(`[monitor] notification log failed: ${e?.message}`); | |
| return { notified: 0, errors: 1 }; | |
| } | |
| } | |
| /** | |
| * Start the monitor interval (runs if called — the dashboard starts it when | |
| * any page is enabled). | |
| */ | |
| export async function startMonitor(): Promise<void> { | |
| const state = getState(); | |
| if (state.timer) return; | |
| const settings = await getSettings(); | |
| const interval = settings.realtimeMode ? Math.min(settings.intervalSeconds, 20) : settings.intervalSeconds; | |
| state.intervalSeconds = interval; | |
| console.log(`[monitor] started, interval=${interval}s (realtime=${settings.realtimeMode})`); | |
| state.timer = setInterval(() => { | |
| runMonitorCycle().catch((e) => console.error("[monitor] cycle error:", e)); | |
| }, interval * 1000); | |
| } | |
| export function stopMonitor(): void { | |
| const state = getState(); | |
| if (state.timer) { | |
| clearInterval(state.timer); | |
| state.timer = null; | |
| } | |
| } | |
| export async function restartMonitor(): Promise<void> { | |
| stopMonitor(); | |
| const pages = await listPages(); | |
| if (pages.some((p) => p.enabled)) { | |
| await startMonitor(); | |
| } | |
| } | |