/* * Meldingen bekijken én afhandelen. Er is bewust geen beheerdersscherm in de app: dat zou een * tweede soort account met meer rechten betekenen, en dat is precies het account dat je niet wilt * hebben in een app waar de brieven privé horen te zijn. Dit draait op de server, naast de database. * * Kijken: * npm run reports # openstaande meldingen, oudste eerst * npm run reports -- --all # ook de afgehandelde * npm run reports -- --user 3 # alles over één persoon: meldingen + eerdere maatregelen * npm run reports -- --afscheid [n] # de brieven van wie vertrokken is (standaard de laatste 20) * * Maatregelen — oplopend, en ze zetten de melding meteen op afgehandeld: * npm run reports -- --warn 12 "tekst" # waarschuwing; hij ziet hem bij het openen van de app * npm run reports -- --remove-letter 12 # de gemelde brief (met foto) weg * npm run reports -- --suspend 12 "reden" --days 30 # schorsen; zonder --days: onbepaalde tijd * npm run reports -- --unsuspend 3 # schorsing opheffen (op gebruikers-id) * npm run reports -- --remove-account 12 "reden" --yes * npm run reports -- --handle 12 # gezien, geen maatregel nodig * npm run reports -- --reopen 12 * * Op de server: draaien vanuit de werkkopie naast de .env, met `npm run reports`. * * Let op: dit toont de gemelde brief. Dat is de enige plek in het systeem waar iemand anders dan de * schrijver en de ontvanger een brief te zien krijgt, en het staat zo in de privacyverklaring — * lezen mag alleen om déze melding te beoordelen. */ import { pool } from "../server/db.js"; import * as db from "../server/queries.js"; import { REPORT_REASONS, REPORT_RETENTION_DAYS } from "../src/constants.js"; const args = process.argv.slice(2); const has = (name) => args.includes(`--${name}`); const flag = (name) => { const i = args.indexOf(`--${name}`); return i === -1 ? null : args[i + 1] ?? ""; }; /* Het tweede woord na een maatregel-vlag is de tekst: --warn 12 "..." */ const textAfter = (name) => { const i = args.indexOf(`--${name}`); return i === -1 ? null : args[i + 2] ?? null; }; const fmtDate = (d) => new Date(d).toLocaleString("nl-NL", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }); const person = (id, name, email) => (id == null ? "(account verwijderd)" : `${name} <${email}> #${id}`); const fail = (msg) => { console.error(msg); process.exitCode = 1; return null; }; /* Een melding ophalen bij een maatregel, met de gemelde erbij */ const reportFor = async (id, what) => { if (!/^\d+$/.test(id ?? "")) return fail(`geef een meldingsnummer: npm run reports -- --${what} 12 "…"`); const r = await db.getReport(id); return r ?? fail(`melding #${id} bestaat niet`); }; try { /* ---------- Maatregelen ---------- */ if (has("warn")) { const id = flag("warn"); const text = textAfter("warn"); const r = await reportFor(id, "warn"); if (r && !text) fail('geef de tekst van de waarschuwing mee: --warn 12 "Je brieven aan X waren…"'); else if (r) { await db.warnUser(r.reported_id, text, { reportId: r.id }); await db.setReportHandled(r.id, true); console.log(`waarschuwing naar ${r.reported_name} #${r.reported_id}; melding #${r.id} afgehandeld`); console.log("hij ziet hem bij het openen van de app en moet hem wegklikken."); } } else if (has("remove-letter")) { const id = flag("remove-letter"); const r = await reportFor(id, "remove-letter"); if (r && !r.letter_id) fail(`melding #${r.id} gaat over de persoon, niet over één brief — er is geen brief om te wissen`); else if (r) { const gone = await db.removeLetter(r.letter_id, { userId: r.reported_id, reportId: r.id, note: textAfter("remove-letter") ?? "" }); if (!gone) fail(`brief ${r.letter_id} bestond al niet meer`); else { await db.setReportHandled(r.id, true); console.log(`brief ${r.letter_id} van ${r.reported_name} #${r.reported_id} verwijderd (foto ging mee); melding #${r.id} afgehandeld`); console.log("de brief is aan beide kanten weg, zonder spoor. Overweeg er een --warn bij, anders weet de schrijver niets."); } } } else if (has("suspend")) { const id = flag("suspend"); const reason = textAfter("suspend"); const days = flag("days"); const r = await reportFor(id, "suspend"); if (r && !reason) fail('geef een reden mee: --suspend 12 "herhaalde intimidatie" --days 30'); else if (r && days !== null && !/^\d+$/.test(days)) fail("--days wil een aantal dagen"); else if (r) { const result = await db.suspendUser(r.reported_id, { days: days === null ? null : Number(days), reason, reportId: r.id }); await db.setReportHandled(r.id, true); console.log( `${r.reported_name} #${r.reported_id} geschorst ${result.until ? `tot ${fmtDate(result.until)}` : "voor onbepaalde tijd"}; melding #${r.id} afgehandeld` ); console.log("sessies zijn ingetrokken; inloggen geeft nu de reden en de einddatum."); } } else if (has("unsuspend")) { const userId = flag("unsuspend"); if (!/^\d+$/.test(userId ?? "")) fail("geef een gebruikers-id: npm run reports -- --unsuspend 3"); else { const ok = await db.unsuspendUser(userId, { note: textAfter("unsuspend") ?? "" }); console.log(ok ? `schorsing van gebruiker ${userId} opgeheven` : `gebruiker ${userId} was niet geschorst`); if (!ok) process.exitCode = 1; } } else if (has("remove-account")) { const id = flag("remove-account"); const reason = textAfter("remove-account"); const r = await reportFor(id, "remove-account"); if (r && !reason) fail('geef een reden mee: --remove-account 12 "minderjarig" --yes'); else if (r && !has("yes")) { console.log(`dit wist het account van ${r.reported_name} #${r.reported_id} definitief:`); console.log("profiel, sessies, favorieten, blokkades en álle brieven van en aan deze persoon —"); console.log("ook bij de correspondenten, en ook de foto's. Er is geen ongedaan maken."); console.log(`weet je het zeker, herhaal dan met --yes`); process.exitCode = 1; } else if (r) { const gone = await db.removeUserAsMeasure(r.reported_id, { reason, reportId: r.id }); console.log(gone ? `account ${r.reported_name} #${r.reported_id} verwijderd (${reason})` : "account bestond al niet meer"); console.log("de melding zelf is met het account meegegaan; in het logboek staat dát er is opgetreden."); } } else if (has("handle") || has("reopen")) { const reopen = has("reopen"); const id = reopen ? flag("reopen") : flag("handle"); if (!/^\d+$/.test(id ?? "")) fail("geef een meldingsnummer: npm run reports -- --handle 12"); else { const ok = await db.setReportHandled(id, !reopen); console.log(ok ? `melding #${id} ${reopen ? "weer open" : "afgehandeld"}` : `melding #${id} bestaat niet`); if (!ok) process.exitCode = 1; } } else if (has("afscheid")) { /* * ---------- De post van wie weggegaan is ---------- * * Geen meldingen, geen maatregelen, niets af te handelen: dit zijn brieven. Er staat geen * afzender bij en dat is het ontwerp (zie `farewells` in db/schema.sql) — antwoorden kan niet, * lezen wel. Het is het enige wat een vertrek in woorden achterlaat, en daarmee de enige plek * waar staat waaróm de teller in `departures` opliep. */ const n = /^\d+$/.test(flag("afscheid") ?? "") ? Number(flag("afscheid")) : 20; const brieven = await db.listFarewells({ limit: n }); if (!brieven.length) console.log("geen afscheidsbrieven"); for (const b of brieven) { console.log("─".repeat(78)); console.log(`${fmtDate(b.at)} (anoniem — hier staat met opzet niet wie dit schreef)`); console.log(b.body.split("\n").map((l) => ` | ${l}`).join("\n")); } if (brieven.length) { console.log("─".repeat(78)); console.log(`${brieven.length} ${brieven.length === 1 ? "brief" : "brieven"}${brieven.length === n ? ` (de laatste ${n}; meer met --afscheid ${n * 2})` : ""}`); } } else if (has("user")) { /* ---------- Eén persoon: het hele dossier ---------- */ const userId = flag("user"); if (!/^\d+$/.test(userId ?? "")) fail("geef een gebruikers-id: npm run reports -- --user 3"); else { const [u, measures] = await Promise.all([db.getUser(userId), db.listMeasures(userId)]); if (!u) fail(`gebruiker ${userId} bestaat niet`); else { const suspended = await db.isSuspended(userId); console.log(`${u.name} #${u.id}${suspended ? " [GESCHORST]" : ""}`); const rows = (await db.listReports({ all: true, limit: 200 })).filter((r) => String(r.reported_id) === String(userId)); console.log(`\nmeldingen (${rows.length}):`); for (const r of rows) { console.log(` #${r.id} ${fmtDate(r.created_at)} ${REPORT_REASONS[r.reason] ?? r.reason}${r.handled_at ? " [afgehandeld]" : ""}`); } console.log(`\nmaatregelen (${measures.length}):`); for (const m of measures) { console.log(` ${fmtDate(m.created_at)} ${m.kind}${m.report_id ? ` (melding #${m.report_id})` : ""}: ${m.note}`); } } } } else { /* ---------- De werkvoorraad ---------- */ const all = has("all"); const rows = await db.listReports({ all }); if (!rows.length) console.log(all ? "geen meldingen" : "geen openstaande meldingen"); for (const r of rows) { const measures = await db.listMeasures(r.reported_id); console.log("─".repeat(78)); console.log( `#${r.id} ${fmtDate(r.created_at)} ${REPORT_REASONS[r.reason] ?? r.reason}` + `${r.handled_at ? ` [afgehandeld ${fmtDate(r.handled_at)}]` : ""}` ); console.log(` gemeld : ${person(r.reported_id, r.reported_name, r.reported_email)}`); console.log(` melder : ${person(r.reporter_id, r.reporter_name, r.reporter_email)}`); if (r.reported_total > 1) console.log(` let op : ${r.reported_total} meldingen over deze persoon`); if (measures.length) console.log(` eerder : ${measures.map((m) => m.kind).join(", ")} (zie --user ${r.reported_id})`); if (r.details) console.log(` toelichting: ${r.details.replace(/\n/g, "\n ")}`); if (r.letter_id) { const body = r.letter_body.length > 600 ? `${r.letter_body.slice(0, 600)}…` : r.letter_body; console.log(` brief #${r.letter_id} van ${fmtDate(r.letter_sent_at)}${r.letter_has_photo ? " (met foto)" : ""}:`); console.log(body.split("\n").map((l) => ` | ${l}`).join("\n")); } else { console.log(" (melding over de persoon, niet over één brief)"); } } if (rows.length) { console.log("─".repeat(78)); console.log('maatregelen: --warn "tekst" · --remove-letter · --suspend "reden" [--days N]'); console.log(" --remove-account \"reden\" --yes · --handle (gezien, niets nodig)"); console.log(`meldingen worden na ${REPORT_RETENTION_DAYS} dagen automatisch gewist.`); } /* * Post van wie weggegaan is. Geen werkvoorraad — er valt niets af te handelen — dus alleen een * regel als er de afgelopen maand iets binnenkwam, en niet een teller die tot in lengte van * dagen blijft staan aandringen. */ const afscheid = await db.countFarewells({ days: 30 }); if (afscheid) console.log(`\nafscheidsbrieven: ${afscheid} in de laatste 30 dagen. Lezen: npm run reports -- --afscheid`); /* * De gratis bak, maar alleen als het ertoe doet. Zwijgen zolang er ruimte zat is; waarschuwen * vanaf 10% resterend, want vanaf dat moment moet het artikel in de Play Console echt werken — * anders loopt de eerste betalende gebruiker tegen een 503 aan in plaats van tegen een kassa. */ const slots = await db.signupCounts(); if (slots.left === 0) { console.log(`\ngratis plekken: op (${slots.granted}/${slots.total}). Registreren kost nu geld; controleer dat de aankoop in de store werkt.`); } else if (slots.left <= slots.total / 10) { console.log(`\ngratis plekken: nog ${slots.left} van de ${slots.total}. Zorg dat het artikel in de Play Console live staat voordat ze op zijn.`); } } } finally { await pool.end(); }