Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bfc62bcb8 | ||
|
|
e00395fc64 | ||
|
|
4a20da2ed2 | ||
|
|
2a02dac74e | ||
|
|
75e99dd0b8 |
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# lokale Umgebungs-/Secret-Dateien
|
||||||
|
.env
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,64 @@ 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.
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|||||||
@@ -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,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:
|
||||||
@@ -405,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")
|
||||||
@@ -412,6 +603,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)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user