humhub: fetch-Befehl + space-list/resolve_container ins Plugin
- fetch <content-id|perma-url> [--save]: löst Perma-Links selbst auf - space-list + resolve_container: container_id akzeptiert Slug/URL/ID - SKILL.md entsprechend ergänzt Gleicht das Plugin an die kanonische humhub.py (dokumentierer) an. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,29 @@ mitgelieferte, self-contained `humhub.py`.
|
|||||||
./humhub.py <befehl> ...
|
./humhub.py <befehl> ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Perma-Link / Content-ID holen (empfohlener Einstieg)
|
||||||
|
|
||||||
|
HumHub-Links der Form `…/content/perma?id=NNNN` tragen eine **Content-ID** — nicht
|
||||||
|
die Wiki-Seiten- oder Post-ID, die die anderen Befehle erwarten. `fetch` löst das
|
||||||
|
selbst auf (erkennt WikiPage vs. Post) und braucht keine Vorab-Recherche:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./humhub.py fetch <content-id|perma-url> # Inhalt + Herkunfts-Kopf auf stdout
|
||||||
|
./humhub.py fetch <content-id|perma-url> --save # ablegen unter resources/humhub-<id>-<slug>.md
|
||||||
|
./humhub.py fetch <content-id|perma-url> --save --out <dir>
|
||||||
|
```
|
||||||
|
|
||||||
|
`--save` schreibt Markdown mit einem Herkunfts-Kopf (Quelle, Titel, Typ, Topics,
|
||||||
|
Stand, Abrufdatum). Zielverzeichnis ist standardmäßig `resources/` im aktuellen
|
||||||
|
Projekt — dieses gehört üblicherweise in `.gitignore` (abgerufene Fremdinhalte
|
||||||
|
werden nicht committet).
|
||||||
|
|
||||||
|
### Spaces
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./humhub.py space-list # Spaces auflisten mit contentcontainer_id
|
||||||
|
```
|
||||||
|
|
||||||
### Wiki
|
### Wiki
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -37,9 +60,10 @@ mitgelieferte, self-contained `humhub.py`.
|
|||||||
```
|
```
|
||||||
|
|
||||||
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` ist die Space-/Profil-Container-ID
|
Shell-Argument (Sonderzeichen). `container_id` akzeptiert eine numerische
|
||||||
in HumHub (z. B. ein User- oder Space-Container). Mit `wiki-list` /
|
Container-ID, einen **Space-Slug** oder eine **Space-URL** (`resolve_container`
|
||||||
`post-list` die passende ID/Seite ermitteln.
|
löst Slug/URL selbst auf). Passende Container per `space-list` ermitteln, Seiten
|
||||||
|
per `wiki-list` / `post-list`.
|
||||||
|
|
||||||
## Zugangsdaten
|
## Zugangsdaten
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from dotenv import load_dotenv, find_dotenv
|
from dotenv import load_dotenv, find_dotenv
|
||||||
@@ -52,6 +55,30 @@ def api_post(session: requests.Session, path: str, payload: dict) -> dict:
|
|||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_container(session: requests.Session, value: str) -> int:
|
||||||
|
"""Resolve a container ID, space slug, or space URL to a contentcontainer_id."""
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# Strip full URL down to slug
|
||||||
|
slug = value.rstrip("/").split("/s/")[-1].rstrip("/") if "/s/" in value else value.strip("/")
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
data = api_get(session, f"/space?limit=50&page={page}")
|
||||||
|
results = data.get("results", [])
|
||||||
|
if not results:
|
||||||
|
break
|
||||||
|
for space in results:
|
||||||
|
space_slug = space.get("url", "").rstrip("/").split("/s/")[-1].rstrip("/")
|
||||||
|
if space_slug == slug or space.get("name", "").lower() == slug.lower():
|
||||||
|
return space["contentcontainer_id"]
|
||||||
|
if len(results) < 50:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
sys.exit(f"Error: Space '{value}' nicht gefunden. Verfügbare Spaces: uv run humhub.py space-list")
|
||||||
|
|
||||||
|
|
||||||
def parse_topics(topics_str: str | None) -> list[dict]:
|
def parse_topics(topics_str: str | None) -> list[dict]:
|
||||||
if not topics_str:
|
if not topics_str:
|
||||||
return []
|
return []
|
||||||
@@ -126,6 +153,7 @@ def cmd_wiki_update(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def cmd_wiki_create(args: argparse.Namespace) -> None:
|
def cmd_wiki_create(args: argparse.Namespace) -> None:
|
||||||
session, _ = get_session()
|
session, _ = get_session()
|
||||||
|
container_id = resolve_container(session, args.container_id)
|
||||||
content = read_file(args.file) if args.file else ""
|
content = read_file(args.file) if args.file else ""
|
||||||
|
|
||||||
wiki_page: dict = {"title": args.title}
|
wiki_page: dict = {"title": args.title}
|
||||||
@@ -140,7 +168,7 @@ def cmd_wiki_create(args: argparse.Namespace) -> None:
|
|||||||
if topics:
|
if topics:
|
||||||
payload["content"] = {"topics": topics}
|
payload["content"] = {"topics": topics}
|
||||||
|
|
||||||
data = api_post(session, f"/wiki/container/{args.container_id}", payload)
|
data = api_post(session, f"/wiki/container/{container_id}", payload)
|
||||||
page_id = data.get("id")
|
page_id = data.get("id")
|
||||||
url = data.get("content", {}).get("metadata", {}).get("url", "")
|
url = data.get("content", {}).get("metadata", {}).get("url", "")
|
||||||
print(f"Erstellt: '{data.get('title')}' (ID {page_id})")
|
print(f"Erstellt: '{data.get('title')}' (ID {page_id})")
|
||||||
@@ -152,7 +180,8 @@ def cmd_wiki_create(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def cmd_post_list(args: argparse.Namespace) -> None:
|
def cmd_post_list(args: argparse.Namespace) -> None:
|
||||||
session, _ = get_session()
|
session, _ = get_session()
|
||||||
data = api_get(session, f"/post/container/{args.container_id}?limit={args.limit}")
|
container_id = resolve_container(session, args.container_id)
|
||||||
|
data = api_get(session, f"/post/container/{container_id}?limit={args.limit}")
|
||||||
results = data.get("results", [])
|
results = data.get("results", [])
|
||||||
if not results:
|
if not results:
|
||||||
print("(keine Posts)")
|
print("(keine Posts)")
|
||||||
@@ -167,6 +196,7 @@ def cmd_post_list(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def cmd_post_create(args: argparse.Namespace) -> None:
|
def cmd_post_create(args: argparse.Namespace) -> None:
|
||||||
session, _ = get_session()
|
session, _ = get_session()
|
||||||
|
container_id = resolve_container(session, args.container_id)
|
||||||
message = read_file(args.file)
|
message = read_file(args.file)
|
||||||
topics = parse_topics(args.topics)
|
topics = parse_topics(args.topics)
|
||||||
|
|
||||||
@@ -174,7 +204,7 @@ def cmd_post_create(args: argparse.Namespace) -> None:
|
|||||||
if topics:
|
if topics:
|
||||||
payload["data"]["content"] = {"topics": topics}
|
payload["data"]["content"] = {"topics": topics}
|
||||||
|
|
||||||
data = api_post(session, f"/post/container/{args.container_id}", payload)
|
data = api_post(session, f"/post/container/{container_id}", payload)
|
||||||
pid = data.get("id")
|
pid = data.get("id")
|
||||||
saved_topics = [t["name"] for t in data.get("content", {}).get("topics", [])]
|
saved_topics = [t["name"] for t in data.get("content", {}).get("topics", [])]
|
||||||
print(f"Post erstellt (ID {pid})")
|
print(f"Post erstellt (ID {pid})")
|
||||||
@@ -199,12 +229,137 @@ def cmd_post_update(args: argparse.Namespace) -> None:
|
|||||||
print(f"Topics: {', '.join(saved_topics)}")
|
print(f"Topics: {', '.join(saved_topics)}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_space_list(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
page = 1
|
||||||
|
found = False
|
||||||
|
while True:
|
||||||
|
data = api_get(session, f"/space?limit=50&page={page}")
|
||||||
|
results = data.get("results", [])
|
||||||
|
if not results:
|
||||||
|
break
|
||||||
|
for space in results:
|
||||||
|
slug = space.get("url", "").rstrip("/").split("/s/")[-1].rstrip("/")
|
||||||
|
ccid = space.get("contentcontainer_id", "?")
|
||||||
|
name = space.get("name", "")
|
||||||
|
print(f"ccid={ccid:>4} slug={slug:<30} {name}")
|
||||||
|
found = True
|
||||||
|
if len(results) < 50:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
if not found:
|
||||||
|
print("(keine Spaces gefunden)")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Fetch (perma-/content-id-agnostisch) ─────────────────────────────────────
|
||||||
|
|
||||||
|
def parse_content_ref(ref: str) -> int:
|
||||||
|
"""Akzeptiert eine blanke Content-ID oder einen Perma-Link
|
||||||
|
(…/content/perma?id=NNNN) und liefert die Content-ID."""
|
||||||
|
ref = ref.strip()
|
||||||
|
if ref.isdigit():
|
||||||
|
return int(ref)
|
||||||
|
qs = parse_qs(urlparse(ref).query)
|
||||||
|
if "id" in qs and qs["id"][0].isdigit():
|
||||||
|
return int(qs["id"][0])
|
||||||
|
sys.exit(f"Error: keine Content-ID erkennbar in '{ref}'")
|
||||||
|
|
||||||
|
|
||||||
|
def slugify(text: str) -> str:
|
||||||
|
text = text.lower()
|
||||||
|
for a, b in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")):
|
||||||
|
text = text.replace(a, b)
|
||||||
|
text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
|
||||||
|
return text[:60] or "inhalt"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_content(session: requests.Session, content_id: int) -> dict:
|
||||||
|
"""Löst eine Content-ID auf den zugrundeliegenden Datensatz auf
|
||||||
|
(WikiPage oder Post) und liefert Titel, Inhalt und Metadaten."""
|
||||||
|
envelope = api_get(session, f"/content/{content_id}")
|
||||||
|
meta = envelope.get("metadata", {})
|
||||||
|
model = meta.get("object_model", "")
|
||||||
|
object_id = meta.get("object_id")
|
||||||
|
page_url = meta.get("url", "")
|
||||||
|
topics = [t["name"] for t in envelope.get("topics", [])]
|
||||||
|
updated_at = meta.get("updated_at", "")
|
||||||
|
|
||||||
|
if model.endswith("WikiPage"):
|
||||||
|
data = api_get(session, f"/wiki/page/{object_id}")
|
||||||
|
title = data.get("title", "") or f"Wiki-Seite {object_id}"
|
||||||
|
content = data.get("latest_revision", {}).get("content", "")
|
||||||
|
kind = f"WikiPage (page {object_id})"
|
||||||
|
seg = page_url.rstrip("/").split("/")[-1]
|
||||||
|
slug = seg if seg and not seg.isdigit() else slugify(title)
|
||||||
|
elif model.endswith("Post"):
|
||||||
|
data = api_get(session, f"/post/{object_id}")
|
||||||
|
content = data.get("data", {}).get("message") or data.get("message", "")
|
||||||
|
first = content.splitlines()[0].lstrip("# ").strip() if content else ""
|
||||||
|
title = first or f"Post {object_id}"
|
||||||
|
kind = f"Post (id {object_id})"
|
||||||
|
slug = slugify(title)
|
||||||
|
else:
|
||||||
|
sys.exit(f"Error: Objekttyp nicht unterstützt: {model or '(unbekannt)'}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"content_id": content_id,
|
||||||
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
"kind": kind,
|
||||||
|
"topics": topics,
|
||||||
|
"updated_at": updated_at,
|
||||||
|
"page_url": f"https://humhub.inmedias.it{page_url}" if page_url else "",
|
||||||
|
"slug": slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_resource(info: dict) -> str:
|
||||||
|
"""Inhalt mit Herkunfts-Kopf als Markdown-Kommentar."""
|
||||||
|
perma = f"https://humhub.inmedias.it/content/perma?id={info['content_id']}"
|
||||||
|
header = (
|
||||||
|
"<!--\n"
|
||||||
|
f"Quelle: {perma}\n"
|
||||||
|
f"Seite: {info['page_url']}\n"
|
||||||
|
f"Titel: {info['title']}\n"
|
||||||
|
f"Typ: {info['kind']}\n"
|
||||||
|
f"Topics: {', '.join(info['topics']) or '(keine)'}\n"
|
||||||
|
f"Stand: {info['updated_at']}\n"
|
||||||
|
f"Abgerufen: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
|
||||||
|
"-->\n\n"
|
||||||
|
)
|
||||||
|
return header + info["content"].rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_fetch(args: argparse.Namespace) -> None:
|
||||||
|
session, _ = get_session()
|
||||||
|
content_id = parse_content_ref(args.ref)
|
||||||
|
info = resolve_content(session, content_id)
|
||||||
|
body = render_resource(info)
|
||||||
|
|
||||||
|
if args.save:
|
||||||
|
outdir = Path(args.out)
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = outdir / f"humhub-{content_id}-{info['slug']}.md"
|
||||||
|
path.write_text(body, encoding="utf-8")
|
||||||
|
print(f"Gespeichert: {path}")
|
||||||
|
print(f" {info['kind']} — '{info['title']}' ({len(info['content'])} Zeichen)")
|
||||||
|
else:
|
||||||
|
sys.stdout.write(body)
|
||||||
|
|
||||||
|
|
||||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description="HumHub REST API CLI")
|
parser = argparse.ArgumentParser(description="HumHub REST API CLI")
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# fetch
|
||||||
|
p = sub.add_parser("fetch", help="Content-ID/Perma-Link auflösen und Inhalt holen (WikiPage oder Post)")
|
||||||
|
p.add_argument("ref", help="Content-ID oder Perma-Link (…/content/perma?id=NNNN)")
|
||||||
|
p.add_argument("--save", action="store_true", help="In Datei ablegen statt auf stdout")
|
||||||
|
p.add_argument("--out", default="resources", help="Zielverzeichnis für --save (Standard: resources/)")
|
||||||
|
p.set_defaults(func=cmd_fetch)
|
||||||
|
|
||||||
# wiki-list
|
# wiki-list
|
||||||
p = sub.add_parser("wiki-list", help="Alle Wiki-Seiten auflisten")
|
p = sub.add_parser("wiki-list", help="Alle Wiki-Seiten auflisten")
|
||||||
p.add_argument("filter", nargs="?", help="Filter nach Titel (case-insensitiv)")
|
p.add_argument("filter", nargs="?", help="Filter nach Titel (case-insensitiv)")
|
||||||
@@ -224,9 +379,13 @@ def main() -> None:
|
|||||||
p.add_argument("--parent-id", type=int, default=None, dest="parent_id", help="ID der Oberseite")
|
p.add_argument("--parent-id", type=int, default=None, dest="parent_id", help="ID der Oberseite")
|
||||||
p.set_defaults(func=cmd_wiki_update)
|
p.set_defaults(func=cmd_wiki_update)
|
||||||
|
|
||||||
|
# space-list
|
||||||
|
p = sub.add_parser("space-list", help="Spaces auflisten mit contentcontainer_id")
|
||||||
|
p.set_defaults(func=cmd_space_list)
|
||||||
|
|
||||||
# wiki-create
|
# wiki-create
|
||||||
p = sub.add_parser("wiki-create", help="Neue Wiki-Seite anlegen")
|
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("container_id", help="Space-Slug, URL oder numerische Container-ID")
|
||||||
p.add_argument("title", help="Seitentitel")
|
p.add_argument("title", help="Seitentitel")
|
||||||
p.add_argument("file", nargs="?", default=None, help="Markdown-Datei oder '-' für stdin (optional)")
|
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("--topics", default=None, help="Komma-getrennte Topic-Namen")
|
||||||
@@ -235,13 +394,13 @@ def main() -> None:
|
|||||||
|
|
||||||
# post-list
|
# post-list
|
||||||
p = sub.add_parser("post-list", help="Posts eines Containers auflisten")
|
p = sub.add_parser("post-list", help="Posts eines Containers auflisten")
|
||||||
p.add_argument("container_id", type=int, help="Container-ID")
|
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
|
||||||
p.add_argument("--limit", type=int, default=10, help="Max. Anzahl Posts (Standard: 10)")
|
p.add_argument("--limit", type=int, default=10, help="Max. Anzahl Posts (Standard: 10)")
|
||||||
p.set_defaults(func=cmd_post_list)
|
p.set_defaults(func=cmd_post_list)
|
||||||
|
|
||||||
# post-create
|
# post-create
|
||||||
p = sub.add_parser("post-create", help="Neuen Post erstellen")
|
p = sub.add_parser("post-create", help="Neuen Post erstellen")
|
||||||
p.add_argument("container_id", type=int, help="Container-ID")
|
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
|
||||||
p.add_argument("file", help="Markdown-Datei oder '-' für stdin")
|
p.add_argument("file", help="Markdown-Datei oder '-' für stdin")
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user