Chat: "@rudel" ruft das ganze Team -- und nur die vier duerfen es

Filipe: "dan will ich auch dass nur die modis, rechte hand, linke hand
und dogfather, alle auch auf einmal markieren koennen im chat mit
einem, @rudel ,dann sollen alle eine benarichtigung bekommen."

DAS WAR EINE OFFENE FRAGE, UND ER HAT SIE BEANTWORTET
-----------------------------------------------------
In chat-erwaehnung.js stand seit dem 20.09. woertlich: "KEIN @alle. Es
waere in fuenf Minuten gebaut und ist der zuverlaessigste Weg, dass alle
die Benachrichtigungen abschalten -- und dann kommt auch die an, die
wirklich fuer einen bestimmten Menschen gedacht war. Wenn Filipe es
ausdruecklich will, gehoert dazu eine Entscheidung, WER es benutzen
darf; das ist eine Frage an ihn, keine, die ich hier beantworte."

Seine Antwort ist genau diese Entscheidung, und sie ist die Sicherung:
Team Dogi und DogFather duerfen rufen, sonst niemand.

WEN DER RUF ERREICHT: DAS TEAM IM RAUM, NICHT DEN RAUM
------------------------------------------------------
"Rudel" heisst das Team -- dieselben vier Gruppen, die auch rufen
duerfen. In einem Team-Kanal ist das jeder Anwesende, also "alle auf
einmal". Sichtbar wird der Unterschied im TREFF, und dort haette die
andere Lesart wehgetan: Dort sitzt die Community. Ein Ruf, der jeden
Zuschauer weckt, waere etwas anderes als der bestellte -- und beim
zweiten Mal haetten sie die Benachrichtigungen abgeschaltet.

WO WAS ENTSCHIEDEN WIRD
-----------------------
chat-erwaehnung.js bleibt ohne Abhaengigkeiten (die Pruefung soll sie
lesen koennen, ohne einen Server hochzufahren). Sie sagt nur, DASS
gerufen wurde; der Aufrufer sagt ihr, ob der Schreibende darf. Wer zum
Rudel gehoert, entscheidet workspace-chat.js ueber istTeamDogi().

Das Schluesselwort gewinnt gegen einen Menschen, der "Rudel" heisst.
Heute heisst niemand so -- aber das ist eine Tatsache von heute, kein
Gesetz. So herum verliert niemand eine Meldung (wer so heisst, ist im
Rudel dabei); andersherum haette ein einziger Zugang den Ruf ans Team
stillschweigend abgeschaltet.

istTeamDogi() NEU IN workspace.js
---------------------------------
Derselbe Ausdruck ("admin oder TEAM_DOGI_ROLLEN") stand dort dreimal
wortgleich: siehtModis, kanaeleFuer, kategorienFuer. Drei gleiche
Ausdruecke sind drei Gelegenheiten, dass einer beim naechsten
Rollenzuschnitt nicht mitgeht. Jetzt eine Stelle, die drei benutzen.

DIE NACHRICHT MERKT SICH DEN RUF (Spalte chat_nachrichten.rudel)
----------------------------------------------------------------
Beim Lesen muesste der Browser sonst wissen, ob der Absender es DAMALS
durfte. Er kennt nur dessen heutige Rolle -- wechselt jemand die Rolle,
verschwaende die Hervorhebung rueckwirkend aus einem Satz von vorletzter
Woche. Und es waere die zweite Rechnung ueber dieselbe Frage.
Vorgabe 0; alle alten Nachrichten haben kein Rudel gerufen, und das ist
keine Annahme, sondern eine Tatsache: Das Wort gab es noch nicht.

DIE MELDUNG SAGT, WAS LOS IST
-----------------------------
"X hat das Rudel gerufen", nicht "X hat dich erwaehnt" -- letzteres
stimmt beim Rudel nicht, und wer dreimal liest, dass er gemeint sei,
und jedes Mal merkt, dass es alle betraf, glaubt beim vierten Mal auch
dem echten nicht mehr. Auf DEMSELBEN Schalter wie die Erwaehnung: Ein
vierter Schalter waere der, den jemand abschaltet und der dann genau im
wichtigen Moment fehlt. Jeder Ruf steht im Protokoll (chat_rudel) --
"@rudel wird zu oft benutzt" soll eine Zahl sein koennen, kein Gefuehl.

