Een dating-app op basis van brieven, niet van gezichten!

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

12.2 kB Ruw bestand
/*
 * 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();
}