import json import requests from django.conf import settings from django.contrib import admin, messages from .models import FacebookPageAssistant, EventType, BotInteraction from .services import FacebookService # Required fields we want on every Page REQUIRED_FIELDS = [ # Page feed (comments/shares/mentions) "feed", "mention", # Messenger "messages", "messaging_postbacks", "message_reads", "message_deliveries", "message_reactions", "message_echoes", ] APP_ID = getattr(settings, "FACEBOOK_APP_ID", None) # optional (nice-to-have for filtering) def _graph_get(url, params): r = requests.get(url, params=params, timeout=15) # Graph often returns 200 even for failures with {"error":{...}} data = r.json() if r.content else {} if "error" in data: raise requests.RequestException(json.dumps(data["error"])) r.raise_for_status() return data def _graph_post(url, data): r = requests.post(url, data=data, timeout=15) data = r.json() if r.content else {} if "error" in data: raise requests.RequestException(json.dumps(data["error"])) r.raise_for_status() return data @admin.register(FacebookPageAssistant) class FacebookPageAssistantAdmin(admin.ModelAdmin): """ Admin for wiring a Facebook Page to your assistant and managing webhook subs. """ 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", ] # ----- small counters ----- def comment_count(self, obj): return obj.events.filter(event_type__code="comment").count() comment_count.short_description = "Comments" def share_count(self, obj): return obj.events.filter(event_type__code="share").count() share_count.short_description = "Shares" # ===================================================================== # ACTION 1: Ensure required fields are subscribed (feed + Messenger) # ===================================================================== def ensure_feed_and_messenger_subscription(self, request, queryset): """ For each selected Page: - fetch Page Access Token with FacebookService - read current subscribed_fields - add any missing REQUIRED_FIELDS """ fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN) for page in queryset: try: # 1) token page_token = getattr(fb_service, "get_page_access_token", None) if callable(page_token): page_access_token = page_token(page.page_id) else: # fallback to private method name in case your svc only exposes _get_page_access_token page_access_token = fb_service._get_page_access_token(page.page_id) # noqa if not page_access_token: self.message_user( request, f"[{page.page_name}] Unable to get Page Access Token.", level=messages.ERROR, ) continue # 2) read existing url_list = f"https://graph.facebook.com/v22.0/{page.page_id}/subscribed_apps" data = _graph_get(url_list, {"access_token": page_access_token}) or {} entries = data.get("data", []) # pick this app's entry (if APP_ID known), else first entry if any 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 = set(app_entry.get("subscribed_fields", [])) if app_entry else set() required = set(REQUIRED_FIELDS) union_fields = sorted(current | required) # 3) update only if needed if required - current: _graph_post( f"https://graph.facebook.com/v22.0/{page.page_id}/subscribed_apps", { "subscribed_fields": ",".join(union_fields), "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. Fields now include: {', '.join(union_fields)}", level=messages.SUCCESS, ) else: page.is_subscribed = True page.save(update_fields=["is_subscribed"]) self.message_user( request, f"[{page.page_name}] Already has all required fields: {', '.join(sorted(current))}", level=messages.INFO, ) except requests.RequestException as e: # try to decode Graph error for clarity msg = str(e) try: err = json.loads(msg) code = err.get("code") sub = err.get("error_subcode") err_msg = err.get("message", "Graph error") self.message_user( request, f"[{page.page_name}] Graph error (code={code}, subcode={sub}): {err_msg}", level=messages.ERROR, ) except Exception: self.message_user( request, f"[{page.page_name}] Subscription failed: {msg}", 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) on selected Pages" # ===================================================================== # ACTION 2: Check status (show exact fields) # ===================================================================== def check_subscription_status(self, request, queryset): """ Shows the actual subscribed_fields for each Page. """ fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN) for page in queryset: try: # token page_token = getattr(fb_service, "get_page_access_token", None) if callable(page_token): page_access_token = page_token(page.page_id) else: page_access_token = fb_service._get_page_access_token(page.page_id) # noqa if not page_access_token: self.message_user( request, f"[{page.page_name}] Unable to get Page Access Token.", level=messages.ERROR, ) continue url = f"https://graph.facebook.com/v22.0/{page.page_id}/subscribed_apps" data = _graph_get(url, {"access_token": page_access_token}) or {} 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 [] has_required = set(REQUIRED_FIELDS).issubset(set(fields)) page.is_subscribed = bool(fields) page.save(update_fields=["is_subscribed"]) level = messages.SUCCESS if has_required else messages.WARNING self.message_user( request, f"[{page.page_name}] Subscribed fields: {', '.join(fields) or '(none)'}", level=level, ) except requests.RequestException as e: self.message_user( request, f"[{page.page_name}] Check failed: {e}", level=messages.ERROR ) check_subscription_status.short_description = "Check webhook subscription fields on selected Pages" # ===================================================================== # ACTION 3: Probe Messenger access (lightweight) # ===================================================================== def probe_messenger_access(self, request, queryset): """ Tries /{PAGE_ID}/conversations to confirm Messenger perms are usable. (If app is in Dev Mode, only app roles will appear here.) """ fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN) for page in queryset: try: page_token = getattr(fb_service, "get_page_access_token", None) if callable(page_token): page_access_token = page_token(page.page_id) else: page_access_token = fb_service._get_page_access_token(page.page_id) # noqa if not page_access_token: self.message_user( request, f"[{page.page_name}] Unable to get Page Access Token.", level=messages.ERROR, ) continue url = f"https://graph.facebook.com/v22.0/{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 here.", level=messages.SUCCESS, ) except requests.RequestException as e: # common Graph codes for perms/token issues: # 190 invalid/expired token, 200 permissions error, 10 permission denied msg = str(e) hint = "" if any(x in msg for x in ('"code": 190', "Invalid OAuth 2.0")): 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, ) probe_messenger_access.short_description = "Probe Messenger access on selected Pages" @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",)