Files
dogfather-universe/server/chat-erwaehnung.js
T
DogFatherGitandClaude Opus 5 28b0143166 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]>
2026-09-23 01:36:09 +02:00

231 lines
10 KiB
JavaScript

/* =====================================================================
WER IST MIT "@" GEMEINT? (20.09.2026)
Filipe: "ich will das man die leute mit @ markieren kann im chat."
---------------------------------------------------------------------
WARUM DAS EINE EIGENE DATEI IST
Die Auflösung "Text -> Menschen" wird an DREI Stellen gebraucht und
muss überall dasselbe ergeben:
1. beim Schreiben -- wer bekommt eine Benachrichtigung,
2. im Browser -- welcher Teil des Satzes wird hervorgehoben,
3. in der Prüfung -- ohne dafür einen Server hochzufahren.
Stünde sie in workspace-chat.js, müsste die Prüfung die halbe
Anwendung laden, um einen Satz zu zerlegen. Und der Browser hätte
ohnehin eine zweite, eigene Fassung -- also genau die Stelle, an der
zwei Rechnungen irgendwann auseinanderlaufen und die Seite jemanden
farbig hervorhebt, den der Server nie benachrichtigt hat.
DIE DATEI HAT DESHALB KEINE ABHÄNGIGKEITEN. Kein Datenbankzugriff,
kein Import. Herein kommt ein Text und eine Liste von Menschen,
heraus kommt, wer gemeint ist. Der Browser bekommt dieselbe Logik in
`chat.js` -- bewusst als kurze, gleichlautende Fassung, die
pruef-erwaehnung.mjs Zeile für Zeile gegen diese hier prüft.
---------------------------------------------------------------------
DIE REGELN, UND WARUM JEDE EINZELNE NÖTIG IST
(1) NUR WER IM RAUM IST.
"@Marina" in einem Gespräch, in dem Marina nicht sitzt, ist kein
Ansprechen, sondern ein Wort über sie. Sie bekäme sonst eine
Meldung zu einem Gespräch, das sie nicht öffnen darf -- eine
Benachrichtigung, die auf eine verschlossene Tür führt.
(2) DAS ZEICHEN DAVOR DARF KEIN BUCHSTABE UND KEINE ZIFFER SEIN.
Ohne diese Regel wird aus "[email protected]" ein
Ansprechen von Dogfather -- jede E-Mail-Adresse im Chat würde
jemanden anpiepsen. Gemessen an einer echten Adresse, siehe
Prüfung.
(3) DAS ZEICHEN DANACH DARF KEIN BUCHSTABE UND KEINE ZIFFER SEIN.
Sonst wäre "@Tili" in "@Tilikum" enthalten. Satzzeichen dagegen
schon: "@Tili, kannst du?" und "@Tili!" sind normale Sätze.
(4) DER LÄNGSTE PASSENDE NAME GEWINNT.
Gäbe es "Van" und "VanVan", träfe die kurze Fassung zuerst und
ließe "Van" stehen. Heute hat niemand einen Namen, der im
Namen eines anderen steckt -- aber das ist eine Tatsache von
heute, kein Gesetz, und sie ändert sich mit dem nächsten Zugang.
(5) GROSS UND KLEIN EGAL.
Wer "@vanvan" tippt, meint VanVan. Auf einer Handytastatur
entscheidet die Autokorrektur mit, nicht der Mensch.
(6) JEDE PERSON HÖCHSTENS EINMAL.
Dreimal "@Anna" in einem Satz ist trotzdem eine Ansprache.
---------------------------------------------------------------------
---------------------------------------------------------------------
"@RUDEL" -- ALLE AUF EINMAL (23.09.2026)
Hier stand bis heute: „KEIN @alle. Es wäre in fünf Minuten gebaut und
ist der zuverlässigste Weg, dass alle die Benachrichtigungen
abschalten -- und dann kommt auch die an, die wirklich für einen
bestimmten Menschen gedacht war. Wenn Filipe es ausdrücklich will,
gehört dazu eine Entscheidung, WER es benutzen darf; das ist eine
Frage an ihn, keine, die ich hier beantworte."
Filipe hat sie beantwortet, und zwar genau in dem Punkt, an dem sie
offen war: „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 EINSCHRÄNKUNG IST DIE ANTWORT AUF DIE SORGE. Nicht jeder kann
das Rudel rufen -- nur Team Dogi und DogFather. In einem Raum, in dem
auch Creator und Scouts sitzen, kann ein Creator „@rudel" schreiben,
und es passiert nichts: kein Ruf, keine Hervorhebung, kein Eintrag.
WER DARF, ENTSCHEIDET DIESE DATEI NICHT. Sie hat keine
Abhängigkeiten und soll keine bekommen (siehe oben). Der Aufrufer
sagt es ihr: `erwaehnungenFinden(text, leute, { rudel: true })`. Im
Server kommt das aus `istTeamDogi(person)`, der einen Stelle des
Hauses dafür.
DAS SCHLÜSSELWORT GEWINNT GEGEN EINEN MENSCHEN, DER "RUDEL" HEISST.
Heute heißt niemand so -- aber das ist eine Tatsache von heute, kein
Gesetz (dieselbe Überlegung wie bei Regel 4). Andersherum wäre der
Schaden größer: Ein einziger Zugang mit diesem Namen würde den Ruf
ans ganze Team stillschweigend abschalten, und niemand fände je
heraus, warum. So herum kostet es nichts -- wer „Rudel" heißt, ist im
Rudel ohnehin dabei und bekommt seine Meldung.
---------------------------------------------------------------------
AUFRUF (auch aus einer Prüfung heraus):
import { erwaehnungenFinden } from "./chat-erwaehnung.js";
===================================================================== */
/** Das Wort, das alle meint. Klein geschrieben -- verglichen wird
* ohnehin kleingeschrieben (Regel 5). */
export const RUDEL_WORT = "rudel";
/** Ein Zeichen, das einen Namen fortsetzen könnte.
*
* BUCHSTABE ODER ZIFFER, und zwar auch mit Umlaut oder Akzent:
* `\p{L}` statt `[a-z]`. Mit `[a-zA-Z0-9]` wäre "@Jose" in "@José"
* ein Treffer gewesen -- das "é" hätte als Grenze gezählt. */
const WORTZEICHEN = /[\p{L}\p{N}_]/u;
/** Steht an dieser Stelle ein Wortzeichen?
* Außerhalb des Textes (Anfang/Ende) ist die Antwort NEIN -- ein
* Satzanfang ist eine Grenze wie jede andere. */
function wortzeichenBei(text, i) {
if (i < 0 || i >= text.length) return false;
return WORTZEICHEN.test(text[i]);
}
/**
* Wer ist in diesem Text mit "@" angesprochen?
*
* @param {string} text Die geschriebene Nachricht.
* @param {Array<{id:number,name:string}>} leute
* Die Teilnehmer DIESES Raums. Wer nicht darin steht, kann
* nicht gemeint sein -- siehe Regel (1) oben.
* @param {{rudel?: boolean}} [wie]
* `rudel: true` heisst: WER DAS HIER SCHREIBT, darf das ganze
* Rudel rufen. Vorgabe ist `false` -- wer nichts sagt, darf es
* nicht. Das ist Absicht: Eine vergessene Angabe soll zu wenig
* koennen, nicht zu viel.
* @returns {Array<{id:number|null, name:string, von:number, bis:number, rudel?:boolean}>}
* Je Treffer die Person und die Stelle im Text (`von` bis
* `bis`, ohne das "@"-Zeichen gerechnet wie `slice`). Die
* Stellen braucht der Browser zum Hervorheben; der Server
* benutzt nur `id`. Sortiert nach Vorkommen im Text.
* Beim Rudel ist `id` null und `rudel` true -- es ist keine
* Person, sondern alle.
*/
export function erwaehnungenFinden(text, leute, wie = {}) {
const darfRudel = wie?.rudel === true;
const satz = String(text || "");
if (!satz.includes("@")) return [];
/* REGEL (4): der längste Name zuerst. Einmal sortiert statt bei
jedem Treffer neu -- und auf einer Kopie, damit die Liste des
Aufrufers unberührt bleibt. */
const nachLaenge = (Array.isArray(leute) ? leute.slice() : [])
.filter((p) => p && p.id != null && String(p.name || "").trim())
.sort((a, b) => String(b.name).length - String(a.name).length);
if (!nachLaenge.length) return [];
const klein = satz.toLowerCase();
const treffer = [];
const schonDa = new Set(); // Regel (6)
for (let i = klein.indexOf("@"); i !== -1; i = klein.indexOf("@", i + 1)) {
/* REGEL (2): Was steht vor dem "@"? */
if (wortzeichenBei(satz, i - 1)) continue;
/* DAS RUDEL WIRD ZUERST GEPRUEFT -- und genau das ist die
Entscheidung aus dem Kopfkommentar: Das Schluesselwort gewinnt
gegen einen Menschen, der "Rudel" heisst. Die Regeln (2), (3)
und (5) gelten fuer es genauso wie fuer einen Namen, sonst
riefe eine Mailadresse "[email protected]" das ganze Team. */
if (darfRudel && klein.startsWith(RUDEL_WORT, i + 1)
&& !wortzeichenBei(satz, i + 1 + RUDEL_WORT.length)) {
treffer.push({ id: null, rudel: true, name: RUDEL_WORT,
von: i, bis: i + 1 + RUDEL_WORT.length });
continue;
}
for (const person of nachLaenge) {
const name = String(person.name);
const nk = name.toLowerCase();
if (!klein.startsWith(nk, i + 1)) continue;
/* REGEL (3): Was steht hinter dem Namen? */
if (wortzeichenBei(satz, i + 1 + name.length)) continue;
/* Die Stelle wird IMMER gemeldet, auch wenn dieselbe Person
schon einmal vorkam: Der Browser soll jedes "@Anna" im Satz
hervorheben, nicht nur das erste. Nur die ID-Liste für die
Benachrichtigung wird entdoppelt -- das macht `ideeVon()`
weiter unten. */
treffer.push({ id: person.id, name, von: i, bis: i + 1 + name.length });
schonDa.add(person.id);
break; // Regel (4): der Längste hat gewonnen
}
}
return treffer;
}
/**
* Nur die Personennummern, jede einmal -- das, was der Server zum
* Benachrichtigen braucht.
*
* ALS EIGENE FUNKTION, damit an der Aufrufstelle nicht steht
* `[...new Set(x.map(t => t.id))]`. Diese Zeile wäre an drei Orten
* gleich zu schreiben, und an einem davon irgendwann anders.
*
* DAS RUDEL STEHT HIER NICHT DRIN, und das ist Absicht. "@rudel" ist
* keine Liste von Namen im Satz, sondern eine Gruppe -- und WER zu ihr
* gehört, ist eine Frage des Hauses, nicht des Textes. Diese Datei hat
* keine Abhängigkeiten und kann sie nicht beantworten (sie sähe einen
* Raum voller `{id, name}` und wüsste nichts von Rollen). Sie sagt nur,
* DASS gerufen wurde -- siehe rudelGerufen() darunter.
*/
export function erwaehnteIds(text, leute, wie = {}) {
return [...new Set(erwaehnungenFinden(text, leute, wie)
.filter((t) => t.id != null)
.map((t) => t.id))];
}
/** Wurde in diesem Text das ganze Rudel gerufen?
*
* ALS EIGENE FRAGE, weil der Server sie getrennt beantworten muss:
* Die Nachricht merkt sich, DASS das Rudel gerufen wurde (Spalte
* `rudel`), und daraus entsteht spaeter die Hervorhebung beim Lesen
* und der Wortlaut der Meldung. Aus der ID-Liste liesse sich das
* nicht ablesen -- in einem Raum zu zweit sieht "alle" genauso aus
* wie "einer".
*
* WEN ES DANN TRIFFT, steht in workspace-chat.js: die Leute im Raum,
* die zu Team Dogi gehoeren (istTeamDogi). NICHT jeder im Raum -- im
* Treff sitzt die Community, und "Rudel" heisst das Team. Ein Ruf,
* der zweihundert Zuschauer weckt, waere etwas anderes als der, den
* Filipe bestellt hat.
*/
export function rudelGerufen(text, leute, wie = {}) {
return erwaehnungenFinden(text, leute, wie).some((t) => t.rudel === true);
}