Author SHA1 Message Date
jhsandClaude Opus 5 2bfc62bcb8 humhub: in bestehenden Konversationen antworten (mail-reply)
mail-send legt immer eine neue Konversation an; fuer eine Antwort braucht es
POST /mail/<id>/entry. Die ID steht in der URL der Konversation.

Holt den Verlauf nicht ab, sondern prueft nur die Erreichbarkeit — fremde
Nachrichten gehoeren nicht ungefragt in einen Modellkontext.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 08:24:23 +02:00
jhsandClaude Opus 5 e00395fc64 humhub: Direktnachrichten senden (mail-send)
Die REST-API kann das Mail-Modul, das Skill bisher nicht — Direktnachrichten
mussten von Hand in der Weboberflaeche geschrieben werden.

Der Stolperstein ist die Empfaengeraufloesung: Profil-Slug und Benutzername
sind nicht dasselbe (zu /u/serge/ gehoert "Serge"), und get-by-username
unterscheidet Gross-/Kleinschreibung. resolve_user() nimmt deshalb ID,
Benutzername oder Profil-URL und faellt auf einen Durchlauf der Benutzerliste
zurueck, der beides case-insensitive vergleicht.

Kein Befehl zum Lesen von Konversationen: fremde Nachrichten gehoeren nicht
ungefragt in einen Modellkontext.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 08:56:09 +02:00
jhsandClaude Fable 5 4a20da2ed2 humhub: comment-list/comment-create (POST /comment via Content-ID/Perma-URL)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 07:28:01 +02:00
sko 2a02dac74e Merge pull request 'humhub: Umfragen anlegen und auflisten (poll-create / poll-list)' (#6) from feat/humhub-polls into main 2026-08-31 09:24:44 +02:00
3 changed files with 175 additions and 2 deletions

No files matched your search

+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "humhub", "name": "humhub",
"version": "0.2.0", "version": "0.3.0",
"description": "HumHub via REST-API: Wiki-Seiten, Posts und Umfragen auflisten, anzeigen, anlegen und aktualisieren (humhub.py).", "description": "HumHub via REST-API: Wiki-Seiten, Posts, Umfragen und Kommentare auflisten, anzeigen, anlegen und aktualisieren (humhub.py).",
"author": { "author": {
"name": "inmedias.it" "name": "inmedias.it"
} }
+37
View File
@@ -80,6 +80,43 @@ und nicht Komfort — ohne Begrenzung antwortet die Instanz ab ~25 Umfragen
ebenfalls mit 500. Ein fehlgeschlagener POST legt nichts an; vor einem zweiten ebenfalls mit 500. Ein fehlgeschlagener POST legt nichts an; vor einem zweiten
Versuch trotzdem mit `poll-list` prüfen. Versuch trotzdem mit `poll-list` prüfen.
### Kommentare
```bash
./humhub.py comment-list <content-id|perma-url> # Kommentare eines Inhalts
./humhub.py comment-create <content-id|perma-url> [datei|-] # Kommentar schreiben
```
Beide nehmen wie `fetch` eine **Content-ID** oder Perma-URL (nicht die Post-/
Wiki-Seiten-ID) und lösen das Zielobjekt selbst auf — funktioniert für Posts
und Wiki-Seiten.
### Direktnachrichten
```bash
./humhub.py mail-send <empfaenger> "<betreff>" [datei|-]
./humhub.py mail-send https://humhub.inmedias.it/u/serge/ "Betreff" entwurf.md
./humhub.py mail-send jan.stobbe,26 "Betreff" -
./humhub.py mail-reply <konversations-id> [datei|-]
```
Empfänger sind Benutzername, Profil-URL oder numerische ID; mehrere mit Komma.
`mail-reply` hängt einen Beitrag an eine bestehende Konversation
(`POST /mail/<id>/entry`). Die ID steht in der URL der Konversation:
`…/mail/mail/index?id=35`.
**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
`get-by-username`, dann ein Durchlauf der Benutzerliste, der Benutzername und
Slug ohne Rücksicht auf Groß-/Kleinschreibung vergleicht.
Zum Lesen von Konversationen gibt es bewusst keinen Befehl — Nachrichten
anderer gehören nicht ungefragt in einen Modellkontext. Auch `mail-reply`
holt den Verlauf nicht ab, sondern prüft nur, ob die Konversation erreichbar
ist.
Inhalte (Markdown) aus Datei oder `-` (stdin) übergeben — nicht als langes Inhalte (Markdown) aus Datei oder `-` (stdin) übergeben — nicht als langes
Shell-Argument (Sonderzeichen). `container_id` akzeptiert eine numerische Shell-Argument (Sonderzeichen). `container_id` akzeptiert eine numerische
Container-ID, einen **Space-Slug** oder eine **Space-URL** (`resolve_container` Container-ID, einen **Space-Slug** oder eine **Space-URL** (`resolve_container`
+136
View File
@@ -306,6 +306,118 @@ def cmd_space_list(args: argparse.Namespace) -> None:
print("(keine Spaces gefunden)") print("(keine Spaces gefunden)")
# ── Comment commands ─────────────────────────────────────────────────────────
def cmd_comment_list(args: argparse.Namespace) -> None:
session, _ = get_session()
content_id = parse_content_ref(args.ref)
data = api_get(session, f"/comment/content/{content_id}")
results = data.get("results", data if isinstance(data, list) else [])
if not results:
print("(keine Kommentare)")
return
for c in results:
cid = c.get("id")
author = (c.get("createdBy") or {}).get("display_name", "?")
created = c.get("createdAt", "")
msg = (c.get("message") or "").replace("\n", " ")[:100]
print(f"ID {cid:>5} [{created}] {author}: {msg}")
def cmd_comment_create(args: argparse.Namespace) -> None:
session, _ = get_session()
content_id = parse_content_ref(args.ref)
message = read_file(args.file)
envelope = api_get(session, f"/content/{content_id}")
meta = envelope.get("metadata", {})
model = meta.get("object_model", "")
object_id = meta.get("object_id")
if not model or not object_id:
sys.exit(f"Error: Content {content_id} nicht auflösbar (object_model/object_id fehlen)")
payload = {
"objectModel": model,
"objectId": object_id,
"Comment": {"message": message},
}
data = api_post(session, "/comment", payload)
cid = data.get("id")
short_model = model.split("\\")[-1]
print(f"Kommentar erstellt (ID {cid}) auf {short_model} {object_id}")
print(f"Perma: https://humhub.inmedias.it/content/perma?id={content_id}")
# ── Mail (Direktnachrichten) ─────────────────────────────────────────────────
def resolve_user(session: requests.Session, value: str) -> int:
"""Resolve a user ID, username, or profile URL (…/u/<slug>/) to a user ID.
Slug und Benutzername sind nicht dasselbe: zu https://…/u/serge/ gehört der
Benutzername "Serge". `get-by-username` kennt nur den Benutzernamen und
unterscheidet Groß-/Kleinschreibung — deshalb der Fallback über die
Benutzerliste, der auch den Slug vergleicht.
"""
value = value.strip()
if value.isdigit():
return int(value)
name = value
if "/" in value:
teile = [t for t in urlparse(value).path.split("/") if t]
if teile:
name = teile[-1]
r = session.get(f"{BASE_URL}/user/get-by-username",
params={"username": name})
if r.ok:
return int(r.json()["id"])
page = 1
while True:
data = api_get(session, f"/user?limit=50&page={page}")
results = data.get("results", [])
for u in results:
username = (u.get("account") or {}).get("username", "")
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 int(u["id"])
if len(results) < 50:
break
page += 1
sys.exit(f"Error: Benutzer {value!r} nicht gefunden")
def cmd_mail_send(args: argparse.Namespace) -> None:
session, _ = get_session()
ids = [resolve_user(session, teil) for teil in args.to.split(",") if teil.strip()]
if not ids:
sys.exit("Error: kein Empfänger angegeben")
message = read_file(args.file)
data = api_post(session, "/mail", {
"title": args.title,
"message": message,
"recipient": ids,
})
empfaenger = ", ".join(str(i) for i in ids)
print(f"Nachricht gesendet (Konversation {data.get('id')}) an Benutzer {empfaenger}")
def cmd_mail_reply(args: argparse.Namespace) -> None:
"""Antwortet in einer bestehenden Konversation.
Bewusst ohne Lesebefehl: der Text der Konversation wird nicht abgerufen,
nur ihre Erreichbarkeit geprueft.
"""
session, _ = get_session()
message = read_file(args.file)
r = session.get(f"{BASE_URL}/mail/{args.id}")
if not r.ok:
sys.exit(f"Error {r.status_code}: Konversation {args.id} nicht erreichbar")
data = api_post(session, f"/mail/{args.id}/entry", {"message": message})
print(f"Antwort geschrieben (Eintrag {data.get('id')}) in Konversation {args.id}")
# ── Fetch (perma-/content-id-agnostisch) ───────────────────────────────────── # ── Fetch (perma-/content-id-agnostisch) ─────────────────────────────────────
def parse_content_ref(ref: str) -> int: def parse_content_ref(ref: str) -> int:
@@ -460,6 +572,30 @@ def main() -> None:
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen") p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen")
p.set_defaults(func=cmd_post_create) p.set_defaults(func=cmd_post_create)
# comment-list
p = sub.add_parser("comment-list", help="Kommentare eines Inhalts auflisten")
p.add_argument("ref", help="Content-ID oder Perma-Link (…/content/perma?id=NNNN)")
p.set_defaults(func=cmd_comment_list)
# comment-create
p = sub.add_parser("comment-create", help="Kommentar auf einen Inhalt (Post/Wiki-Seite) schreiben")
p.add_argument("ref", help="Content-ID oder Perma-Link (…/content/perma?id=NNNN)")
p.add_argument("file", help="Markdown-Datei oder '-' für stdin")
p.set_defaults(func=cmd_comment_create)
# mail-send
p = sub.add_parser("mail-send", help="Direktnachricht an Benutzer senden")
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.set_defaults(func=cmd_mail_send)
# 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)")
p.add_argument("file", help="Markdown-Datei oder '-' für stdin")
p.set_defaults(func=cmd_mail_reply)
# post-update # post-update
p = sub.add_parser("post-update", help="Post aktualisieren") p = sub.add_parser("post-update", help="Post aktualisieren")
p.add_argument("id", type=int, help="Post-ID") p.add_argument("id", type=int, help="Post-ID")