Merge pull request 'plugins/mediawiki: wiki.py als Skill (Wiki lesen/rendern/schreiben)' (#1) from feature/mediawiki-plugin into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -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.
|
||||||
Executable
+181
@@ -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()
|
||||||
Reference in New Issue
Block a user