From d677ef4b3ee4315a8fb61d85382b3bb2a739c3cc Mon Sep 17 00:00:00 2001 From: Elton Turing Date: Tue, 1 Sep 2026 20:50:24 +0200 Subject: [PATCH] Anwesenheit anzeigen, und der Boden traegt mit 15 Sekunden Andreas: "waehrend mmnotify per ws angemeldet ist, kann es dann den status des agenten auf online setzen?" Ja -- und hive hatte das Muster schon fertig (channels/team.py): abwesend beim Verbinden, gruen waehrend der Arbeit, wieder abwesend nach Ruhe, offline beim Beenden. Ein Agent ohne Status sieht aus, als sei niemand da; einer, der rund um die Uhr gruen leuchtet, sagt genauso wenig. Beim Testen fiel ein eigener Fehler auf: Der Boden setzte den Agenten auf gruen. Routinemaessiges Nachsehen ist aber keine Anwesenheit -- bei Boden alle 20 s und Frist 25 s blieb er dauerhaft gruen. Jetzt macht nur ein echtes Ereignis gruen. Und zur Verzoegerung, die Andreas zu Recht nicht hinnehmen wollte: Statt weiter zu raten, gemessen. Der eigene Beitrag loest sehr wohl ein posted aus, ein fremder nie -- die Verbindung hat also die Kanal-Abos, es fehlen nur fremde Ereignisse. Ausgeschlossen: authentication_challenge, Token im Authorization-Header, ein echter Bot-Account statt Nutzer-Token, und das Sitzungs-Cookie wie im Browser. Alle vier gleich. Das Mattermost-Forum beschreibt dasselbe, dort auch mit System-Admin-Token, ohne Loesung. Konsequenz: floor_seconds ist der Weg, nicht der Notnagel, und steht deshalb auf 15 s statt 60. Ein Durchgang kostet einen API-Aufruf -- billig genug, um es oft zu tun. --- README.md | 50 ++++++++++++++++++++---- main.go | 111 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 145 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a212e56..9189f2b 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ Die Datei enthält ein Token und gehört mit `0600` dem Dienstbenutzer. | `on_mention` | Befehl bei Erwähnung (`@name`) im Kanal | | `on_direct` | Befehl bei Direktnachricht; leer = `on_mention` | | `debounce_seconds` | Mindestabstand zwischen zwei Läufen (Standard 5) | -| `floor_seconds` | Nachsehen auch ohne Ereignis (Standard 60, `0` schaltet ab) | +| `floor_seconds` | Nachsehen auch ohne Ereignis (Standard 15, `0` schaltet ab) | | `ignore_users` | Absender, die nie wecken | +| `away_after_seconds` | Wann der grüne Punkt auf abwesend zurückfällt (Standard 120) | Der Befehl bekommt die Ereignisdaten über die Umgebung: `MM_TRIGGER` (`event` oder `floor`), `MM_CHANNEL`, `MM_CHANNEL_NAME`, `MM_CHANNEL_TYPE`, @@ -47,17 +48,52 @@ ist dort begründet: *„A stream that dies quietly must make us LATE, never blind."* **Hier ist der Boden nicht nur Vorsicht.** Gemessen am 2026-09-01 gegen -Mattermost 11.7.10: Die Verbindung kommt zustande, die Anmeldung wird mit -`status: OK` quittiert, danach kommen `hello` und `status_change` — aber -**kein `posted`**, auch wenn ein anderes Konto in einen Kanal schreibt, in dem -der Agent nachweislich Mitglied ist. Ursache offen; Verdacht sind Broadcasts an -Verbindungen, die über ein Personal Access Token angemeldet sind. Mit dem Boden -arbeitet der Dienst trotzdem, er ist dann nur langsamer. +Mattermost 11.7.10: + +| | | +|---|---| +| eigener Beitrag | `posted` kommt an | +| Beitrag eines anderen Kontos | kommt **nie** an | + +Die Verbindung ist also voll funktionsfähig und hat die Kanal-Abos — es fehlen +ausschließlich die Ereignisse fremder Konten. Ausgeschlossen wurden: +`authentication_challenge`, Token im Authorization-Header beim Handshake, ein +echter Bot-Account statt eines Nutzers mit Token, und das Sitzungs-Cookie +`MMAUTHTOKEN` wie im Browser. Alle vier verhalten sich gleich. + +Das Verhalten ist [im Mattermost-Forum +beschrieben](https://forum.mattermost.com/t/websocket-does-not-provide-posted-events-from-other-accounts/14876) +und tritt dort auch mit einem System-Admin-Token auf; eine Lösung steht nicht +dabei. + +Deshalb ist `floor_seconds` der Weg und nicht der Notnagel, und der Standard +ist kurz: **15 Sekunden**. Ein Durchgang kostet einen API-Aufruf — billig genug, +um es oft zu tun. Wer den Stream repariert bekommt, kann ihn hochsetzen. Daraus folgt die zweite Entscheidung: **mmnotify liest den Beitrag nicht aus dem Ereignis.** Es klopft an; was zu tun ist, entscheidet der Agent, wenn er nachsieht. Zwei Leser wären zwei Antworten. +## Anwesenheit + +Solange mmnotify verbunden ist, zeigt der Punkt neben dem Namen, woran man ist: + +- **abwesend**, sobald die Verbindung steht — da, aber untätig +- **grün**, während der Befehl wegen einer Erwähnung läuft +- **abwesend** wieder, wenn es `away_after_seconds` still war +- **offline** beim Beenden, statt einen grünen Punkt zurückzulassen, hinter dem + niemand mehr sitzt + +Das routinemäßige Nachsehen des Bodens macht ausdrücklich **nicht** grün. Läuft +der Boden häufiger als die Away-Frist, leuchtete der Agent sonst rund um die +Uhr — und ein Punkt, der immer grün ist, sagt genauso wenig wie gar keiner. +(Beim Testen genau so passiert: Boden alle 20 s, Frist 25 s, nie abwesend.) + +Ein Status ist eine Höflichkeit gegenüber dem, der in die Seitenleiste schaut. +Weigert sich der Server, wird das protokolliert und weitergearbeitet: Ein Agent, +der aufhört zu antworten, weil er einen Punkt nicht einfärben konnte, wäre +absurd. + ## Robustheit - Wiederaufbau mit wachsendem Abstand (1 s bis 60 s). Ein Server, der neu diff --git a/main.go b/main.go index 4bb077c..e8fb42c 100644 --- a/main.go +++ b/main.go @@ -17,12 +17,28 @@ // Latenz statt Nachrichten. // // Das ist hier keine Vorsichtsmaßnahme, sondern nötig. Gemessen am 2026-09-01 -// gegen team.42i.org (Mattermost 11.7.10): Die Verbindung kommt zustande, die -// Anmeldung wird mit "status: OK" quittiert, und danach kommen `hello` und -// `status_change` — aber KEIN `posted`, auch wenn ein anderes Konto in einen -// Kanal schreibt, in dem der Agent nachweislich Mitglied ist. Ursache noch -// offen (Verdacht: Broadcasts an Verbindungen, die über ein Personal Access -// Token angemeldet sind). Mit dem Boden funktioniert der Dienst trotzdem. +// gegen Mattermost 11.7.10: +// +// eigener Beitrag -> `posted` kommt an +// Beitrag eines anderen -> kommt NIE an +// +// Die Verbindung ist also voll funktionsfähig und hat die Kanal-Abos; es +// fehlen ausschließlich die Ereignisse fremder Konten. Vier Varianten wurden +// ausgeschlossen: Anmeldung per `authentication_challenge`, Token im +// Authorization-Header beim Handshake, ein echter Bot-Account (statt Nutzer +// mit Token) und das Sitzungs-Cookie MMAUTHTOKEN wie im Browser. Alle vier +// verhalten sich gleich. +// +// Das Verhalten ist bekannt und nicht auf uns beschränkt: +// https://forum.mattermost.com/t/websocket-does-not-provide-posted-events-from-other-accounts/14876 +// Dort tritt es auch mit einem System-Admin-Token auf, und eine Lösung steht +// nicht dabei. +// +// Konsequenz für den Betrieb: floor_seconds ist der Weg, nicht der Notnagel, +// und der Standard ist deshalb bewusst kurz (15 s). Ein Durchgang kostet einen +// API-Aufruf — das ist billig genug, um es oft zu tun, und 15 Sekunden fühlen +// sich noch wie eine Antwort an. Wer den Stream repariert bekommt, kann den +// Boden hochsetzen; bis dahin trägt er. // // Deshalb gilt auch: mmnotify liest den Beitrag NICHT aus dem Ereignis. Es // klopft nur an; was zu tun ist, entscheidet der Agent, wenn er nachsieht. @@ -65,12 +81,16 @@ type Config struct { // Minute fünf Agentenläufe auslösen. DebounceSeconds int `json:"debounce_seconds"` // Der Boden unter dem Stream: Auch ohne Ereignis wird nach so vielen - // Sekunden nachgesehen. Standard 60. Auf 0 zu setzen heißt, sich allein auf - // den Stream zu verlassen — siehe die Messung im Kopf dieser Datei. + // Sekunden nachgesehen. Standard 15 — kurz, weil der Stream fremde Beiträge + // nicht liefert (siehe Kopf dieser Datei). Auf 0 zu setzen heißt, sich allein + // auf den Stream zu verlassen; das hört heute nur die eigenen Beiträge. FloorSeconds int `json:"floor_seconds"` // Antwortet der Agent auf eigene Beiträge, entsteht eine Schleife. Der // eigene Benutzer wird beim Start ermittelt und hier nicht gebraucht. IgnoreUsers []string `json:"ignore_users"` + // Nach wie vielen Sekunden ohne Arbeit der Punkt von grün auf abwesend + // zurückfällt. Standard 120, 0 schaltet die Statusanzeige ganz ab. + AwayAfterSeconds int `json:"away_after_seconds"` } type Ereignis struct { @@ -88,6 +108,9 @@ type Melder struct { letzter time.Time laufMu sync.Mutex gesehen map[string]bool + statusMu sync.Mutex + status string // zuletzt gesetzter Status; nur über statusSetzen anfassen + aktiv time.Time } func main() { @@ -110,7 +133,10 @@ func main() { cfg.DebounceSeconds = 5 } if cfg.FloorSeconds == 0 { - cfg.FloorSeconds = 60 + cfg.FloorSeconds = 15 + } + if cfg.AwayAfterSeconds == 0 { + cfg.AwayAfterSeconds = 120 } m := &Melder{cfg: cfg, namen: map[string]string{}, gesehen: map[string]bool{}} @@ -126,12 +152,18 @@ func main() { go func() { <-schluss log.Print("Beende auf Signal.") + // Offline sagen, statt einen grünen Punkt zurückzulassen, hinter dem + // niemand mehr sitzt. + m.statusSetzen("offline") os.Exit(0) }() if cfg.FloorSeconds > 0 { go m.boden() } + if cfg.AwayAfterSeconds > 0 { + go m.abwesendWennStill() + } m.verbindungsschleife() } @@ -206,6 +238,8 @@ func (m *Melder) lauschen() error { } }() + // Da, aber untätig. Grün ab der ersten Sekunde würde nichts aussagen. + m.statusSetzen("away") log.Print("Verbunden, warte auf Ereignisse.") for { var ev Ereignis @@ -296,6 +330,15 @@ func (m *Melder) wecken(befehl string, umgebung map[string]string) { } m.letzter = time.Now() + // Grün heißt: Es passiert gerade etwas, das jemanden angeht. Das routinemäßige + // Nachsehen des Bodens gehört ausdrücklich NICHT dazu -- läuft er häufiger als + // die Away-Frist, leuchtete der Agent sonst rund um die Uhr, und ein Punkt, + // der immer grün ist, sagt genauso wenig wie gar keiner (beim Testen am + // 2026-09-01 genau so passiert: Boden alle 20 s, Frist 25 s, nie abwesend). + if umgebung["MM_TRIGGER"] != "floor" { + m.aktiv = time.Now() + m.statusSetzen("online") + } log.Printf("Wecke wegen Beitrag von %s in %s", umgebung["MM_SENDER"], umgebung["MM_CHANNEL_NAME"]) cmd := exec.Command("/bin/sh", "-c", befehl) cmd.Env = os.Environ() @@ -323,6 +366,56 @@ func (m *Melder) boden() { } } +// status sagt grün oder abwesend — und darf dabei nie etwas kaputtmachen. +// +// Der Punkt neben dem Namen ist eine Höflichkeit gegenüber dem, der in die +// Seitenleiste schaut. Weigert sich der Server, muss die Arbeit trotzdem +// stattfinden: Ein Agent, der aufhört zu antworten, weil er einen Punkt nicht +// einfärben konnte, wäre absurd (übernommen aus hive, channels/team.py). +// +// Warum überhaupt zwei Zustände: Ein Agent ganz ohne Status sieht aus, als sei +// niemand da. Einer, der rund um die Uhr grün leuchtet, sagt genauso wenig. +// Beides ist schlechter als die Wahrheit — da, und das ist der Zeitpunkt, an +// dem zuletzt etwas geschah. +func (m *Melder) statusSetzen(neu string) { + m.statusMu.Lock() + if m.cfg.AwayAfterSeconds < 0 || m.status == neu { + m.statusMu.Unlock() + return + } + m.statusMu.Unlock() + koerper, _ := json.Marshal(map[string]string{"user_id": m.ich, "status": neu}) + req, _ := http.NewRequest("PUT", m.cfg.URL+"/api/v4/users/"+m.ich+"/status", + strings.NewReader(string(koerper))) + req.Header.Set("Authorization", "Bearer "+m.cfg.Token) + req.Header.Set("Content-Type", "application/json") + r, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("Status %q nicht gesetzt: %v", neu, err) + return + } + defer r.Body.Close() + if r.StatusCode != 200 { + log.Printf("Status %q nicht gesetzt: HTTP %d", neu, r.StatusCode) + return + } + m.statusMu.Lock() + m.status = neu + m.statusMu.Unlock() +} + +// abwesendWennStill lässt das Grün verblassen, sobald es still geworden ist. +func (m *Melder) abwesendWennStill() { + t := time.NewTicker(15 * time.Second) + defer t.Stop() + for range t.C { + if !m.aktiv.IsZero() && + time.Since(m.aktiv) > time.Duration(m.cfg.AwayAfterSeconds)*time.Second { + m.statusSetzen("away") + } + } +} + func (m *Melder) ichErmitteln() error { req, _ := http.NewRequest("GET", m.cfg.URL+"/api/v4/users/me", nil) req.Header.Set("Authorization", "Bearer "+m.cfg.Token)