diff --git a/homelab-ai-bot/core/wordpress_client.py b/homelab-ai-bot/core/wordpress_client.py new file mode 100644 index 00000000..d7346052 --- /dev/null +++ b/homelab-ai-bot/core/wordpress_client.py @@ -0,0 +1,225 @@ +"""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)