diff --git a/plugins/humhub/.claude-plugin/plugin.json b/plugins/humhub/.claude-plugin/plugin.json index a790f3d..1742541 100644 --- a/plugins/humhub/.claude-plugin/plugin.json +++ b/plugins/humhub/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "humhub", - "version": "0.3.1", + "version": "0.4.0", "description": "HumHub via REST-API: Wiki-Seiten, Posts, Umfragen und Kommentare auflisten, anzeigen, anlegen und aktualisieren (humhub.py).", "author": { "name": "inmedias.it" diff --git a/plugins/humhub/skills/humhub/SKILL.md b/plugins/humhub/skills/humhub/SKILL.md index 2123d27..c6f8f60 100644 --- a/plugins/humhub/skills/humhub/SKILL.md +++ b/plugins/humhub/skills/humhub/SKILL.md @@ -94,13 +94,17 @@ und Wiki-Seiten. ### Direktnachrichten ```bash -./humhub.py mail-send "" [datei|-] +./humhub.py mail-send "" [datei|-] # Probelauf +./humhub.py mail-send "" [datei|-] --send # senden ./humhub.py mail-send https://humhub.inmedias.it/u/serge/ "Betreff" entwurf.md -./humhub.py mail-send jan.stobbe,26 "Betreff" - +./humhub.py mail-send jan.stobbe,26 "Betreff" - --send ./humhub.py mail-reply [datei|-] +./humhub.py mail-user-remove ``` Empfänger sind Benutzername, Profil-URL oder numerische ID; mehrere mit Komma. +**Ohne `--send` wird nichts gesendet**, sondern nur aufgelöst und angezeigt, +wer die Nachricht bekäme — der Probelauf ist der Default. **`recipient` will GUIDs, keine User-IDs.** `mail-send` löst jede Angabe deshalb bis zur GUID auf (`/user/` liefert sie, die Benutzerliste je nach @@ -113,6 +117,27 @@ falsche Richtung. (`POST /mail//entry`). Die ID steht in der URL der Konversation: `…/mail/mail/index?id=35`. +**Eine Fehlzustellung lässt sich nicht löschen.** `DELETE /mail/` gibt es +in dieser API-Version nicht (404, Route unbekannt), und über die Oberfläche +kommt man auch nicht dran. Das einzige Gegenmittel ist +`mail-user-remove`, das einen Teilnehmer wieder aus der Konversation nimmt +(`DELETE /mail//user/`). Ob der Empfänger vorher schon eine +Benachrichtigung bekommen hat, bleibt dabei offen. + +**Deshalb prüft `mail-send` nach dem Versand die Teilnehmerliste.** Eine +falsche Kennung in `recipient` führt nicht verlässlich zu einer +Fehlermeldung: am 25.09.2026 hat die API einen Versand mit numerischen IDs +mit `HTTP 200` quittiert und die Nachricht an einen **anderen** Benutzer +zugestellt. Der Befehl gibt deshalb aus, wer die Konversation tatsächlich +bekommen hat, und nennt bei einer Abweichung das `mail-user-remove`-Kommando. +Die Teilnehmerliste kann direkt nach dem Anlegen unvollständig sein — im +Zweifel noch einmal nachsehen, bevor man daraus einen Schluss zieht. + +Zwei Routen, zwei Kennungen: `POST /mail` will die **GUID**, +`DELETE /mail//user/` die **numerische ID**. Beides ist im +Skript gekapselt, `mail-user-remove` nimmt wie alle Befehle Benutzername, +Profil-URL oder ID. + **Slug ≠ Benutzername.** Zur Profil-URL `/u/serge/` gehört der Benutzername `Serge`, und `get-by-username` unterscheidet Groß-/Kleinschreibung. Wer den Slug einsetzt, bekommt sonst `User not found!`. `mail-send` fängt das ab: erst diff --git a/plugins/humhub/skills/humhub/humhub.py b/plugins/humhub/skills/humhub/humhub.py index 5d03861..821851a 100755 --- a/plugins/humhub/skills/humhub/humhub.py +++ b/plugins/humhub/skills/humhub/humhub.py @@ -362,13 +362,8 @@ def user_guid(session: requests.Session, user: dict) -> str: return str(api_get(session, f"/user/{uid}")["guid"]) -def resolve_user(session: requests.Session, value: str) -> str: - """Resolve a user ID, username, or profile URL (…/u//) to a user GUID. - - **GUID, nicht ID.** `POST /mail` nimmt als `recipient` ausschließlich - GUIDs an; mit numerischen IDs antwortet die API `400 Validation failed` - und „Du kannst keine Nachricht ohne Empfänger senden!" — eine Meldung, - die nach einem leeren Feld klingt, obwohl das Feld gefüllt ist. +def resolve_user_info(session: requests.Session, value: str) -> dict: + """Benutzerdatensatz zu numerischer ID, Benutzername oder Profil-URL. Slug und Benutzername sind nicht dasselbe: zu https://…/u/serge/ gehört der Benutzername "Serge". `get-by-username` kennt nur den Benutzernamen und @@ -377,7 +372,7 @@ def resolve_user(session: requests.Session, value: str) -> str: """ value = value.strip() if value.isdigit(): - return user_guid(session, {"id": int(value)}) + return api_get(session, f"/user/{int(value)}") name = value if "/" in value: teile = [t for t in urlparse(value).path.split("/") if t] @@ -387,7 +382,7 @@ def resolve_user(session: requests.Session, value: str) -> str: r = session.get(f"{BASE_URL}/user/get-by-username", params={"username": name}) if r.ok: - return user_guid(session, r.json()) + return r.json() page = 1 while True: @@ -398,26 +393,107 @@ def resolve_user(session: requests.Session, value: str) -> str: slug_teile = [t for t in urlparse(u.get("url", "")).path.split("/") if t] slug = slug_teile[-1] if slug_teile else "" if name.lower() in (username.lower(), slug.lower()): - return user_guid(session, u) + return u if len(results) < 50: break page += 1 sys.exit(f"Error: Benutzer {value!r} nicht gefunden") +def resolve_user(session: requests.Session, value: str) -> str: + """Resolve a user ID, username, or profile URL (…/u//) to a user GUID. + + **GUID, nicht ID.** `POST /mail` nimmt als `recipient` ausschließlich + GUIDs an; mit numerischen IDs antwortet die API `400 Validation failed` + und „Du kannst keine Nachricht ohne Empfänger senden!" — eine Meldung, + die nach einem leeren Feld klingt, obwohl das Feld gefüllt ist. + """ + return user_guid(session, resolve_user_info(session, value)) + + +def mail_teilnehmer(session: requests.Session, conv_id: int) -> list[dict]: + """Teilnehmer einer Konversation. Leere Liste, wenn nicht lesbar.""" + r = session.get(f"{BASE_URL}/mail/{conv_id}/users") + if not r.ok: + return [] + daten = r.json() + return daten if isinstance(daten, list) else daten.get("results", []) + + +def eigene_guid(session: requests.Session) -> str: + """GUID des eigenen Kontos, leer wenn nicht ermittelbar.""" + r = session.get(f"{BASE_URL}/user/me") + return str(r.json().get("guid", "")) if r.ok else "" + + def cmd_mail_send(args: argparse.Namespace) -> None: session, _ = get_session() - guids = [resolve_user(session, teil) for teil in args.to.split(",") if teil.strip()] - if not guids: + leute = [resolve_user_info(session, teil) + for teil in args.to.split(",") if teil.strip()] + if not leute: sys.exit("Error: kein Empfänger angegeben") + guids = [user_guid(session, u) for u in leute] + namen = {g: u.get("display_name", "?") for g, u in zip(guids, leute)} + for g in guids: + print(f"Empfänger: {namen[g]} ({g})") + if not args.send: + print("PROBELAUF — nichts gesendet. Mit --send abschicken.") + return + message = read_file(args.file) data = api_post(session, "/mail", { "title": args.title, "message": message, "recipient": guids, }) - empfaenger = ", ".join(guids) - print(f"Nachricht gesendet (Konversation {data.get('id')}) an {empfaenger}") + conv_id = data.get("id") + print(f"Nachricht gesendet (Konversation {conv_id})") + + # Gegenprobe. Am 25.09.2026 hat die API eine Nachricht mit numerischen IDs + # in `recipient` mit HTTP 200 quittiert und sie an einen *anderen* + # Benutzer zugestellt — sie meldet einen falschen Empfaenger also nicht + # zwingend als Fehler. Deshalb nach jedem Versand nachsehen, wer die + # Konversation tatsaechlich bekommen hat. Loeschen laesst sie sich nicht + # (`DELETE /mail/` gibt es nicht), nur Teilnehmer entfernen. + teilnehmer = mail_teilnehmer(session, conv_id) + if not teilnehmer: + print("ACHTUNG: Teilnehmerliste nicht lesbar — Zustellung nicht geprüft.") + return + print("Teilnehmer laut Server:") + for u in teilnehmer: + print(f" {u.get('display_name')} ({u.get('guid')})") + drin = {str(u.get("guid")) for u in teilnehmer} + ich = eigene_guid(session) + fehlend = [g for g in guids if g not in drin] + fremd = [u for u in teilnehmer + if str(u.get("guid")) not in guids and str(u.get("guid")) != ich] + if fehlend: + print("ACHTUNG: nicht zugestellt an " + + ", ".join(f"{namen[g]} ({g})" for g in fehlend)) + if fremd: + print("ACHTUNG: unerwartete Teilnehmer — entfernen mit:") + for u in fremd: + print(f" ./humhub.py mail-user-remove {conv_id} {u.get('id')}") + + +def cmd_mail_user_remove(args: argparse.Namespace) -> None: + """Nimmt einen Teilnehmer aus einer Konversation. + + Der einzige Weg, eine Fehlzustellung zurueckzunehmen: die Konversation + selbst laesst sich weder ueber die API noch ueber die Oberflaeche + loeschen. Anders als beim Versand will diese Route die **numerische ID**, + nicht die GUID. + """ + session, _ = get_session() + user = resolve_user_info(session, args.user) + uid = user.get("id") + r = session.delete(f"{BASE_URL}/mail/{args.id}/user/{uid}") + if not r.ok: + sys.exit(f"Error {r.status_code}: {r.text[:200]}") + print(f"{user.get('display_name')} (ID {uid}) aus Konversation " + f"{args.id} entfernt. Verbleibend:") + for u in mail_teilnehmer(session, args.id): + print(f" {u.get('display_name')} ({u.get('guid')})") def cmd_mail_reply(args: argparse.Namespace) -> None: @@ -605,8 +681,17 @@ def main() -> None: p.add_argument("to", help="Benutzername, Profil-URL oder ID; mehrere mit Komma") p.add_argument("title", help="Betreff") p.add_argument("file", help="Markdown-Datei oder '-' für stdin") + p.add_argument("--send", action="store_true", + help="wirklich senden (ohne: Probelauf mit Empfängernamen)") p.set_defaults(func=cmd_mail_send) + # mail-user-remove + p = sub.add_parser("mail-user-remove", + help="Teilnehmer aus einer Konversation entfernen (Fehlzustellung)") + p.add_argument("id", type=int, help="Konversations-ID") + p.add_argument("user", help="Benutzername, Profil-URL oder ID") + p.set_defaults(func=cmd_mail_user_remove) + # mail-reply p = sub.add_parser("mail-reply", help="In einer bestehenden Konversation antworten") p.add_argument("id", type=int, help="Konversations-ID (aus /mail/mail/index?id=NN)")