Postfach/Team-Verwaltung/Supporter-Abo als Node.js/Express-Server portiert
Vollständiger 1:1-Port des cloudflare-worker/ (dogfather-universe-postfach) auf server-internal/: Bewerbungs-Postfach mit Rollen/Rechten, Team- Zugangsverwaltung (verschlüsselte Owner-only-Codes), DogiCrew-Supporter-Abo (PayPal Subscriptions, Google-Login, Resend-E-Mail, 30-Monats-Prämienzyklus) und automatische TikTok-Live-Erkennung. D1 -> better-sqlite3, Cloudflare- Cron -> node-cron. Läuft ohne gesetzte Secrets weiter (PayPal/Google/Resend/ Ticketanizer/Discord bleiben "nicht konfiguriert" statt kaputt), bis die echten Zugangsdaten eingetragen werden.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/* =====================================================================
|
||||
lib/tiktok-live.js — Automatische Live-Erkennung für TikTok. 1:1 portiert
|
||||
aus cloudflare-worker/src/lib/tiktok-live.js (das "cf"-Fetch-Sonderfeld
|
||||
von Cloudflare Workers entfällt, hat in Node keine Entsprechung/Wirkung).
|
||||
===================================================================== */
|
||||
|
||||
const LIVE_STATUS_RUNNING = 2;
|
||||
|
||||
const BROWSER_HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
|
||||
"Cache-Control": "no-cache",
|
||||
Pragma: "no-cache",
|
||||
};
|
||||
|
||||
function extractScriptJson(html, id) {
|
||||
const marker = `<script id="${id}"`;
|
||||
const start = html.indexOf(marker);
|
||||
if (start === -1) return null;
|
||||
const open = html.indexOf(">", start);
|
||||
if (open === -1) return null;
|
||||
const end = html.indexOf("</script>", open);
|
||||
if (end === -1) return null;
|
||||
try {
|
||||
return JSON.parse(html.slice(open + 1, end));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkTikTokLive(username) {
|
||||
const handle = String(username || "").replace(/^@/, "").trim();
|
||||
if (!handle) return { ok: false, reason: "kein-benutzername" };
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`https://www.tiktok.com/@${encodeURIComponent(handle)}/live`, {
|
||||
headers: BROWSER_HEADERS,
|
||||
redirect: "follow",
|
||||
});
|
||||
} catch (err) {
|
||||
return { ok: false, reason: `netzwerkfehler: ${String(err).slice(0, 120)}` };
|
||||
}
|
||||
|
||||
if (!res.ok) return { ok: false, reason: `http-${res.status}` };
|
||||
|
||||
const html = await res.text();
|
||||
if (html.length < 5000) return { ok: false, reason: "seite-zu-klein-vermutlich-blockiert" };
|
||||
|
||||
const sigi = extractScriptJson(html, "SIGI_STATE");
|
||||
let info = sigi?.LiveRoom?.liveRoomUserInfo;
|
||||
|
||||
if (!info) {
|
||||
const universal = extractScriptJson(html, "__UNIVERSAL_DATA_FOR_REHYDRATION__");
|
||||
info = universal?.__DEFAULT_SCOPE__?.["webapp.live-detail"]?.liveRoomUserInfo;
|
||||
}
|
||||
if (!info) return { ok: false, reason: "struktur-nicht-gefunden" };
|
||||
|
||||
const status = info.liveRoom?.status;
|
||||
if (typeof status !== "number") return { ok: false, reason: "kein-status-feld" };
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
live: status === LIVE_STATUS_RUNNING,
|
||||
rawStatus: status,
|
||||
title: info.liveRoom?.title || null,
|
||||
roomId: info.user?.roomId ? String(info.user.roomId) : null,
|
||||
followers: info.stats?.followerCount ?? null,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user