Files
claude-plugins/plugins/humhub/skills/humhub/humhub.py
T
skoandClaude Opus 4.8 f45d7a3f8f plugins/humhub: humhub.py als Skill (Wiki-Seiten & Posts via REST-API)
Neues Plugin, das HumHub (humhub.inmedias.it) ueber die gevendorte, self-
contained humhub.py (uv run --script; requests+python-dotenv) bedient:
wiki-list/get/update/create, post-list/create/update. Token bleibt lokal
(Umgebungsvariable bzw. dotenv-Datei) - Beispiel-Vorlage beiliegend. SKILL.md
dokumentiert Nutzung. Marketplace-Eintrag ergaenzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:01:25 +02:00

262 lines
9.6 KiB
Python
Executable File

#!/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()