/* ===================================================================== lib/live-status.js — Automatische TikTok-Live-Erkennung für den grünen Punkt auf der Startseite. 1:1 portiert aus dem Live-Status-Teil von cloudflare-worker/src/worker.js, D1 → better-sqlite3. Ein Cron-Auftrag (node-cron in index.js, ersetzt Cloudflares `scheduled()`) fragt jede Minute bei TikTok nach, ob @dogfather0804 gerade sendet, und schreibt das Ergebnis in die Tabelle app_settings. Zwei Schlüssel: live_auto – letzter ERFOLGREICH ermittelter Stand (wird bei einem fehlgeschlagenen Abruf bewusst NICHT überschrieben, damit ein TikTok-Aussetzer den Punkt nicht fälschlich auf offline zieht) live_override – manueller Vorrang aus dem Postfach, mit Ablaufzeit. Solange er gilt, schlägt er die Automatik. ===================================================================== */ import { db } from "../db.js"; import { json } from "./http.js"; import { logAction } from "./audit.js"; import { checkTikTokLive } from "./tiktok-live.js"; const DEFAULT_OVERRIDE_MINUTES = 180; function readSetting(key) { const row = db.prepare(`SELECT value FROM app_settings WHERE key = ?`).get(key); if (!row || !row.value) return null; try { return JSON.parse(row.value); } catch { return null; } } function writeSetting(key, value) { db.prepare( `INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at` ).run(key, JSON.stringify(value), new Date().toISOString()); } /** Ermittelt den Stand, der nach außen gilt (Vorrang: manuell vor Automatik). */ function resolveLiveStatus() { const auto = readSetting("live_auto") || {}; const override = readSetting("live_override"); const overrideActive = override && override.until && new Date(override.until).getTime() > Date.now(); if (overrideActive) { return { live: !!override.live, source: "manuell", updatedAt: override.setAt || null, overrideUntil: override.until, autoLive: auto.live ?? null, autoCheckedAt: auto.checkedAt || null, autoHealthy: auto.ok !== false, }; } return { live: !!auto.live, source: "automatisch", updatedAt: auto.changedAt || auto.checkedAt || null, title: auto.title || null, autoLive: auto.live ?? null, autoCheckedAt: auto.checkedAt || null, autoHealthy: auto.ok !== false, }; } export async function handleLiveStatusGet(req, res) { return json(res, { ok: true, ...resolveLiveStatus() }); } /** * Manueller Eingriff aus dem Postfach. * { live: true|false, minutes?: number } → Vorrang setzen * { auto: true } → Vorrang aufheben, Automatik übernimmt */ export async function handleLiveStatusSet(req, res, session) { if (!session?.isOwner) return json(res, { ok: false, error: "Nur Dogi darf den Live-Status ändern." }, 403); const body = req.body || {}; if (body.auto === true) { db.prepare(`DELETE FROM app_settings WHERE key = 'live_override'`).run(); logAction("owner", "live.override_cleared", null, null); return json(res, { ok: true, ...resolveLiveStatus() }); } const minutes = Number.isFinite(body.minutes) ? Math.min(Math.max(body.minutes, 5), 1440) : DEFAULT_OVERRIDE_MINUTES; const now = new Date(); writeSetting("live_override", { live: !!body.live, setAt: now.toISOString(), until: new Date(now.getTime() + minutes * 60_000).toISOString(), }); logAction("owner", "live.override_set", null, { live: !!body.live, minutes }); return json(res, { ok: true, ...resolveLiveStatus() }); } /** Wird vom Cron-Auftrag aufgerufen (index.js) — fragt TikTok ab und schreibt das Ergebnis. */ export async function refreshLiveFromTikTok() { const username = process.env.TIKTOK_USERNAME || "dogfather0804"; const result = await checkTikTokLive(username); const now = new Date().toISOString(); if (!result.ok) { // Fehlgeschlagen: letzten guten Stand behalten, nur den Fehler vermerken. const previous = readSetting("live_auto") || {}; writeSetting("live_auto", { ...previous, ok: false, lastError: result.reason, checkedAt: now }); return { ok: false, reason: result.reason }; } const previous = readSetting("live_auto") || {}; const changed = previous.live !== result.live; writeSetting("live_auto", { ok: true, live: result.live, title: result.title, roomId: result.roomId, rawStatus: result.rawStatus, checkedAt: now, changedAt: changed ? now : previous.changedAt || now, lastError: null, }); if (changed) { logAction("system", result.live ? "live.started" : "live.ended", null, { title: result.title }); } return { ok: true, live: result.live, changed }; }