diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d18f6f8..092a60f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -23,6 +23,11 @@ "source": "./plugins/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)." + }, { "name": "mediawiki", "source": "./plugins/mediawiki", diff --git a/plugins/humhub/.claude-plugin/plugin.json b/plugins/humhub/.claude-plugin/plugin.json new file mode 100644 index 0000000..5ade752 --- /dev/null +++ b/plugins/humhub/.claude-plugin/plugin.json @@ -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" + } +} diff --git a/plugins/humhub/skills/humhub/.env.example b/plugins/humhub/skills/humhub/.env.example new file mode 100644 index 0000000..06419fa --- /dev/null +++ b/plugins/humhub/skills/humhub/.env.example @@ -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 diff --git a/plugins/humhub/skills/humhub/SKILL.md b/plugins/humhub/skills/humhub/SKILL.md new file mode 100644 index 0000000..6aee06e --- /dev/null +++ b/plugins/humhub/skills/humhub/SKILL.md @@ -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 ... +``` + +### Wiki + +```bash +./humhub.py wiki-list ["filter"] # Wiki-Seiten auflisten (Titel-Filter optional) +./humhub.py wiki-get # Wiki-Seite anzeigen (Inhalt + URL) +./humhub.py wiki-update [datei|-] [--title ...] [--topics a,b] [--parent-id N] +./humhub.py wiki-create "" [datei|-] [--topics a,b] [--parent-id N] +``` + +### Posts + +```bash +./humhub.py post-list [--limit N] +./humhub.py post-create [datei|-] +./humhub.py post-update [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. diff --git a/plugins/humhub/skills/humhub/humhub.py b/plugins/humhub/skills/humhub/humhub.py new file mode 100755 index 0000000..1cecddf --- /dev/null +++ b/plugins/humhub/skills/humhub/humhub.py @@ -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()