plugins/humhub: humhub.py als Skill (Wiki-Seiten & Posts) #2
@@ -22,6 +22,11 @@
|
|||||||
"name": "humanizer",
|
"name": "humanizer",
|
||||||
"source": "./plugins/humanizer",
|
"source": "./plugins/humanizer",
|
||||||
"description": "Entfernt typische Merkmale KI-generierten Texts, damit er natürlicher/menschlicher klingt (vendored von github.com/blader/humanizer)."
|
"description": "Entfernt typische Merkmale KI-generierten Texts, damit er natürlicher/menschlicher klingt (vendored von github.com/blader/humanizer)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "humhub",
|
||||||
|
"source": "./plugins/humhub",
|
||||||
|
"description": "HumHub via REST-API: Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren (Token lokal)."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "humhub",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "HumHub via REST-API: Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren (humhub.py).",
|
||||||
|
"author": {
|
||||||
|
"name": "inmedias.it"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Zugangsdaten fuer humhub.py — kopieren nach ~/.config/dokumentierer/.env
|
||||||
|
# oder .env im Arbeitsverzeichnis, oder als Umgebungsvariable setzen.
|
||||||
|
# NICHT committen. Token nur als berechtigter Benutzer unter:
|
||||||
|
# https://humhub.inmedias.it/rest/admin/index
|
||||||
|
|
||||||
|
HUMHUB_API_TOKEN=change-me
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
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".
|
||||||
|
version: 0.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# HumHub (humhub.py)
|
||||||
|
|
||||||
|
Interagiert mit HumHub (https://humhub.inmedias.it) über die REST-API:
|
||||||
|
Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren — über die
|
||||||
|
mitgelieferte, self-contained `humhub.py`.
|
||||||
|
|
||||||
|
## Ausführen
|
||||||
|
|
||||||
|
`humhub.py` ist ein eigenständiges `uv run --script` (Inline-Deps: `requests`,
|
||||||
|
`python-dotenv`). Aus dem Skill-Verzeichnis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./humhub.py <befehl> ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wiki
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./humhub.py wiki-list ["filter"] # Wiki-Seiten auflisten (Titel-Filter optional)
|
||||||
|
./humhub.py wiki-get <id> # Wiki-Seite anzeigen (Inhalt + URL)
|
||||||
|
./humhub.py wiki-update <id> [datei|-] [--title ...] [--topics a,b] [--parent-id N]
|
||||||
|
./humhub.py wiki-create <container_id> "<titel>" [datei|-] [--topics a,b] [--parent-id N]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Posts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./humhub.py post-list <container_id> [--limit N]
|
||||||
|
./humhub.py post-create <container_id> [datei|-]
|
||||||
|
./humhub.py post-update <id> [datei|-]
|
||||||
|
```
|
||||||
|
|
||||||
|
Inhalte (Markdown) aus Datei oder `-` (stdin) übergeben — nicht als langes
|
||||||
|
Shell-Argument (Sonderzeichen). `container_id` ist die Space-/Profil-Container-ID
|
||||||
|
in HumHub (z. B. ein User- oder Space-Container). Mit `wiki-list` /
|
||||||
|
`post-list` die passende ID/Seite ermitteln.
|
||||||
|
|
||||||
|
## Zugangsdaten
|
||||||
|
|
||||||
|
`humhub.py` bringt **keinen** Token mit. Jede/r hinterlegt ein eigenes
|
||||||
|
API-Token als Umgebungsvariable oder in einer dotenv-Datei (Reihenfolge:
|
||||||
|
`~/.config/dokumentierer/.env`, dann Datei im Arbeitsverzeichnis, dann bereits
|
||||||
|
gesetzte Env-Variablen). Siehe `.env.example`:
|
||||||
|
|
||||||
|
- `HUMHUB_API_TOKEN` — REST-API-Token. Nur als **berechtigter Benutzer**
|
||||||
|
erhältlich unter https://humhub.inmedias.it/rest/admin/index
|
||||||
|
|
||||||
|
Die Basis-URL (`https://humhub.inmedias.it/api/v1`) ist fest im Skript; für eine
|
||||||
|
andere Instanz `BASE_URL` oben in `humhub.py` anpassen.
|
||||||
Executable
+261
@@ -0,0 +1,261 @@
|
|||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "requests",
|
||||||
|
# "python-dotenv",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
"""CLI for interacting with HumHub via REST API (wiki pages and posts)."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv, find_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
BASE_URL = "https://humhub.inmedias.it/api/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def get_session() -> tuple[requests.Session, str]:
|
||||||
|
load_dotenv(Path.home() / ".config/dokumentierer/.env")
|
||||||
|
load_dotenv() # CWD/Projektverzeichnis, überschreibt nichts
|
||||||
|
token = os.environ.get("HUMHUB_API_TOKEN", "")
|
||||||
|
if not token:
|
||||||
|
sys.exit("Error: HUMHUB_API_TOKEN is not set in .env")
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers.update({"Authorization": f"Bearer {token}"})
|
||||||
|
return session, token
|
||||||
|
|
||||||
|
|
||||||
|
def api_get(session: requests.Session, path: str) -> dict:
|
||||||
|
r = session.get(f"{BASE_URL}{path}")
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def api_put(session: requests.Session, path: str, payload: dict) -> dict:
|
||||||
|
r = session.put(f"{BASE_URL}{path}", json=payload)
|
||||||
|
if not r.ok:
|
||||||
|
sys.exit(f"Error {r.status_code}: {r.text}")
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def api_post(session: requests.Session, path: str, payload: dict) -> dict:
|
||||||
|
r = session.post(f"{BASE_URL}{path}", json=payload)
|
||||||
|
if not r.ok:
|
||||||
|
sys.exit(f"Error {r.status_code}: {r.text}")
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_topics(topics_str: str | None) -> list[dict]:
|
||||||
|
if not topics_str:
|
||||||
|
return []
|
||||||
|
return [{"name": t.strip()} for t in topics_str.split(",") if t.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(path: str) -> str:
|
||||||
|
if path == "-":
|
||||||
|
return sys.stdin.read()
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
return fh.read()
|
||||||
|
except OSError as exc:
|
||||||
|
sys.exit(f"Error reading file: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Wiki commands ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def cmd_wiki_list(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
data = api_get(session, "/wiki?per-page=200")
|
||||||
|
query = args.filter.lower() if args.filter else None
|
||||||
|
for page in data["results"]:
|
||||||
|
if query and query not in page["title"].lower():
|
||||||
|
continue
|
||||||
|
print(f"{page['id']:>4} {page['title']}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_wiki_get(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
data = api_get(session, f"/wiki/page/{args.id}")
|
||||||
|
title = data.get("title", "")
|
||||||
|
content = data.get("latest_revision", {}).get("content", "")
|
||||||
|
topics = [t["name"] for t in data.get("content", {}).get("topics", [])]
|
||||||
|
url = data.get("content", {}).get("metadata", {}).get("url", "")
|
||||||
|
print(f"Title: {title}")
|
||||||
|
print(f"Topics: {', '.join(topics) or '(keine)'}")
|
||||||
|
print(f"URL: https://humhub.inmedias.it{url}")
|
||||||
|
print(f"Content ({len(content)} Zeichen):")
|
||||||
|
print(content)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_wiki_update(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
content = read_file(args.file) if args.file else None
|
||||||
|
|
||||||
|
current = api_get(session, f"/wiki/page/{args.id}")
|
||||||
|
title = args.title or current.get("title", "")
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
content = current.get("latest_revision", {}).get("content", "")
|
||||||
|
|
||||||
|
wiki_page: dict = {"title": title}
|
||||||
|
if args.parent_id is not None:
|
||||||
|
wiki_page["parent_page_id"] = args.parent_id
|
||||||
|
|
||||||
|
payload: dict = {
|
||||||
|
"WikiPage": wiki_page,
|
||||||
|
"WikiPageRevision": {"content": content},
|
||||||
|
}
|
||||||
|
topics = parse_topics(args.topics)
|
||||||
|
if topics:
|
||||||
|
payload["content"] = {"topics": topics}
|
||||||
|
|
||||||
|
data = api_put(session, f"/wiki/page/{args.id}", payload)
|
||||||
|
saved_topics = [t["name"] for t in data.get("content", {}).get("topics", [])]
|
||||||
|
parent_id = data.get("parent_page_id")
|
||||||
|
print(f"Aktualisiert: '{data.get('title')}' (Revision {data.get('latest_revision', {}).get('id')}){f', parent={parent_id}' if parent_id else ''}")
|
||||||
|
if saved_topics:
|
||||||
|
print(f"Topics: {', '.join(saved_topics)}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_wiki_create(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
content = read_file(args.file) if args.file else ""
|
||||||
|
|
||||||
|
wiki_page: dict = {"title": args.title}
|
||||||
|
if args.parent_id is not None:
|
||||||
|
wiki_page["parent_page_id"] = args.parent_id
|
||||||
|
|
||||||
|
payload: dict = {
|
||||||
|
"WikiPage": wiki_page,
|
||||||
|
"WikiPageRevision": {"content": content},
|
||||||
|
}
|
||||||
|
topics = parse_topics(args.topics)
|
||||||
|
if topics:
|
||||||
|
payload["content"] = {"topics": topics}
|
||||||
|
|
||||||
|
data = api_post(session, f"/wiki/container/{args.container_id}", payload)
|
||||||
|
page_id = data.get("id")
|
||||||
|
url = data.get("content", {}).get("metadata", {}).get("url", "")
|
||||||
|
print(f"Erstellt: '{data.get('title')}' (ID {page_id})")
|
||||||
|
if url:
|
||||||
|
print(f"URL: https://humhub.inmedias.it{url}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Post commands ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def cmd_post_list(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
data = api_get(session, f"/post/container/{args.container_id}?limit={args.limit}")
|
||||||
|
results = data.get("results", [])
|
||||||
|
if not results:
|
||||||
|
print("(keine Posts)")
|
||||||
|
return
|
||||||
|
for post in results:
|
||||||
|
pid = post.get("id")
|
||||||
|
msg = post.get("message", "").replace("\n", " ")[:80]
|
||||||
|
topics = [t["name"] for t in post.get("content", {}).get("topics", [])]
|
||||||
|
topic_str = f" [{', '.join(topics)}]" if topics else ""
|
||||||
|
print(f"ID {pid}{topic_str}: {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_post_create(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
message = read_file(args.file)
|
||||||
|
topics = parse_topics(args.topics)
|
||||||
|
|
||||||
|
payload: dict = {"data": {"message": message}}
|
||||||
|
if topics:
|
||||||
|
payload["data"]["content"] = {"topics": topics}
|
||||||
|
|
||||||
|
data = api_post(session, f"/post/container/{args.container_id}", payload)
|
||||||
|
pid = data.get("id")
|
||||||
|
saved_topics = [t["name"] for t in data.get("content", {}).get("topics", [])]
|
||||||
|
print(f"Post erstellt (ID {pid})")
|
||||||
|
if saved_topics:
|
||||||
|
print(f"Topics: {', '.join(saved_topics)}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_post_update(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
message = read_file(args.file)
|
||||||
|
topics = parse_topics(args.topics)
|
||||||
|
|
||||||
|
payload: dict = {"data": {"message": message}}
|
||||||
|
if topics:
|
||||||
|
payload["data"]["content"] = {"topics": topics}
|
||||||
|
|
||||||
|
data = api_put(session, f"/post/{args.id}", payload)
|
||||||
|
pid = data.get("id")
|
||||||
|
saved_topics = [t["name"] for t in data.get("content", {}).get("topics", [])]
|
||||||
|
print(f"Post aktualisiert (ID {pid})")
|
||||||
|
if saved_topics:
|
||||||
|
print(f"Topics: {', '.join(saved_topics)}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="HumHub REST API CLI")
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# wiki-list
|
||||||
|
p = sub.add_parser("wiki-list", help="Alle Wiki-Seiten auflisten")
|
||||||
|
p.add_argument("filter", nargs="?", help="Filter nach Titel (case-insensitiv)")
|
||||||
|
p.set_defaults(func=cmd_wiki_list)
|
||||||
|
|
||||||
|
# wiki-get
|
||||||
|
p = sub.add_parser("wiki-get", help="Wiki-Seite anzeigen")
|
||||||
|
p.add_argument("id", type=int, help="Wiki-Seiten-ID")
|
||||||
|
p.set_defaults(func=cmd_wiki_get)
|
||||||
|
|
||||||
|
# wiki-update
|
||||||
|
p = sub.add_parser("wiki-update", help="Wiki-Seite aktualisieren")
|
||||||
|
p.add_argument("id", type=int, help="Wiki-Seiten-ID")
|
||||||
|
p.add_argument("file", nargs="?", default=None, help="Markdown-Datei oder '-' für stdin (optional)")
|
||||||
|
p.add_argument("--title", default=None, help="Neuer Titel (sonst unverändert)")
|
||||||
|
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen (ersetzt bestehende)")
|
||||||
|
p.add_argument("--parent-id", type=int, default=None, dest="parent_id", help="ID der Oberseite")
|
||||||
|
p.set_defaults(func=cmd_wiki_update)
|
||||||
|
|
||||||
|
# wiki-create
|
||||||
|
p = sub.add_parser("wiki-create", help="Neue Wiki-Seite anlegen")
|
||||||
|
p.add_argument("container_id", type=int, help="Container-ID (z.B. 37 für user serge)")
|
||||||
|
p.add_argument("title", help="Seitentitel")
|
||||||
|
p.add_argument("file", nargs="?", default=None, help="Markdown-Datei oder '-' für stdin (optional)")
|
||||||
|
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen")
|
||||||
|
p.add_argument("--parent-id", type=int, default=None, dest="parent_id", help="ID der Oberseite")
|
||||||
|
p.set_defaults(func=cmd_wiki_create)
|
||||||
|
|
||||||
|
# post-list
|
||||||
|
p = sub.add_parser("post-list", help="Posts eines Containers auflisten")
|
||||||
|
p.add_argument("container_id", type=int, help="Container-ID")
|
||||||
|
p.add_argument("--limit", type=int, default=10, help="Max. Anzahl Posts (Standard: 10)")
|
||||||
|
p.set_defaults(func=cmd_post_list)
|
||||||
|
|
||||||
|
# post-create
|
||||||
|
p = sub.add_parser("post-create", help="Neuen Post erstellen")
|
||||||
|
p.add_argument("container_id", type=int, help="Container-ID")
|
||||||
|
p.add_argument("file", help="Markdown-Datei oder '-' für stdin")
|
||||||
|
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen")
|
||||||
|
p.set_defaults(func=cmd_post_create)
|
||||||
|
|
||||||
|
# post-update
|
||||||
|
p = sub.add_parser("post-update", help="Post aktualisieren")
|
||||||
|
p.add_argument("id", type=int, help="Post-ID")
|
||||||
|
p.add_argument("file", help="Markdown-Datei oder '-' für stdin")
|
||||||
|
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen (ersetzt bestehende)")
|
||||||
|
p.set_defaults(func=cmd_post_update)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user