import { adminApproveSubmission } from "../../../../db/domains"; export default defineEventHandler(async (event) => { const config = useRuntimeConfig(); const adminSecret = getHeader(event, "x-admin-secret"); if (adminSecret !== config.adminSecret) { throw createError({ statusCode: 401, message: "Unauthorized" }); } const id = getRouterParam(event, "id"); if (!id) throw createError({ statusCode: 400, message: "ID fehlt" }); const body = await readBody(event).catch(() => ({})); const result = await adminApproveSubmission(id, body?.note); // Lyra-Post über die neu genehmigte Domain (fire & forget) const domain = (result as any)?.domain ?? null; const domainType: string = (result as any)?.type ?? "web"; const submitterUserId = (result as any)?.userId ?? null; const lyraBotUserId = config.lyraBotUserId; console.log( `[approve] domain=${domain}, lyraBotUserId=${lyraBotUserId}, hasGroq=${!!config.groqApiKey}`, ); if (domain && lyraBotUserId && config.groqApiKey) { // db + submitterName VOR der IIFE holen (usePrisma braucht Event-Kontext) const db = usePrisma(); let rawName: string | null = null; if (submitterUserId) { try { // Rebreak Prisma-Modell heißt 'profile' (nicht 'user') const submitter = await db.profile.findUnique({ where: { id: submitterUserId }, select: { nickname: true, username: true }, }); rawName = submitter?.nickname || submitter?.username || null; } catch {} } // Für @mention: Leerzeichen entfernen (Regex matcht nur einzelne Wörter) const mentionName = rawName?.replace(/\s+/g, "") ?? null; const hasMention = !!mentionName; // Stats für Lyra-Text holen (Zahlen sind locale-agnostic, locale-Format // bauen wir per-Sprache zusammen). let totalDomains = 0; let monthlyAdded = 0; try { totalDomains = await db.blocklistDomain.count({ where: { isActive: true } }); const startOfMonth = new Date(); startOfMonth.setDate(1); startOfMonth.setHours(0, 0, 0, 0); monthlyAdded = await db.domainSubmission.count({ where: { status: "approved", reviewedAt: { gte: startOfMonth } }, }); } catch {} const isMailDomain = domainType === "mail_domain"; // Per-Locale-Prompt-Bausteine. 4 parallele Groq-Calls statt 1 — Latency // bleibt ~gleich (parallel) + Output ist garantiert konsistent pro Sprache // (vs. 1 Multi-Locale-Call der gerne JSON-Format-Fehler produziert). type Lang = "de" | "en" | "fr" | "ar"; const LANGS: Lang[] = ["de", "en", "fr", "ar"]; const PROMPT_CFG: Record string; thanksSegment: (mentionRef: string) => string; }> = { de: { promptLang: "Deutsch", anonRef: "einem Community-Mitglied", subjectMail: `der Mail-Absender "${domain}"`, subjectWeb: `die Domain "${domain}"`, actionMail: `zur ReBreak-Blockliste hinzugefügt – casino-affiliates nutzen oft unauffällige Absender-Domains`, actionWeb: `zur ReBreak-Blockliste hinzugefügt`, statsLine: (t, m) => `Damit schützen wir gemeinsam vor ${t.toLocaleString("de-DE")} Domains${m > 0 ? ` (+${m} diesen Monat)` : ""}.`, thanksSegment: (m) => `– möglich gemacht durch ${m}. Erwähne ${m} genau einmal`, }, en: { promptLang: "English", anonRef: "a community member", subjectMail: `the mail sender "${domain}"`, subjectWeb: `the domain "${domain}"`, actionMail: `was added to the ReBreak blocklist — casino affiliates often use inconspicuous sender domains`, actionWeb: `was added to the ReBreak blocklist`, statsLine: (t, m) => `Together we now protect against ${t.toLocaleString("en-US")} domains${m > 0 ? ` (+${m} this month)` : ""}.`, thanksSegment: (m) => `— made possible by ${m}. Mention ${m} exactly once`, }, fr: { promptLang: "français", anonRef: "un membre de la communauté", subjectMail: `l'expéditeur de mail « ${domain} »`, subjectWeb: `le domaine « ${domain} »`, actionMail: `a été ajouté à la liste de blocage ReBreak — les affiliés casino utilisent souvent des domaines d'expéditeur discrets`, actionWeb: `a été ajouté à la liste de blocage ReBreak`, statsLine: (t, m) => `Ensemble, nous protégeons maintenant contre ${t.toLocaleString("fr-FR")} domaines${m > 0 ? ` (+${m} ce mois-ci)` : ""}.`, thanksSegment: (m) => `— rendu possible par ${m}. Mentionne ${m} exactement une fois`, }, ar: { promptLang: "العربية", anonRef: "أحد أفراد المجتمع", subjectMail: `المُرسِل البريدي «${domain}»`, subjectWeb: `النطاق «${domain}»`, actionMail: `تمت إضافته إلى قائمة الحظر في ReBreak — يستخدم المنتسبون للكازينوهات غالباً نطاقات مُرسِلين غير لافتة`, actionWeb: `تمت إضافته إلى قائمة الحظر في ReBreak`, statsLine: (t, m) => `معاً نحمي الآن من ${t.toLocaleString("ar-EG")} نطاق${m > 0 ? ` (+${m} هذا الشهر)` : ""}.`, thanksSegment: (m) => `— بفضل ${m}. اذكر ${m} مرة واحدة فقط`, }, }; const groqApiKey = config.groqApiKey; (async () => { try { // 4 parallele Groq-Calls, eine pro Sprache. Promise.allSettled damit // ein fehlgeschlagener Locale-Call die anderen 3 nicht killt — wir // speichern dann nur die erfolgreichen. const results = await Promise.allSettled( LANGS.map(async (lang) => { const cfg = PROMPT_CFG[lang]; const subject = isMailDomain ? cfg.subjectMail : cfg.subjectWeb; const action = isMailDomain ? cfg.actionMail : cfg.actionWeb; const stats = cfg.statsLine(totalDomains, monthlyAdded); const mentionRef = hasMention ? `@${mentionName}` : cfg.anonRef; const thanksPart = hasMention ? ` ${cfg.thanksSegment(mentionRef)}` : ""; const userPrompt = `Schreibe einen kurzen Community-Post (max. 2 Sätze, auf ${cfg.promptLang}): ${subject} ${action}${thanksPart}. Füge am Ende diesen Satz exakt ein: "${stats}" Warm, direkt, kein doppelter Dank.`; const response = await $fetch<{ choices: { message: { content: string } }[]; }>("https://api.groq.com/openai/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${groqApiKey}`, "Content-Type": "application/json", }, body: { model: "llama-3.3-70b-versatile", max_tokens: 200, messages: [ { role: "system", content: `Du bist Lyra – Recovery-Coach der ReBreak-Community. Tonalität: warm, persönlich, direkt. Schreibe NUR den Post-Text in der angefragten Sprache, kein Prefix, keine Anführungszeichen.`, }, { role: "user", content: userPrompt }, ], }, }); const content = response.choices?.[0]?.message?.content?.trim(); if (!content) throw new Error("empty content"); return [lang, content] as const; }), ); const localized: Partial> = {}; for (const r of results) { if (r.status === "fulfilled") { const [lang, content] = r.value; localized[lang] = content; } } // Mindestens DE muss klappen — sonst kein Post (sicheres Fallback, // statt einen halben deutsch-leeren Post zu schreiben). if (!localized.de) { console.error(`[approve] Lyra-Post abgebrochen: DE-Generation fehlgeschlagen für ${domain}`); return; } const contentJson = JSON.stringify(localized); const faviconUrl = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`; await db.communityPost.create({ data: { userId: lyraBotUserId, category: "domain_approved", // JSON-encoded mit Locale-Keys — PostCard.tsx try-parsed + picked // current-locale; fällt auf DE zurück wenn locale fehlt. content: contentJson, imageUrl: faviconUrl, isAnonymous: false, isModerated: false, }, }); console.log( `[approve] Lyra-Post erstellt für domain=${domain}, submitter=${mentionName ?? "anonym"}, locales=${Object.keys(localized).join(",")}`, ); } catch (err) { console.error(`[approve] Lyra-Post fehlgeschlagen:`, err); } })(); } return { ok: true }; });