import { NextResponse } from 'next/server' import { getDb } from '@/lib/db' export const runtime = 'nodejs' // Simple bad word check function hasBadWords(text: string): { flag: boolean; reason?: string } { const lower = text.toLowerCase() const badWords = [ { word: 'hurensohn', reason: 'Beleidigung' }, { word: 'arschloch', reason: 'Beleidigung' }, { word: 'wichser', reason: 'Beleidigung' }, { word: 'fotze', reason: 'Beleidigung' }, { word: 'spam', reason: 'Spam-Verdacht' }, { word: 'werbung', reason: 'Werbung' }, { word: 'casino', reason: 'Werbung' }, { word: 'viagra', reason: 'Werbung' } ] for (const { word, reason } of badWords) { if (lower.includes(word)) { return { flag: true, reason } } } return { flag: false } } // Background AI moderation with Ollama async function moderateWithAI(contributionId: number, content: string) { console.log(`[AI-Mod] Starting for ${contributionId}`) // Step 1: Instant bad word check const lowerCheck = content.toLowerCase() const badWords = ['hurensohn', 'arschloch', 'wichser', 'fotze'] const foundBadWord = badWords.find(word => lowerCheck.includes(word)) if (foundBadWord) { console.log(`[AI-Mod] ⚠️ INSTANT FLAG: "${foundBadWord}" detected!`) const db = getDb() db.prepare(` UPDATE contributions SET status = 'flagged', moderation_reason = ? WHERE id = ? `).run(`Unangemessene Sprache: "${foundBadWord}"`, contributionId) console.log(`[AI-Mod] ✅ FLAGGED ${contributionId} instantly`) return } // Step 2: AI check for subtle issues (irrelevant content, hidden insults etc.) console.log(`[AI-Mod] No bad words, asking AI...`) try { const prompt = `Du prüfst Beiträge für eine Gedenkseite einer verstorbenen Großmutter (Maria Malejka). Text: "${content}" WICHTIG: Sei SEHR großzügig! Die meisten Beiträge sind von Familienmitgliedern und Freunden. ERLAUBT (immer appropriate=true): - Kurze Beschreibungen wie "Hochzeit", "Geburtstag", "Urlaub in..." - das sind Erinnerungen! - Namen, Orte, Daten - das sind Zeitstrahl-Einträge - Alles was eine Erinnerung, ein Ereignis oder ein Lebensmoment sein könnte - Persönliche Geschichten, Beileidsbekundungen, Liebe, Vermissen, Trauer - Auch sehr kurze Texte oder einzelne Wörter sind OK wenn sie ein Ereignis beschreiben NUR VERBOTEN (appropriate=false): - Beleidigungen, Hassrede, Schimpfwörter - Offensichtlicher Spam oder Werbung mit Links - Komplett sinnloser Text (zufällige Buchstaben, Tastatur-Spam) Im Zweifel: appropriate=true Antworte NUR mit JSON: {"appropriate": true} oder {"appropriate": false, "reason": "kurze Begründung"} JSON:` const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 15000) const ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434' const res = await fetch(`${ollamaUrl}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, body: JSON.stringify({ model: 'llama3.2:latest', prompt, stream: false, options: { temperature: 0.1, num_predict: 60, num_ctx: 512 } }) }) if (!res.ok) { clearTimeout(timeout) console.warn(`[AI-Mod] Ollama error: ${res.status}`) return } const data = await res.json() clearTimeout(timeout) const responseText = (data.response || '').trim() console.log(`[AI-Mod] Response: "${responseText}"`) // Parse JSON let result: any = null try { const jsonMatch = responseText.match(/\{[^}]+\}/) if (jsonMatch) { result = JSON.parse(jsonMatch[0]) } } catch (e) { console.error(`[AI-Mod] Parse error:`, e) } // Flag if inappropriate if (result && result.appropriate === false) { const db = getDb() db.prepare(` UPDATE contributions SET status = 'flagged', moderation_reason = ? WHERE id = ? `).run(result.reason || 'KI-Warnung', contributionId) console.log(`[AI-Mod] ⚠️ FLAGGED ${contributionId}: ${result.reason}`) } else { console.log(`[AI-Mod] ✅ Passed ${contributionId}`) } } catch (error: any) { if (error.name === 'AbortError') { console.warn(`[AI-Mod] Timeout for ${contributionId}`) } else { console.error(`[AI-Mod] Error for ${contributionId}:`, error.message) } } } export async function POST(request: Request) { try { const body = await request.json() const { name, type, title, content, year, month, day, location, media_filenames } = body if (!type) { return NextResponse.json( { error: 'Type is required' }, { status: 400 } ) } // Require content for memory type, title for timeline if (type === 'memory' && !content) { return NextResponse.json( { error: 'Content is required for memories' }, { status: 400 } ) } if (type === 'timeline' && !title) { return NextResponse.json( { error: 'Title is required for timeline entries' }, { status: 400 } ) } const db = getDb() // 1. Check bad words instantly - clean content is auto-approved const textToCheck = [content, title].filter(Boolean).join(' ') const badWordCheck = textToCheck ? hasBadWords(textToCheck) : { flag: false } const initialStatus = badWordCheck.flag ? 'flagged' : 'approved' const moderationReason = badWordCheck.flag ? (badWordCheck as any).reason : null // 2. Insert contribution with all fields const result = db.prepare(` INSERT INTO contributions (name, type, title, content, year, month, day, location, media_filenames, status, moderation_reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( name || 'Anonym', type, title || null, content || null, year || null, month || null, day || null, location || null, media_filenames || null, initialStatus, moderationReason || null ) const contributionId = Number(result.lastInsertRowid) console.log(`[API] Created contribution ${contributionId}, type: ${type}, status: ${initialStatus}`) // 3. If not already flagged and has text, run AI check in background if (!badWordCheck.flag && textToCheck) { moderateWithAI(contributionId, textToCheck).catch(e => console.error('[AI-Mod] Background error:', e) ) } return NextResponse.json({ success: true, id: contributionId, message: 'Beitrag wurde gespeichert' }) } catch (error) { console.error('[API] Error:', error) return NextResponse.json( { error: 'Failed to create contribution' }, { status: 500 } ) } } export async function GET(request: Request) { try { const { searchParams } = new URL(request.url) const status = searchParams.get('status') const db = getDb() let query: string let params: string[] = [] if (status) { query = 'SELECT * FROM contributions WHERE status = ? ORDER BY created_at DESC' params = [status] } else { // Return ALL contributions (admin needs all, public will filter client-side) query = 'SELECT * FROM contributions ORDER BY created_at DESC' } const contributions = db.prepare(query).all(...params) return NextResponse.json(contributions) } catch (error) { console.error('[API] Error fetching:', error) return NextResponse.json( { error: 'Failed to fetch contributions' }, { status: 500 } ) } }