# pxy_meta_pages/admin.py from __future__ import annotations import json from typing import Optional, Dict, Any import requests from django.conf import settings from django.contrib import admin, messages from .models import FacebookPageAssistant, EventType, BotInteraction from .services import FacebookService # ----------------------------------------------------------------------------- # Config # ----------------------------------------------------------------------------- FACEBOOK_API_VERSION: str = getattr(settings, "FACEBOOK_API_VERSION", "v22.0") APP_ID: Optional[str] = getattr(settings, "FACEBOOK_APP_ID", None) # Fields we require on every Page subscription (Page Feed + Messenger) REQUIRED_FIELDS = [ # Page feed (comments/shares/mentions) "feed", "mention", # Messenger "messages", "messaging_postbacks", "message_reads", "message_deliveries", "message_reactions", "message_echoes", ] # ----------------------------------------------------------------------------- # Small Graph helpers with consistent error handling # ----------------------------------------------------------------------------- def _graph_get(url: str, params: Dict[str, Any]) -> Dict[str, Any]: """GET wrapper that raises RequestException on Graph errors.""" resp = requests.get(url, params=params, timeout=15) data = resp.json() if resp.content else {} if isinstance(data, dict) and "error" in data: # Normalize to RequestException so callers can unify handling raise requests.RequestException(json.dumps(data["error"])) resp.raise_for_status() return data or {} def _graph_post(url: str, data: Dict[str, Any]) -> Dict[str, Any]: """POST wrapper that raises RequestException on Graph errors.""" resp = requests.post(url, data=data, timeout=15) payload = resp.json() if resp.content else {} if isinstance(payload, dict) and "error" in payload: raise requests.RequestException(json.dumps(payload["error"])) resp.raise_for_status() return payload or {} def _decode_graph_error(e: requests.RequestException) -> str: """ Attempt to pretty-print a Graph API error dict, else return the raw message. """ msg = str(e) try: err = json.loads(msg) # Typical Graph error shape code = err.get("code") sub = err.get("error_subcode") text = err.get("message", "Graph error") return f"Graph error (code={code}, subcode={sub}): {text}" except Exception: return msg def _get_page_token(fb_service: FacebookService, page_id: str) -> Optional[str]: """ Works with either a public get_page_access_token or the private _get_page_access_token. """ getter = getattr(fb_service, "get_page_access_token", None) if callable(getter): return getter(page_id) private_getter = getattr(fb_service, "_get_page_access_token", None) if callable(private_getter): return private_getter(page_id) return None # ----------------------------------------------------------------------------- # Admins # ----------------------------------------------------------------------------- @admin.register(FacebookPageAssistant) class FacebookPageAssistantAdmin(admin.ModelAdmin): """ Admin for wiring a Facebook Page to an OpenAI assistant and managing webhook subscriptions. """ list_display = ( "page_name", "page_id", "assistant", "is_subscribed", "created_at", "comment_count", "share_count", ) search_fields = ("page_name", "page_id", "assistant__name") list_filter = ("is_subscribed", "assistant") actions = [ "ensure_feed_and_messenger_subscription", "check_subscription_status", "probe_messenger_access", ] # ----- Counters ---------------------------------------------------------- def comment_count(self, obj: FacebookPageAssistant) -> int: return obj.events.filter(event_type__code="comment").count() comment_count.short_description = "Comments" def share_count(self, obj: FacebookPageAssistant) -> int: return obj.events.filter(event_type__code="share").count() share_count.short_description = "Shares" # ----- Action 1: Ensure required fields (feed + Messenger) -------------- def ensure_feed_and_messenger_subscription(self, request, queryset): """ For each selected Page: 1) Fetch the Page Access Token via FacebookService. 2) Read current subscribed_fields. 3) Add any missing REQUIRED_FIELDS in a single POST. """ fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN) for page in queryset: try: page_access_token = _get_page_token(fb_service, page.page_id) if not page_access_token: self.message_user( request, f"[{page.page_name}] Unable to obtain Page Access Token.", level=messages.ERROR, ) continue list_url = f"https://graph.facebook.com/{FACEBOOK_API_VERSION}/{page.page_id}/subscribed_apps" current_data = _graph_get(list_url, {"access_token": page_access_token}) entries = current_data.get("data", []) # If APP_ID is known, narrow to our app row; otherwise use first row if present app_entry = None if APP_ID: app_entry = next((e for e in entries if str(e.get("id")) == str(APP_ID)), None) if app_entry is None and entries: app_entry = entries[0] current_fields = set(app_entry.get("subscribed_fields", [])) if app_entry else set() required = set(REQUIRED_FIELDS) if required - current_fields: new_fields_csv = ",".join(sorted(current_fields | required)) _graph_post(list_url, { "subscribed_fields": new_fields_csv, "access_token": page_access_token, }) page.is_subscribed = True page.save(update_fields=["is_subscribed"]) self.message_user( request, f"[{page.page_name}] Subscribed/updated. Now includes: {new_fields_csv}", level=messages.SUCCESS, ) else: page.is_subscribed = True page.save(update_fields=["is_subscribed"]) self.message_user( request, f"[{page.page_name}] Already has required fields.", level=messages.INFO, ) except requests.RequestException as e: self.message_user( request, f"[{page.page_name}] {_decode_graph_error(e)}", level=messages.ERROR, ) except Exception as e: self.message_user( request, f"[{page.page_name}] Unexpected error: {e}", level=messages.ERROR, ) ensure_feed_and_messenger_subscription.short_description = "Ensure Webhooks (feed + Messenger)" # ----- Action 2: Check subscription status ------------------------------ def check_subscription_status(self, request, queryset): """ Shows the exact subscribed_fields currently active for each Page. """ fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN) for page in queryset: try: page_access_token = _get_page_token(fb_service, page.page_id) if not page_access_token: self.message_user( request, f"[{page.page_name}] Unable to obtain Page Access Token.", level=messages.ERROR, ) continue url = f"https://graph.facebook.com/{FACEBOOK_API_VERSION}/{page.page_id}/subscribed_apps" data = _graph_get(url, {"access_token": page_access_token}) entries = data.get("data", []) app_entry = None if APP_ID: app_entry = next((e for e in entries if str(e.get("id")) == str(APP_ID)), None) if app_entry is None and entries: app_entry = entries[0] fields = app_entry.get("subscribed_fields", []) if app_entry else [] page.is_subscribed = bool(fields) page.save(update_fields=["is_subscribed"]) has_all = set(REQUIRED_FIELDS).issubset(set(fields)) level = messages.SUCCESS if has_all else messages.WARNING self.message_user( request, f"[{page.page_name}] Subscribed fields: {', '.join(fields) if fields else '(none)'}", level=level, ) except requests.RequestException as e: self.message_user( request, f"[{page.page_name}] {_decode_graph_error(e)}", level=messages.ERROR, ) except Exception as e: self.message_user( request, f"[{page.page_name}] Unexpected error: {e}", level=messages.ERROR, ) check_subscription_status.short_description = "Check webhook subscription fields" # ----- Action 3: Probe Messenger access --------------------------------- def probe_messenger_access(self, request, queryset): """ Light probe for Messenger perms using /{PAGE_ID}/conversations. (In Dev Mode, you’ll only see app-role users here.) """ fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN) for page in queryset: try: page_access_token = _get_page_token(fb_service, page.page_id) if not page_access_token: self.message_user( request, f"[{page.page_name}] Unable to obtain Page Access Token.", level=messages.ERROR, ) continue url = f"https://graph.facebook.com/{FACEBOOK_API_VERSION}/{page.page_id}/conversations" data = _graph_get(url, {"access_token": page_access_token, "limit": 1}) total = len(data.get("data", [])) self.message_user( request, ( f"[{page.page_name}] Messenger probe OK. Conversations sample: {total}. " "Note: in Dev Mode you’ll only see app-role users." ), level=messages.SUCCESS, ) except requests.RequestException as e: msg = _decode_graph_error(e) # Add quick hints for common codes hint = "" if '"code": 190' in msg or "Invalid OAuth 2.0" in msg: hint = " (Token invalid/expired)" elif '"code": 200' in msg: hint = " (Permissions error: check pages_messaging & pages_manage_metadata; app roles or Advanced Access)" elif '"code": 10' in msg: hint = " (Permission denied: user role or access level missing)" self.message_user( request, f"[{page.page_name}] Messenger probe failed: {msg}{hint}", level=messages.ERROR, ) except Exception as e: self.message_user( request, f"[{page.page_name}] Unexpected error: {e}", level=messages.ERROR, ) probe_messenger_access.short_description = "Probe Messenger access" @admin.register(EventType) class EventTypeAdmin(admin.ModelAdmin): list_display = ("code", "label") search_fields = ("code", "label") @admin.register(BotInteraction) class BotInteractionAdmin(admin.ModelAdmin): list_display = ("page", "object_id", "parent_object_id", "platform", "created_at") search_fields = ("object_id", "prompt", "bot_response") list_filter = ("platform",)