3 Commits
Author SHA1 Message Date
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
jhsandClaude Opus 5 75e99dd0b8 humhub: Umfragen anlegen und auflisten (poll-create / poll-list)
Die Polls-REST-API ist auf der Instanz aktiv, der Skill kannte sie nicht.
Zwei Abweichungen vom Muster der Post-API, die beide in
HTTP 500 'Internal error while save a poll!' enden:

* Der Controller macht $poll->load(Yii::$app->request->post()) — die Felder
  gehoeren unter den Yii-Formnamen 'Poll', nicht unter 'data'.
* Die Optionen heissen beim Anlegen 'newAnswers'; 'answers' ist die Leseform
  und wird beim POST ignoriert.

Beim Lesen ist limit Pflicht: ohne Begrenzung antwortet die Instanz ab etwa
25 Umfragen ebenfalls mit 500.

Quelle: humhub/polls, controllers/rest/PollsController.php + models/Poll.php.
Verifiziert an Space inmedias (Umfrage 36).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 09:24:31 +02:00
5 changed files with 175 additions and 7 deletions
+6
View File
@@ -0,0 +1,6 @@
# Python
__pycache__/
*.py[cod]
# lokale Umgebungs-/Secret-Dateien
.env
+3 -2
View File
@@ -79,9 +79,10 @@ natürlicher klingen. Basiert auf Wikipedias „Signs of AI writing".
### humhub ### humhub
HumHub (humhub.inmedias.it) über die REST-API: Wiki-Seiten und Posts auflisten, HumHub (humhub.inmedias.it) über die REST-API: Wiki-Seiten, Posts und Umfragen auflisten,
anzeigen, anlegen und aktualisieren — via mitgelieferter, self-contained anzeigen, anlegen und aktualisieren — via mitgelieferter, self-contained
`humhub.py` (`wiki-list/get/update/create`, `post-list/create/update`). `humhub.py` (`wiki-list/get/update/create`, `post-list/create/update`,
`poll-list/create`).
- Voraussetzung: `HUMHUB_API_TOKEN` als Umgebungsvariable bzw. dotenv-Datei - Voraussetzung: `HUMHUB_API_TOKEN` als Umgebungsvariable bzw. dotenv-Datei
(kein Token im Plugin). Token nur als berechtigter Benutzer unter (kein Token im Plugin). Token nur als berechtigter Benutzer unter
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "humhub", "name": "humhub",
"version": "0.1.0", "version": "0.3.0",
"description": "HumHub via REST-API: Wiki-Seiten und Posts 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"
} }
+35 -3
View File
@@ -1,13 +1,13 @@
--- ---
name: humhub name: humhub
description: HumHub Wiki-Seiten und Posts lesen/erstellen/aktualisieren via REST-API. Use when the user mentions "HumHub", "Space", "Wiki-Seite in HumHub" or "Post/Beitrag". description: HumHub Wiki-Seiten, Posts und Umfragen lesen/erstellen/aktualisieren via REST-API. Use when the user mentions "HumHub", "Space", "Wiki-Seite in HumHub", "Post/Beitrag" or "Umfrage/Poll".
version: 0.1.0 version: 0.2.0
--- ---
# HumHub (humhub.py) # HumHub (humhub.py)
Interagiert mit HumHub (https://humhub.inmedias.it) über die REST-API: Interagiert mit HumHub (https://humhub.inmedias.it) über die REST-API:
Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren — über die Wiki-Seiten, Posts und Umfragen auflisten, anzeigen, anlegen und aktualisieren — über die
mitgelieferte, self-contained `humhub.py`. mitgelieferte, self-contained `humhub.py`.
## Ausführen ## Ausführen
@@ -59,6 +59,38 @@ werden nicht committet).
./humhub.py post-update <id> [datei|-] ./humhub.py post-update <id> [datei|-]
``` ```
### Umfragen
```bash
./humhub.py poll-list <container_id> [--limit N]
./humhub.py poll-create <container_id> "<frage>" --answers "A|B|C" \
[--file beschreibung.md] [--anonymous] [--multiple] [--hide-results]
```
Antwortoptionen mit `|` trennen, mindestens zwei. Der Beschreibungstext kommt
wie überall aus einer Datei oder `-`; Markdown ist erlaubt. Ohne `--anonymous`
ist sichtbar, wer wie gestimmt hat — bei Anmeldungen mit begrenzten Plätzen ist
das der Sinn der Sache.
Zwei Eigenheiten der Polls-API, die zu `HTTP 500 "Internal error while save a
poll!"` führen, wenn man dem Muster der Post-API folgt: die Felder gehören
unter den Yii-Formnamen **`Poll`** (nicht `data`), und die Optionen heißen beim
Anlegen **`newAnswers`** (beim Lesen `answers`). Beim Lesen ist `limit` Pflicht
und nicht Komfort — ohne Begrenzung antwortet die Instanz ab ~25 Umfragen
ebenfalls mit 500. Ein fehlgeschlagener POST legt nichts an; vor einem zweiten
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.
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`
+129
View File
@@ -212,6 +212,61 @@ def cmd_post_create(args: argparse.Namespace) -> None:
print(f"Topics: {', '.join(saved_topics)}") print(f"Topics: {', '.join(saved_topics)}")
def cmd_poll_list(args: argparse.Namespace) -> None:
session, _ = get_session()
container_id = resolve_container(session, args.container_id)
# limit ist Pflicht, nicht Komfort: ohne Begrenzung antwortet die Instanz
# bei vielen Umfragen mit HTTP 500 statt mit einer Liste.
data = api_get(session, f"/polls/container/{container_id}?limit={args.limit}")
results = data.get("results", [])
if not results:
print("(keine Umfragen)")
return
for poll in results:
antworten = [a.get("answer", "") for a in poll.get("answers", [])]
zu = " [geschlossen]" if poll.get("closed") else ""
print(f"ID {poll.get('id')}{zu}: {poll.get('question', '')}")
print(f" {' | '.join(antworten)}")
def cmd_poll_create(args: argparse.Namespace) -> None:
"""Umfrage anlegen.
Zwei Abweichungen von der Post-API, die beide mit HTTP 500
("Internal error while save a poll!") enden, wenn man sie übersieht:
* Der Controller macht `$poll->load(Yii::$app->request->post())` — Yii
erwartet die Felder deshalb unter dem Formnamen **`Poll`**, nicht unter
`data` wie bei Posts und Wiki-Seiten.
* Die Optionen heißen beim Anlegen **`newAnswers`**; `answers` ist die
Leseform und wird beim POST ignoriert.
Quelle: humhub/polls, controllers/rest/PollsController.php + models/Poll.php.
Pflicht sind `question` und mindestens zwei Antworten.
"""
session, _ = get_session()
container_id = resolve_container(session, args.container_id)
antworten = [a.strip() for a in args.answers.split("|") if a.strip()]
if len(antworten) < 2:
sys.exit("Error: mindestens zwei Antwortoptionen nötig "
"(--answers 'Ja|Nein').")
beschreibung = read_file(args.file) if args.file else ""
payload = {"Poll": {
"question": args.question,
"description": beschreibung,
"newAnswers": antworten,
"anonymous": 1 if args.anonymous else 0,
"allow_multiple": 1 if args.multiple else 0,
"show_result_after_close": 1 if args.hide_results else 0,
"is_random": 0,
}}
data = api_post(session, f"/polls/container/{container_id}", payload)
print(f"Umfrage erstellt (ID {data.get('id')}): {data.get('question')}")
print("Optionen: " + " | ".join(a.get("answer", "")
for a in data.get("answers", [])))
def cmd_post_update(args: argparse.Namespace) -> None: def cmd_post_update(args: argparse.Namespace) -> None:
session, _ = get_session() session, _ = get_session()
message = read_file(args.file) message = read_file(args.file)
@@ -251,6 +306,48 @@ 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}")
# ── Fetch (perma-/content-id-agnostisch) ───────────────────────────────────── # ── Fetch (perma-/content-id-agnostisch) ─────────────────────────────────────
def parse_content_ref(ref: str) -> int: def parse_content_ref(ref: str) -> int:
@@ -405,6 +502,17 @@ 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)
# 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")
@@ -412,6 +520,27 @@ def main() -> None:
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen (ersetzt bestehende)") p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen (ersetzt bestehende)")
p.set_defaults(func=cmd_post_update) p.set_defaults(func=cmd_post_update)
# poll-list
p = sub.add_parser("poll-list", help="Umfragen eines Containers auflisten")
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
p.add_argument("--limit", type=int, default=10, help="Max. Anzahl (Standard: 10)")
p.set_defaults(func=cmd_poll_list)
# poll-create
p = sub.add_parser("poll-create", help="Neue Umfrage anlegen")
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
p.add_argument("question", help="Die Frage (Pflicht)")
p.add_argument("--answers", required=True,
help="Antwortoptionen, mit | getrennt: 'Ja|Nein|Vielleicht' "
"(mindestens zwei)")
p.add_argument("--file", default=None,
help="Markdown-Datei mit dem Beschreibungstext, '-' für stdin")
p.add_argument("--anonymous", action="store_true", help="anonyme Abstimmung")
p.add_argument("--multiple", action="store_true", help="Mehrfachauswahl erlauben")
p.add_argument("--hide-results", action="store_true",
help="Ergebnis erst nach Ende der Umfrage zeigen")
p.set_defaults(func=cmd_poll_create)
args = parser.parse_args() args = parser.parse_args()
args.func(args) args.func(args)