Een dating-app op basis van brieven, niet van gezichten!
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
/*
* 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 <nr> "tekst" · --remove-letter <nr> · --suspend <nr> "reden" [--days N]');
console.log(" --remove-account <nr> \"reden\" --yes · --handle <nr> (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();
}