DIE FARBE WIRD ABGELEITET, NICHT GESETZT
----------------------------------------
Eine Nachricht liegt in der Blase, deren Farbe ihr Absender AUSGESUCHT
hat -- siebzehn Moeglichkeiten. Eine feste Farbe darauf ist eine Wette,
und genau die habe ich am 22.09. beim Loeschknopf verloren (1,91:1 auf
Babyblau, gemessen, nachdem es live war). Die Marke nimmt deshalb
`--blase-text` -- die Schrift, die schriftFuer() fuer DIESE Blase mit
mindestens 7:1 ausgerechnet hat. Unterschieden wird ueber Form statt
Farbton: Toenung, Kante, Gewicht 700, ein Zeichen davor. In der
Auswahlliste darf es einen eigenen Ton haben -- sie liegt auf der
Flaeche des Hauses, deren Farbe feststeht.

DER VORSCHLAG ERSCHEINT NUR, WO ER ETWAS BEWIRKT
------------------------------------------------
In einem Zweier-Gespraech mit einem Creator ist ausser mir niemand aus
dem Team. `darf_rudel` fragt deshalb beides: darf ich, und sitzt hier
noch jemand aus dem Team. Dieselbe Ueberlegung wie beim eigenen Namen,
den die Liste auch nicht anbietet. Die Schranke beim Schreiben haengt
nicht daran -- wer es von Hand tippt, ruft eben niemanden.

GEPRUEFT
--------
pruef-erwaehnung: 119 Pruefungen, 0 Fehler (vorher 66).
Neu darin, und die zweite ist die wichtigere:
  * der Modi ruft im Treff genau das Team -- die Liste wird aus der
    Besetzung ABGELEITET, nicht abgeschrieben
  * der Gast im selben Raum ruft NICHTS: kein Eintrag, kein Merkmal,
    kein Vorschlag. Ohne diese Pruefung stuende Filipes "nur die
    modis, rechte hand, linke hand und dogfather" bloss im Kommentar
  * beide Fassungen der Regel (Server und Browser) an 14 zusaetzlichen
    Proben nebeneinander, mit beiden Rechten -- samt Gegenprobe, dass
    der Vergleich einen Unterschied ueberhaupt sehen kann
  * am Bildschirm: der Ruf steht oben in der Liste, sagt daneben, was
    er bedeutet, Enter setzt ihn ein, und im Satz ist er an Gewicht
    und Rahmen erkennbar -- nicht nur an der Farbe

Beim Bauen gemessen statt vermutet: Ein Scout und ein Modi kommen gar
nicht in denselben Raum (403, Haeusertrennung) -- deshalb prueft der
Verhaltenstest im Treff. Und ein Gast meldet sich nur mit
Altersbestaetigung an (400 ohne).

Die Nachtruhe des Treffs wird in dieser Pruefung abgeschaltet (gleiche
Stunden = keine Nachtruhe). Sonst waere sie zwischen Mitternacht und
sechs rot und danach gruen -- ein Test, der die Wanduhr misst.

