homelab-brain/homelab-ai-bot/core/wordpress_client.py
root fb9ab8ab81 feat: wordpress_client.py — REST API für Blog-Statistiken
Funktionen:
- get_post_stats(days) — Posts heute/gestern/Woche/Monat
- get_pending_comments() — warten auf Freigabe
- get_top_posts(limit) — Top Posts nach Zugriffen/Kommentaren
- get_plugin_status() — aktive/inaktive Plugins
- check_connectivity() — WordPress erreichbar?
- format_overview() — /wp Command Output

Nutzt Tailscale IP für Zugriff auf CT 101 WordPress.

Made-with: Cursor
2026-03-09 13:10:26 +07:00

225 lines
7.1 KiB
Python

"""WordPress REST API Client — Blog-Statistiken und Status."""
import requests
from datetime import datetime, timedelta
from typing import Optional
# REST API Endpoints (CT 101 auf pve-hetzner, über Cloudflare Tunnel)
WP_URL = "" # Wird aus homelab.conf geladen
WP_USER = ""
WP_PASSWORD = ""
def init(cfg):
"""Initialisierung mit homelab.conf Daten."""
global WP_URL, WP_USER, WP_PASSWORD
ct_101 = None
for c in cfg.containers:
if c.vmid == 101:
ct_101 = c
break
if not ct_101:
return False
# WordPress erreichbar über Tailscale IP oder Domain
WP_URL = f"http://{ct_101.tailscale_ip}"
WP_USER = "admin"
WP_PASSWORD = cfg.passwords.get("wordpress_admin", "")
if not WP_PASSWORD:
# Fallback: aus raw config
WP_PASSWORD = cfg.raw.get("PW_WP_ADMIN", "")
return bool(WP_PASSWORD)
def _request(endpoint: str, method: str = "GET", params: dict = None) -> Optional[dict]:
"""REST API Request mit Basic Auth."""
try:
url = f"{WP_URL}/wp-json/wp/v2{endpoint}"
auth = (WP_USER, WP_PASSWORD)
timeout = 10
if method == "GET":
resp = requests.get(url, auth=auth, params=params, timeout=timeout)
else:
resp = requests.request(method, url, auth=auth, json=params, timeout=timeout)
resp.raise_for_status()
return resp.json()
except Exception as e:
return None
def check_connectivity() -> bool:
"""Ist WordPress erreichbar?"""
try:
result = _request("/posts?per_page=1")
return result is not None
except:
return False
def get_post_stats(days: int = 1) -> dict:
"""
Posts-Statistik für einen Tag.
Returns:
{
"today": 3,
"yesterday": 5,
"this_week": 12,
"this_month": 45
}
"""
try:
now = datetime.now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
yesterday_start = today_start - timedelta(days=1)
week_start = today_start - timedelta(days=today_start.weekday())
month_start = today_start.replace(day=1)
# Alle Posts mit Publish-Datum abfragen (kann viele sein, pagiert)
posts = []
page = 1
while True:
batch = _request("/posts", params={"per_page": 100, "page": page, "status": "publish"})
if not batch or len(batch) == 0:
break
posts.extend(batch)
page += 1
if page > 5: # Max 500 Posts pro Abfrage
break
if not posts:
return {"today": 0, "yesterday": 0, "this_week": 0, "this_month": 0}
today_count = sum(1 for p in posts if datetime.fromisoformat(p["date"].replace("Z", "+00:00")).replace(tzinfo=None) >= today_start)
yesterday_count = sum(1 for p in posts if yesterday_start <= datetime.fromisoformat(p["date"].replace("Z", "+00:00")).replace(tzinfo=None) < today_start)
week_count = sum(1 for p in posts if datetime.fromisoformat(p["date"].replace("Z", "+00:00")).replace(tzinfo=None) >= week_start)
month_count = sum(1 for p in posts if datetime.fromisoformat(p["date"].replace("Z", "+00:00")).replace(tzinfo=None) >= month_start)
return {
"today": today_count,
"yesterday": yesterday_count,
"this_week": week_count,
"this_month": month_count,
}
except Exception as e:
return {"error": str(e)}
def get_pending_comments() -> int:
"""Wie viele Kommentare warten auf Freigabe?"""
try:
comments = _request("/comments", params={"status": "hold", "per_page": 100})
if comments is None:
return -1
return len(comments) if isinstance(comments, list) else 0
except:
return -1
def get_top_posts(limit: int = 5) -> list[dict]:
"""
Top Posts nach Zugriffen (nutzt Matomo-Tracking wenn vorhanden).
Fallback: neueste Posts mit höchster Kommentar-Anzahl.
Returns:
[
{"title": "...", "visits": 123, "comments": 5, "url": "..."},
...
]
"""
try:
posts = _request("/posts", params={"per_page": limit, "orderby": "date", "order": "desc", "status": "publish"})
if not posts:
return []
result = []
for p in posts:
result.append({
"title": p.get("title", {}).get("rendered", "Untitled")[:60],
"visits": -1, # Matomo-Integration später
"comments": p.get("_links", {}).get("replies", [{}])[0].get("count", 0) if "replies" in p.get("_links", {}) else 0,
"url": p.get("link", ""),
"date": p.get("date", ""),
})
return result
except Exception as e:
return []
def get_plugin_status() -> dict:
"""
Plugin-Status (active/inactive).
Returns:
{
"total": 15,
"active": 12,
"inactive": 3,
"active_list": ["plugin1", "plugin2", ...],
"inactive_list": ["plugin4", ...]
}
"""
try:
# Plugins können nur über /wp/v2/plugins abgefragt werden (braucht Admin-Token)
# Als Fallback: /settings auslesen falls konfiguriert
plugins = _request("/plugins")
if not plugins:
return {"error": "Plugin API nicht erreichbar (braucht higher permissions)"}
if isinstance(plugins, dict) and "code" in plugins:
return {"error": plugins.get("message", "Permission denied")}
active = [p["name"] for p in plugins if p.get("status") == "active"]
inactive = [p["name"] for p in plugins if p.get("status") != "active"]
return {
"total": len(plugins),
"active": len(active),
"inactive": len(inactive),
"active_list": active,
"inactive_list": inactive,
}
except:
return {"error": "Plugins nicht abrufbar"}
def format_overview(cfg) -> str:
"""Kurzer Überblick für /wp Command."""
if not init(cfg):
return "❌ WordPress nicht konfiguriert"
if not check_connectivity():
return "❌ WordPress nicht erreichbar"
lines = ["📝 **WordPress Status (arakavanews.com)**\n"]
stats = get_post_stats(days=1)
lines.append(f"📊 Posts: heute {stats.get('today', 0)}, diese Woche {stats.get('this_week', 0)}")
pending = get_pending_comments()
if pending > 0:
lines.append(f"💬 ⏳ {pending} Kommentare warten auf Freigabe")
else:
lines.append(f"💬 Keine pending Kommentare")
top = get_top_posts(limit=3)
if top:
lines.append("\n🔝 Top Posts:")
for p in top:
lines.append(f"{p['title']} ({p['comments']} 💬)")
plugins = get_plugin_status()
if "error" not in plugins:
lines.append(f"\n🔌 Plugins: {plugins['active']} aktiv, {plugins['inactive']} inaktiv")
return "\n".join(lines)