From 212ce3b129ef2db6836d36466504c5338951a8e0 Mon Sep 17 00:00:00 2001 From: Ekaropolus Date: Sun, 7 Sep 2025 04:16:17 -0600 Subject: [PATCH] Admin for Suscribing to Messenger Servers --- pxy_meta_pages/admin.py | 301 ++++++++++++++++++++++++++++++++-------- 1 file changed, 246 insertions(+), 55 deletions(-) diff --git a/pxy_meta_pages/admin.py b/pxy_meta_pages/admin.py index fe215c9..d9cf8e5 100644 --- a/pxy_meta_pages/admin.py +++ b/pxy_meta_pages/admin.py @@ -1,24 +1,71 @@ +import json import requests from django.conf import settings -from django.contrib import admin -from .models import FacebookPageAssistant -from .services import FacebookService # Import FacebookService for API calls +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 panel configuration for managing Facebook Page Assistants. + 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" + "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 = ["subscribe_to_webhook", "check_subscription_status"] + 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" @@ -27,79 +74,223 @@ class FacebookPageAssistantAdmin(admin.ModelAdmin): return obj.events.filter(event_type__code="share").count() share_count.short_description = "Shares" - def subscribe_to_webhook(self, request, queryset): + # ===================================================================== + # ACTION 1: Ensure required fields are subscribed (feed + Messenger) + # ===================================================================== + def ensure_feed_and_messenger_subscription(self, request, queryset): """ - Subscribes selected pages to Facebook webhooks using FacebookService. + 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: - page_access_token = fb_service.get_page_access_token(page.page_id) - if not page_access_token: - self.message_user(request, f"Failed to get access token for {page.page_name}", level="error") - continue - - url = f"https://graph.facebook.com/v22.0/{page.page_id}/subscribed_apps" - data = { - "subscribed_fields": "feed,mention", - "access_token": page_access_token - } - try: - response = requests.post(url, data=data) - response.raise_for_status() - page.is_subscribed = True - page.save() - self.message_user(request, f"Successfully subscribed {page.page_name} to webhooks.") + # 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: - self.message_user(request, f"Failed to subscribe {page.page_name}: {e}", level="error") + # 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, + ) - subscribe_to_webhook.short_description = "Subscribe selected pages to webhooks" + 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): """ - Checks whether selected pages are subscribed to Facebook webhooks using FacebookService. + Shows the actual subscribed_fields for each Page. """ fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN) for page in queryset: - page_access_token = fb_service.get_page_access_token(page.page_id) - if not page_access_token: - self.message_user(request, f"Failed to get access token for {page.page_name}", level="error") - continue - - url = f"https://graph.facebook.com/v22.0/{page.page_id}/subscribed_apps?access_token={page_access_token}" - try: - response = requests.get(url) - response.raise_for_status() - data = response.json() - - if "data" in data and len(data["data"]) > 0: - page.is_subscribed = True - self.message_user(request, f"{page.page_name} is subscribed.") + # 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.is_subscribed = False - self.message_user(request, f"{page.page_name} is NOT subscribed.", level="warning") + 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, + ) - page.save() except requests.RequestException as e: - self.message_user(request, f"Failed to check subscription for {page.page_name}: {e}", level="error") + self.message_user( + request, f"[{page.page_name}] Check failed: {e}", level=messages.ERROR + ) - check_subscription_status.short_description = "Check webhook subscription status" + 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" -from .models import EventType @admin.register(EventType) class EventTypeAdmin(admin.ModelAdmin): list_display = ("code", "label") search_fields = ("code", "label") -from .models import BotInteraction @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",) - + list_display = ("page", "object_id", "parent_object_id", "platform", "created_at") + search_fields = ("object_id", "prompt", "bot_response") + list_filter = ("platform",)