pruef-chat, pruef-treffchat (110), pruef-chat-kanaele (81),
pruef-chat-ausbau (64): alle unveraendert gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-23 01:36:09 +02:00
co-authored by Claude Opus 5
parent 2a3058e98e
commit 28b0143166
45 changed files with 1427 additions and 628 deletions
+425 -1
View File
@@ -40,7 +40,7 @@ import { fileURLToPath } from "node:url";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
import { notbremse } from "./helfer-notbremse.mjs";
import { erwaehnungenFinden, erwaehnteIds } from "./chat-erwaehnung.js";
import { erwaehnungenFinden, erwaehnteIds, rudelGerufen, RUDEL_WORT } from "./chat-erwaehnung.js";
const HIER = dirname(fileURLToPath(import.meta.url));
const PORT = await eigenerPort(import.meta, "pruef-erwaehnung");
@@ -50,6 +50,19 @@ process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
/* KEINE NACHTRUHE WAEHREND DER PRUEFUNG (23.09.2026).
Abschnitt 7 schreibt im Treff -- und dort gilt von 0 bis 6 Uhr
Schreibverbot. Eine Pruefung, die zwischen Mitternacht und sechs
scheitert und danach wieder gruen ist, misst die Uhr, nicht das
Programm. Genau diese Falle steht in den Hausregeln ("Ein Test, der
die Wanduhr als Annahme benutzt, misst irgendwann das Gegenteil").
Gleiche Werte heissen "keine Nachtruhe" -- so steht es in
istNachtruhe(), erste Zeile. Die Nachtruhe SELBST wird in
pruef-treffchat geprueft, dort gehoert sie hin. */
process.env.TREFF_NACHT_AB = "0";
process.env.TREFF_NACHT_BIS = "0";
const express = (await import("express")).default;
const ec = express.response.cookie;
@@ -141,6 +154,73 @@ melde("\n=== 1. Wer ist gemeint? ===");
immer jemanden findet, findet nichts. */
ok(erwaehnteIds("@VanVan", []).length === 0,
"Gegenprobe: ohne Teilnehmerliste ist niemand gemeint");
/* ---- "@RUDEL" — ALLE AUF EINMAL (23.09.2026) ------------------
Filipe: „dan will ich auch dass nur die modis, rechte hand, linke
hand und dogfather, alle auch auf einmal markieren können im chat
mit einem, @rudel ,dann sollen alle eine benarichtigung bekommen."
DIE WICHTIGSTE PRÜFUNG HIER IST DIE ZWEITE: dass es NICHTS tut,
wenn der Schreibende nicht darf. Sonst hätte die Einschränkung,
die Filipe ausdrücklich genannt hat, nur im Kommentar gestanden.
Die Grenzregeln (2) und (3) gelten für das Schlüsselwort genauso
wie für einen Namen — sonst ruft eine Mailadresse das ganze Team. */
const rudelFaelle = [
["@rudel alle mal herhören", true, true, "der einfache Fall"],
["@RUDEL", true, true, "gross geschrieben trifft auch"],
["@rudel alle mal herhören", false, false,
"wer nicht darf, ruft nichts — und merkt nichts davon"],
["schreib an [email protected]", true, false,
"eine Mailadresse ruft das Rudel nicht"],
["@rudeltier", true, false, "ein längeres Wort ist kein Ruf"],
["das rudel ohne at", true, false, "ohne @ ist es nur ein Wort"],
["(@rudel)", true, true, "in Klammern"],
["@Tili und @rudel", true, true, "neben einem Namen"],
];
let rudelStimmt = 0;
for (const [text, darf, soll, was] of rudelFaelle) {
const ist = rudelGerufen(text, leute, { rudel: darf });
if (ist === soll) rudelStimmt++;
ok(ist === soll, `@rudel: ${was}${ist === soll ? "" : ` (${JSON.stringify(text)}, darf=${darf} -> ${ist})`}`);
}
ok(rudelStimmt === rudelFaelle.length && rudelFaelle.length >= 8,
`alle ${rudelFaelle.length} Rudel-Fälle stimmen`);
/* OHNE ANGABE DARF MAN NICHT. Eine vergessene Angabe soll zu wenig
können, nicht zu viel — sonst ruft der erste Aufruf, der die
Option nicht kennt, aus Versehen das ganze Team. */
ok(rudelGerufen("@rudel", leute) === false,
"ohne Angabe gilt: darf nicht (eine vergessene Angabe kann zu wenig, nie zu viel)");
/* NEBEN EINEM NAMEN BLEIBT DER NAME. "@Tili und @rudel" ist beides. */
ok(JSON.stringify(erwaehnteIds("@Tili und @rudel", leute, { rudel: true })) === "[3]",
"neben dem Ruf bleibt der einzeln Genannte einzeln genannt");
/* DAS SCHLÜSSELWORT GEWINNT GEGEN EINEN MENSCHEN NAMENS "RUDEL".
Heute heisst niemand so — aber das ist eine Tatsache von heute.
So herum verliert niemand eine Meldung: Wer "Rudel" heisst, ist
im Rudel ohnehin dabei. Andersherum wäre der Ruf ans Team durch
einen einzigen Zugang stillschweigend abgeschaltet. */
const mitRudel = [{ id: 20, name: "Rudel" }, { id: 21, name: "Anna" }];
ok(rudelGerufen("@Rudel hallo", mitRudel, { rudel: true }) === true,
"heisst jemand „Rudel“, gewinnt trotzdem das Schlüsselwort");
ok(JSON.stringify(erwaehnteIds("@Rudel hallo", mitRudel, { rudel: false })) === "[20]",
" und wer nicht rufen darf, spricht damit genau diesen Menschen an");
/* DAS WORT STEHT AN EINER STELLE. Stünde es zweimal, hiesse es nach
einer Umbenennung an einer davon anders. */
ok(RUDEL_WORT === "rudel", `das Wort kommt aus der Datei („${RUDEL_WORT}“)`);
/* GEGENPROBE: Die Auflösung liefert beim Rudel KEINE Personennummer
— es ist keine Person. Wer daraus eine ID läse, bekäme `null` in
die Empfängerliste und benachrichtigte niemanden. */
const rt = erwaehnungenFinden("@rudel", leute, { rudel: true });
ok(rt.length === 1 && rt[0].rudel === true && rt[0].id === null,
"der Ruf ist keine Person (id null, rudel true)");
ok(erwaehnteIds("@rudel", leute, { rudel: true }).length === 0,
" und taucht deshalb in der Namensliste gar nicht auf");
}
/* =====================================================================
@@ -391,6 +471,22 @@ melde("\n=== 4. Die Meldung sagt, dass DU gemeint bist ===");
ok(art?.vorgabe === true, " und ist von sich aus an");
ok(ARTEN.find((a) => a.schluessel === "chat_nachricht")?.schluessel !== art?.schluessel,
" getrennt von der gewöhnlichen Chatnachricht — zwei Schalter, zwei Entscheidungen");
/* ---- UND BEIM RUDEL SAGT SIE, DASS ALLE GEMEINT SIND ----------
Filipe: „dann sollen alle eine benarichtigung bekommen."
WARUM NICHT DERSELBE SATZ WIE BEI DER ERWÄHNUNG: Weil er nicht
stimmt. Beim Rudel ist man mitgemeint, nicht gemeint — und wer
dreimal „hat dich erwähnt" liest und jedes Mal merkt, dass es
alle betraf, glaubt beim vierten Mal auch dem echten nicht mehr. */
const gerufen = pushTextFuer("rudel", n, raumId);
ok(/Rudel gerufen/.test(gerufen.titel), `-> ${gerufen.titel}`);
ok(gerufen.titel !== erw.titel && gerufen.titel !== normal.titel,
" und unterscheidet sich von BEIDEN anderen Meldungen");
ok(gerufen.text.includes("schaust du mal"),
" der Anfang der Nachricht steht auch hier darunter");
ok(gerufen.ziel === normal.ziel, " und führt in dasselbe Gespräch");
}
/* =====================================================================
@@ -451,6 +547,43 @@ melde("\n=== 5. Server und Browser rechnen gleich ===");
}
ok(gleich === proben.length && proben.length >= 10,
`beide kommen bei allen ${proben.length} Proben auf dasselbe Ergebnis`);
/* ---- UND DASSELBE MIT "@RUDEL" (23.09.2026) ----------------
DER DRITTE PARAMETER MUSS MITGEPRUEFT WERDEN, sonst waere er die
Stelle, an der die beiden Fassungen auseinanderlaufen -- und
zwar unbemerkt, weil die elf Proben darueber ihn gar nicht
benutzen. Beide Wahrheitswerte, damit auch das Nichtstun
verglichen wird. */
const rudelProben = [
"@rudel alle mal herhoeren", "@RUDEL", "[email protected]",
"@rudeltier", "(@rudel)", "@Tili und @rudel", "kein at rudel",
];
let rGleich = 0, rVerglichen = 0;
for (const t of rudelProben) {
for (const darf of [true, false]) {
rVerglichen++;
const a = JSON.stringify(erwaehnungenFinden(t, leute, { rudel: darf }));
const b = JSON.stringify(browserFassung(t, leute, { rudel: darf }));
if (a === b) rGleich++;
else {
console.log(` ungleich bei ${JSON.stringify(t)} (darf=${darf})`);
console.log(` Server: ${a}`);
console.log(` Browser: ${b}`);
}
}
}
ok(rGleich === rVerglichen && rVerglichen >= 14,
`auch beim Rudel dasselbe Ergebnis (${rVerglichen} Proben, beide Rechte)`);
/* GEGENPROBE DES VERGLEICHS: Er muss einen Unterschied auch SEHEN.
Ohne das waere er eine Schleife, die immer „gleich“ sagt -- und
genau so eine stand am 01.09. gruen da, ohne je etwas anzusehen. */
const ungleichErkannt =
JSON.stringify(erwaehnungenFinden("@rudel", leute, { rudel: true }))
!== JSON.stringify(browserFassung("@rudel", leute, { rudel: false }));
ok(ungleichErkannt,
"Gegenprobe: bei verschiedenen Rechten kommt Verschiedenes heraus — der Vergleich kann Unterschiede sehen");
}
}
@@ -467,6 +600,39 @@ melde("\n=== 5. Server und Browser rechnen gleich ===");
liegt, wenn Enter statt zu wählen die halbe Nachricht abschickt,
oder wenn der Name am Ende grau bleibt wie jedes andere Wort.
===================================================================== */
/* EIN RAUM, IN DEM DER RUF ETWAS BEWIRKT (23.09.2026).
Fuer den Bildschirmteil wird ein Gespraech gebraucht, in dem ausser
mir noch jemand aus dem Team sitzt -- sonst bietet die Liste den Ruf
gar nicht erst an (und das ist richtig so: ein Vorschlag, der
niemanden erreicht, ist ein Vorschlag, der nichts tut).
ZWEI ZUGAENGE MIT DER DOGFATHER-ROLLE, und zwar bewusst statt eines
Modis: Der Browser dieser Pruefung laeuft auf 127.0.0.1, und dort
steht die Agentur-Wand -- die Rolle "modi" gibt es auf ihr nicht
(siehe der Kommentar ganz oben). Wer hier das Team braucht, nimmt
die einzige Team-Rolle, die auf beiden Waenden vorkommt. */
let raumTeam = null;
{
const d3 = new DatabaseSync(process.env.WORKSPACE_DB);
const sch = d3.prepare("SELECT wert FROM einstellungen WHERE schluessel = ?")
.get("code_kennung_schluessel").wert;
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-DOGI-0002", salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d3.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run("Zweiter", "admin", hash, salt, 32768,
createHmac("sha256", sch).update("CODE-DOGI-0002").digest("hex"), jetzt);
const idZwei = d3.prepare("SELECT last_insert_rowid() AS id").get().id;
d3.close();
const g = await ruf("/workspace/api/chat/gruppe", aDogi.keks, "POST",
{ name: "Team intern", mit: [idZwei] });
raumTeam = json(g).raum_id ?? null;
ok(Number.isInteger(raumTeam),
`ein Raum, in dem noch jemand aus dem Team sitzt (Raum ${raumTeam})`);
}
melde("\n=== 6. Am echten Bildschirm ===");
const BASIS = `http://127.0.0.1:${PORT}`;
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
@@ -631,12 +797,270 @@ try {
const jetztRaus = await s.locator(".chat-nachricht").count();
ok(jetztRaus > vorher + 1, ` und Enter schickt wieder ab (${jetztRaus}, vorher ${vorher})`);
/* ---- "@RUDEL" AM BILDSCHIRM (23.09.2026) -----------------------
Alles bisher hat bewiesen, dass der Server den Richtigen ruft.
Hier geht es um die andere Haelfte: Findet man es, und sieht man
es? Ein Ruf, den niemand vorgeschlagen bekommt und der im Satz
grau bleibt wie jedes andere Wort, ist eine Attrappe. */
if (Number.isInteger(raumTeam)) {
await s.goto(`${BASIS}/workspace/chat.html?raum=${raumTeam}`, { waitUntil: "networkidle" });
await s.waitForTimeout(1200);
await s.click("#text");
await s.type("#text", "@", { delay: 40 });
await s.waitForTimeout(400);
const eintraege = await liste.locator(".erwaehn-liste__eintrag").count();
const ersterName = await liste.locator(".erwaehn-liste__name").first().innerText();
const ersterZusatz = await liste.locator(".erwaehn-liste__rolle").first().innerText();
ok(eintraege >= 2 && ersterName === "rudel",
`der Ruf steht ganz oben in der Liste (${eintraege} Eintraege, erster: ${ersterName})`);
/* WAS ES BEDEUTET, MUSS DANEBENSTEHEN. "rudel" allein sagt keinem
Neuen, dass damit das ganze Team gemeint ist -- und wer das erst
nach dem Absenden erfaehrt, hat schon alle geweckt. */
ok(/alle im Team/i.test(ersterZusatz),
` und daneben steht, was es bedeutet ("${ersterZusatz}")`);
const alsPerson = await liste.locator('.erwaehn-liste__eintrag[data-rudel="ja"]').count();
ok(alsPerson === 1, ` genau einmal, als eigene Sorte Eintrag (${alsPerson})`);
await s.keyboard.press("Enter");
await s.waitForTimeout(300);
ok((await s.inputValue("#text")) === "@rudel ",
`Enter setzt den Ruf ein: ${JSON.stringify(await s.inputValue("#text"))}`);
await s.type("#text", "bitte alle kurz schauen", { delay: 10 });
await s.keyboard.press("Enter");
await s.waitForTimeout(1500);
const marke = await s.evaluate(() => {
const m = document.querySelector('.chat-erwaehnung[data-rudel="ja"]');
if (!m) return null;
const st = getComputedStyle(m);
const normal = document.querySelector(".chat-nachricht__text");
return {
wort: m.textContent.trim(),
gewicht: st.fontWeight,
schatten: st.boxShadow !== "none",
eigen: m.dataset.ich === "ja",
farbeGleichText: st.color === getComputedStyle(normal).color,
};
});
ok(!!marke, "der Ruf ist im Satz hervorgehoben");
/* Das Zeichen steht per Stilvorlage davor (::before) und zaehlt
nicht zum Textinhalt -- geprueft wird deshalb das Wort. */
ok(marke?.wort === "@rudel", ` und zwar genau: ${marke?.wort}`);
/* NICHT NUR DIE FARBE. Wer Farben schlecht unterscheidet, muss es
trotzdem sehen -- dieselbe Regel wie beim eigenen Namen. */
ok(Number(marke?.gewicht) >= 600 && marke?.schatten === true,
` erkennbar an Gewicht (${marke?.gewicht}) und Rahmen (${marke?.schatten}), nicht nur an der Farbe`);
/* GEGENPROBE: Es ist NICHT die Marke fuer "du bist gemeint". Saehen
beide gleich aus, verloere die persoenliche ihre Wirkung. */
ok(marke?.eigen === false,
" und es ist nicht die Marke fuer den eigenen Namen — alle gemeint ist etwas anderes als du gemeint");
} else {
ok(false, "ohne Team-Raum ist der Ruf am Bildschirm nicht messbar");
}
await dogi.ctx.close();
await van.ctx.close();
} finally {
await browser.close();
}
/* =====================================================================
7. "@RUDEL" DURCH DIE SCHNITTSTELLE (23.09.2026)
Filipe: „dan will ich auch dass nur die modis, rechte hand, linke
hand und dogfather, alle auch auf einmal markieren können im chat
mit einem, @rudel ,dann sollen alle eine benarichtigung bekommen."
ALLES BISHER WAR RECHNEN OHNE SERVER. Hier geht es durch die
richtige Tür.
GEPRÜFT WIRD IM TREFF, UND DAS IST KEINE BEQUEMLICHKEIT. Der Treff
ist der einzige Raum des Hauses, in dem Team Dogi und Menschen
sitzen, die NICHT dazugehören (Rolle „gast" -- die Community). In
einem gewöhnlichen Gespräch geht das gar nicht: Die Häusertrennung
lässt einen Scout und einen Modi nicht in denselben Raum, das ist
beim Bauen dieser Prüfung gemessen worden (403, „Mit mindestens
einer Person kannst du nicht schreiben").
Damit ist der Treff die einzige Stelle, an der der Unterschied
zwischen „alle im Raum" und „alle im Team" überhaupt sichtbar wird
-- und zugleich die, an der die falsche Lesart wehtäte: Sie hätte
jeden Zuschauer geweckt.
ZWEI FRAGEN, UND DIE ZWEITE IST DIE WICHTIGERE:
1. Ruft der Modi wirklich sein Team?
2. Ruft ein Gast wirklich NICHTS? -- Ohne diese Prüfung stünde
Filipes „nur die modis, rechte hand, linke hand und dogfather"
bloss im Kommentar.
===================================================================== */
melde("\n" + "=== 7. Der Ruf ans Rudel geht durch die Tür ===");
{
/* Die Anmeldewand entscheidet am HOST, in welchem Haus sie steht.
Team Dogi gibt es nur auf crew.… -- ohne diesen Kopf käme jede
Anfrage auf der Agenturseite an, und die Rolle "modi" gibt es
dort gar nicht. `fetch` verwirft den Host-Kopf, deshalb node:http. */
const CREW = "crew.dogfather-universe.com";
function crewRuf(pfad, keks, methode = "GET", koerper = null) {
return new Promise((fertig, schief) => {
const daten = koerper === null ? null : Buffer.from(JSON.stringify(koerper));
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: {
Host: CREW, Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/`,
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
...(keks ? { Cookie: keks } : {}),
},
}, (antwort) => {
const st = [];
antwort.on("data", (x) => st.push(x));
antwort.on("end", () => {
const text = Buffer.concat(st).toString("utf8");
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
fertig({
code: antwort.statusCode, json: j, text,
keks: [].concat(antwort.headers["set-cookie"] || [])
.map((c) => c.split(";")[0]).join("; "),
});
});
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
}
/* Drei weitere Zugänge: ein Modi, die linke Hand und ein Gast. */
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
const schluessel2 = d2.prepare(
"SELECT wert FROM einstellungen WHERE schluessel = ?").get("code_kennung_schluessel").wert;
const anlegen2 = (name, rolle, code) => {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d2.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", schluessel2).update(code).digest("hex"), jetzt);
return d2.prepare("SELECT last_insert_rowid() AS id").get().id;
};
const idModi = anlegen2("Frida", "modi", "CODE-MODI-0001");
const idLinke = anlegen2("Lino", "linke", "CODE-LINK-0001");
const idGast = anlegen2("Zuschauer Zoe", "gast", "CODE-GAST-0001");
d2.close();
const aModi = await crewRuf("/workspace/api/anmelden", null, "POST",
{ rolle: "modi", code: "CODE-MODI-0001" });
const aLinke = await crewRuf("/workspace/api/anmelden", null, "POST",
{ rolle: "linke", code: "CODE-LINK-0001" });
/* EIN GAST BESTAETIGT SEIN ALTER. Ohne `alter_ok` kommt 400 --
gemessen, nicht vermutet; der erste Anlauf dieser Pruefung ist
genau daran haengengeblieben. Das Feld gilt NUR fuer die
Community-Kachel, fuer alle anderen Rollen verlangt der Server es
gar nicht erst. Dieselbe Stelle steht in pruef-treff. */
const aGast = await crewRuf("/workspace/api/anmelden", null, "POST",
{ rolle: "gast", code: "CODE-GAST-0001", alter_ok: true });
ok(!!aModi.keks && !!aLinke.keks && !!aGast.keks,
`Modi, linke Hand und ein Gast sind angemeldet (${aModi.code}/${aLinke.code}/${aGast.code})`);
/* DER TREFF FUELLT SICH, WENN MAN IHN AUFMACHT. treffAngleichen()
laeuft beim Abrufen der Gespraechsliste -- also einmal je Zugang. */
for (const k of [aModi.keks, aLinke.keks, aGast.keks]) {
await crewRuf("/workspace/api/chat/raeume", k);
}
const drin = new DatabaseSync(process.env.WORKSPACE_DB);
const treff = drin.prepare(
"SELECT id FROM chat_raeume WHERE art = 'kanal' AND kategorie = ?").get("treff")?.id;
/* DRITTER AUSGANG: Gibt es den Treff nicht, steht hier warum --
statt dass die folgenden Pruefungen an `undefined` arbeiten und
Fehler melden, die mit dem Rudel nichts zu tun haben. */
ok(Number.isInteger(treff), `der Treff steht (Raum ${treff})`);
const besetzung = Number.isInteger(treff) ? drin.prepare(
`SELECT t.person_id AS id, p.rolle FROM chat_teilnehmer t
JOIN personen p ON p.id = t.person_id
WHERE t.raum_id = ? AND t.raus_am IS NULL`).all(treff) : [];
const gaeste = besetzung.filter((x) => x.rolle === "gast").map((x) => x.id);
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Saesse kein Gast im Treff,
waere die Gegenprobe weiter unten leer und trotzdem gruen --
genau der Fehler, der am 01.09. dreimal gruen dastand. */
ok(gaeste.includes(idGast) && besetzung.length >= 3,
`im Treff sitzen ${besetzung.length} Leute, darunter ein Gast`);
/* WER ERWARTET WIRD, WIRD ABGELEITET -- nicht abgeschrieben. Eine
feste Liste hier waere beim naechsten Rollenzuschnitt still
falsch, und die Pruefung haette den Fehler mitgemacht. */
const TEAM = new Set(["admin", "hand", "linke", "modi"]);
const erwartet = besetzung
.filter((x) => TEAM.has(x.rolle) && x.id !== idModi)
.map((x) => x.id).sort((x, y) => x - y);
/* ---- 1. DER MODI RUFT ------------------------------------------ */
const ruf1 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
aModi.keks, "POST", { text: "@rudel bitte alle kurz herschauen" });
ok(ruf1.code === 201, `der Modi darf im Treff schreiben (${ruf1.code} ${ruf1.code === 201 ? "" : ruf1.text.slice(0, 90)})`);
ok(ruf1.json?.nachricht?.rudel === true,
" und die Nachricht weiss, dass sie das Rudel gerufen hat");
const nrId = ruf1.json?.nachricht?.id;
const gemeint = drin.prepare(
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ? ORDER BY person_id")
.all(nrId).map((x) => x.person_id).sort((x, y) => x - y);
ok(gemeint.length > 0 && JSON.stringify(gemeint) === JSON.stringify(erwartet),
`gerufen wird genau das Team im Raum (${JSON.stringify(gemeint)}, erwartet ${JSON.stringify(erwartet)})`);
/* DIE EIGENTLICHE AUSSAGE. "Rudel" heisst das Team -- nicht jeder im
Raum. Im Treff sitzt die Community; ein Ruf, der dort jeden
Zuschauer weckt, waere etwas anderes als der bestellte. */
ok(!gemeint.includes(idGast),
" der Gast im selben Raum aber NICHT — das Rudel ist das Team, nicht der Raum");
ok(!gemeint.includes(idModi),
" und der Rufer selbst auch nicht — man ruft nicht sich");
ok(gemeint.includes(idLinke),
" die linke Hand ist dabei — sie gehoert zum Team");
/* Die Spalte bleibt stehen -- sie ist es, aus der beim Lesen die
Hervorhebung entsteht. */
const gespeichert = drin.prepare(
"SELECT rudel FROM chat_nachrichten WHERE id = ?").get(nrId)?.rudel;
ok(gespeichert === 1, `und es steht an der Nachricht (rudel = ${gespeichert})`);
const verlauf = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`, aModi.keks);
const geladen = (verlauf.json?.nachrichten || []).find((x) => x.id === nrId);
ok(geladen?.rudel === true, "der Verlauf bringt es mit — auch nach dem Neuladen");
ok(verlauf.json?.darf_rudel === true, "und der Modi bekommt gesagt, dass er rufen darf");
ok(verlauf.json?.rudel_wort === RUDEL_WORT,
`das Wort kommt vom Server, nicht aus dem Browser (${verlauf.json?.rudel_wort})`);
/* ---- 2. GEGENPROBE: DER GAST RUFT INS LEERE --------------------- */
const ruf2 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
aGast.keks, "POST", { text: "@rudel und ihr alle mal" });
ok(ruf2.code === 201, `der Gast darf im Treff schreiben wie alle (${ruf2.code})`);
const nr2 = ruf2.json?.nachricht?.id;
ok(ruf2.json?.nachricht?.rudel !== true, " aber sein @rudel ruft niemanden");
const gemeint2 = drin.prepare(
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ?").all(nr2);
ok(gemeint2.length === 0,
` kein einziger Eintrag (${gemeint2.length}) — nur Team Dogi und DogFather duerfen rufen`);
const gespeichert2 = drin.prepare(
"SELECT rudel FROM chat_nachrichten WHERE id = ?").get(nr2)?.rudel;
ok(!gespeichert2, ` und an der Nachricht steht nichts (rudel = ${gespeichert2})`);
const verlauf2 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`, aGast.keks);
ok(verlauf2.json?.darf_rudel === false,
"dem Gast sagt der Server, dass er nicht rufen darf — der Vorschlag erscheint gar nicht erst");
/* ---- 3. EIN NAME WIRKT WEITERHIN FUER ALLE ---------------------- */
const ruf3 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
aGast.keks, "POST", { text: "@Frida schaust du mal?" });
const gemeint3 = drin.prepare(
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ?")
.all(ruf3.json?.nachricht?.id).map((x) => x.person_id);
ok(gemeint3.length === 1 && gemeint3[0] === idModi,
"wer nicht rufen darf, kann trotzdem einzeln ansprechen — das bleibt unberuehrt");
drin.close();
}
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);