plugins/mediawiki: wiki.py als Skill (Wiki lesen/rendern/schreiben) #1

Merged
sko merged 2 commits from feature/mediawiki-plugin into main 2026-07-16 10:10:24 +02:00
5 changed files with 271 additions and 0 deletions
+5
View File
@@ -27,6 +27,11 @@
"name": "humhub", "name": "humhub",
"source": "./plugins/humhub", "source": "./plugins/humhub",
"description": "HumHub via REST-API: Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren (Token lokal)." "description": "HumHub via REST-API: Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren (Token lokal)."
},
{
"name": "mediawiki",
"source": "./plugins/mediawiki",
"description": "MediaWiki-Seiten/Templates lesen, rendern und schreiben via wiki.py (Instanzen wiki & devwiki; Zugangsdaten lokal)."
} }
] ]
} }
@@ -0,0 +1,8 @@
{
"name": "mediawiki",
"version": "0.1.0",
"description": "MediaWiki-Seiten/Templates lesen, rendern und schreiben via wiki.py (fetch/update/render/search, Instanzen wiki & devwiki).",
"author": {
"name": "inmedias.it"
}
}
@@ -0,0 +1,14 @@
# Zugangsdaten fuer wiki.py — kopieren nach ~/.config/dokumentierer/.env
# oder .env im Arbeitsverzeichnis, oder als Umgebungsvariablen setzen.
# NICHT committen. Am besten ein eigener Bot-/Service-Account mit Schreibrechten.
# Prod-Instanz (wiki -> https://wiki.inmedias.it)
MEDIAWIKI_USER=botuser
MEDIAWIKI_PASSWORD=change-me
# MEDIAWIKI_URL=https://wiki.inmedias.it # nur falls abweichend
# Test-Instanz (devwiki -> https://devwiki.inmedias.it)
# Faellt auf MEDIAWIKI_USER/PASSWORD zurueck, wenn nicht gesetzt.
DEVMEDIAWIKI_USER=botuser
DEVMEDIAWIKI_PASSWORD=change-me
# DEVMEDIAWIKI_URL=https://devwiki.inmedias.it # nur falls abweichend
@@ -0,0 +1,63 @@
---
name: mediawiki
description: MediaWiki-Seiten und -Templates lesen, rendern und schreiben. Use when editing/fetching wiki pages (wiki.inmedias.it, devwiki.inmedias.it) or when the user mentions "wiki", "Vorlage/Template", "Kundenseite".
version: 0.1.0
---
# MediaWiki (wiki.py)
Liest, rendert und schreibt MediaWiki-Seiten/Templates über die mitgelieferte
CLI `wiki.py` (mwclient + Login). Unterstützt mehrere Instanzen.
## Grundregel
Wiki-Seiten/Templates werden **ausschließlich** über `wiki.py` (oder ein
MediaWiki-MCP) bearbeitet — **niemals** per `ssh <host> php .../maintenance/edit.php`
oder anderen direkten Server-Eingriffen. Reine Lese-/Render-Checks
(`curl ...?action=render`) sind ok; nur das **Schreiben** läuft über `wiki.py`.
## Ausführen
`wiki.py` ist ein eigenständiges `uv run --script` (Inline-Deps: `mwclient`,
`python-dotenv`). Aus dem Skill-Verzeichnis ausführen:
```bash
./wiki.py -w <instanz> <befehl> ...
```
`-w` / `--wiki` wählt die Instanz (Default: `wiki`):
- `wiki` → https://wiki.inmedias.it (Prod)
- `devwiki` → https://devwiki.inmedias.it (Test)
### Befehle
```bash
./wiki.py -w devwiki fetch "Kunde:Beispiel GmbH" # Wikitext -> stdout
./wiki.py -w devwiki render "Kunde:Beispiel GmbH" # gerendertes HTML -> stdout
./wiki.py -w devwiki search "Suchbegriff" [-n <namespace>] # Titelsuche
./wiki.py -w devwiki update "Kunde:Beispiel GmbH" datei.wiki --summary "..." # speichern
./wiki.py -w wiki update "Titel" - --summary "..." # Inhalt von stdin
```
Tipp: Beim Schreiben den Inhalt aus einer Datei (`datei.wiki`) oder `-` (stdin)
übergeben — nicht als langes Shell-Argument (Sonderzeichen/Backticks!).
## Zugangsdaten
`wiki.py` bringt **keine** Zugangsdaten mit. Jede/r hinterlegt eigene Creds als
Umgebungsvariablen oder in einer `.env` (Reihenfolge: `~/.config/dokumentierer/.env`,
dann `.env` im aktuellen Verzeichnis, dann bereits gesetzte Env-Variablen). Siehe
`.env.example`:
- `MEDIAWIKI_USER`, `MEDIAWIKI_PASSWORD` — für die Prod-Instanz (`wiki`)
- `DEVMEDIAWIKI_USER`, `DEVMEDIAWIKI_PASSWORD` — für `devwiki` (fällt auf die
`MEDIAWIKI_*`-Werte zurück, wenn nicht gesetzt)
- optional `MEDIAWIKI_URL` / `DEVMEDIAWIKI_URL` — nur nötig, wenn die
Default-URLs abweichen
Es empfiehlt sich ein eigener Bot-/Service-Account mit den nötigen Schreibrechten.
## Hinweise
- `search` liefert Titel; `render` ist nützlich, um vor/nach dem Schreiben das
Ergebnis zu prüfen, ohne die Seite im Browser zu öffnen.
- Für neue Instanzen: `WIKI_ALIASES` oben in `wiki.py` erweitern.
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "mwclient",
# "python-dotenv",
# ]
# ///
"""CLI for fetching and updating MediaWiki pages."""
import argparse
import os
import sys
from pathlib import Path
from urllib.parse import urlparse
from dotenv import load_dotenv
import mwclient
WIKI_ALIASES = {
"wiki": ("MEDIAWIKI_URL", "https://wiki.inmedias.it"),
"devwiki": ("DEVMEDIAWIKI_URL", "https://devwiki.inmedias.it"),
}
def get_site(wiki: str = "wiki") -> mwclient.Site:
load_dotenv(Path.home() / ".config/dokumentierer/.env")
load_dotenv() # current dir, doesn't override already-set vars
if wiki not in WIKI_ALIASES:
sys.exit(f"Error: unknown wiki '{wiki}'. Known: {', '.join(WIKI_ALIASES)}")
url_env, url_default = WIKI_ALIASES[wiki]
url = os.environ.get(url_env, url_default).rstrip("/")
prefix = "DEVMEDIAWIKI_" if wiki == "devwiki" else "MEDIAWIKI_"
user = os.environ.get(f"{prefix}USER") or os.environ.get("MEDIAWIKI_USER", "")
password = os.environ.get(f"{prefix}PASSWORD") or os.environ.get("MEDIAWIKI_PASSWORD", "")
if not user:
sys.exit(f"Error: MEDIAWIKI_USER is not set in .env")
if not password:
sys.exit(f"Error: MEDIAWIKI_PASSWORD is not set in .env")
parsed = urlparse(url)
host = parsed.netloc
# mwclient expects path to end with '/'
path = (parsed.path or "/") + "/"
path = path.replace("//", "/")
scheme = parsed.scheme or "https"
site = mwclient.Site(host, path=path, scheme=scheme)
site.login(user, password)
return site
def cmd_fetch(args: argparse.Namespace) -> None:
site = get_site(args.wiki)
page = site.pages[args.title]
if not page.exists:
sys.exit(f"Error: page '{args.title}' does not exist")
print(page.text(), end="")
NAMESPACES = {
"main": 0, "talk": 1, "user": 2, "project": 4,
"file": 6, "template": 10, "category": 14,
}
def _strip_html(text: str) -> str:
"""Remove HTML tags (used to clean search snippets)."""
import re
return re.sub(r"<[^>]+>", "", text)
def cmd_search(args: argparse.Namespace) -> None:
site = get_site(args.wiki)
ns_key = args.namespace
if ns_key is None or ns_key.lower() == "all":
ns = "*"
elif ns_key.isdigit():
ns = int(ns_key)
else:
ns = NAMESPACES.get(ns_key.lower())
if ns is None:
sys.exit(f"Error: unknown namespace '{ns_key}'. "
f"Known: all, {', '.join(NAMESPACES)}")
result = site.api(
"query", list="search",
srsearch=args.query,
srnamespace=ns,
srlimit=50,
srprop="title|snippet",
)
hits = result["query"]["search"]
if not hits:
print("(keine Ergebnisse)", file=sys.stderr)
for hit in hits:
print(hit["title"])
if hit.get("snippet"):
print(f" {_strip_html(hit['snippet']).strip()}")
def cmd_render(args: argparse.Namespace) -> None:
site = get_site(args.wiki)
result = site.api("parse", page=args.title, prop="text")
print(result["parse"]["text"]["*"], end="")
def cmd_upload(args: argparse.Namespace) -> None:
path = Path(args.file_path)
if not path.exists():
sys.exit(f"Error: file not found: {path}")
site = get_site(args.wiki)
with path.open("rb") as fh:
site.upload(fh, filename=args.wiki_filename,
description=args.description or "", ignore=True)
print(f"Uploaded '{args.wiki_filename}'")
def cmd_update(args: argparse.Namespace) -> None:
if args.file == "-":
content = sys.stdin.read()
else:
try:
with open(args.file, "r", encoding="utf-8") as fh:
content = fh.read()
except OSError as exc:
sys.exit(f"Error reading file: {exc}")
site = get_site(args.wiki)
page = site.pages[args.title]
page.save(content, summary=args.summary or "")
print(f"Saved '{args.title}'")
def main() -> None:
parser = argparse.ArgumentParser(description="MediaWiki page fetch/update tool")
parser.add_argument(
"--wiki", "-w",
choices=list(WIKI_ALIASES),
default="wiki",
help="Wiki-Instanz (Standard: wiki = wiki.inmedias.it)",
)
sub = parser.add_subparsers(dest="command", required=True)
fetch_p = sub.add_parser("fetch", help="Print page wikitext to stdout")
fetch_p.add_argument("title", help="Page title")
fetch_p.set_defaults(func=cmd_fetch)
search_p = sub.add_parser("search", help="Search for pages by title")
search_p.add_argument("query", help="Search term")
search_p.add_argument("--namespace", "-n", default=None,
help="Namespace filter: all (default), main, template, category, file, ... or numeric ID")
search_p.set_defaults(func=cmd_search)
render_p = sub.add_parser("render", help="Print rendered HTML of a page to stdout")
render_p.add_argument("title", help="Page title")
render_p.set_defaults(func=cmd_render)
upload_p = sub.add_parser("upload", help="Upload a local file to the wiki file store")
upload_p.add_argument("file_path", help="Absolute path to the local file")
upload_p.add_argument("wiki_filename", help="Target filename on the wiki (without 'File:' prefix)")
upload_p.add_argument("--description", default="", help="Wikitext for the file description page")
upload_p.set_defaults(func=cmd_upload)
update_p = sub.add_parser("update", help="Save wikitext to a page")
update_p.add_argument("title", help="Page title")
update_p.add_argument("file", help="File containing wikitext, or '-' for stdin")
update_p.add_argument("--summary", default="", help="Edit summary")
update_p.set_defaults(func=cmd_update)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()