Admin for Suscribing to Messenger Servers
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Ekaropolus 2025-09-07 04:16:17 -06:00
parent c954488c28
commit 212ce3b129

View File

@ -1,24 +1,71 @@
import json
import requests import requests
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin, messages
from .models import FacebookPageAssistant
from .services import FacebookService # Import FacebookService for API calls 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) @admin.register(FacebookPageAssistant)
class FacebookPageAssistantAdmin(admin.ModelAdmin): 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 = ( list_display = (
"page_name", "page_id", "assistant", "page_name",
"is_subscribed", "created_at", "page_id",
"comment_count", "share_count" "assistant",
"is_subscribed",
"created_at",
"comment_count",
"share_count",
) )
search_fields = ("page_name", "page_id", "assistant__name") search_fields = ("page_name", "page_id", "assistant__name")
list_filter = ("is_subscribed", "assistant") 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): def comment_count(self, obj):
return obj.events.filter(event_type__code="comment").count() return obj.events.filter(event_type__code="comment").count()
comment_count.short_description = "Comments" comment_count.short_description = "Comments"
@ -27,79 +74,223 @@ class FacebookPageAssistantAdmin(admin.ModelAdmin):
return obj.events.filter(event_type__code="share").count() return obj.events.filter(event_type__code="share").count()
share_count.short_description = "Shares" 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) fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN)
for page in queryset: for page in queryset:
page_access_token = fb_service.get_page_access_token(page.page_id) 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: if not page_access_token:
self.message_user(request, f"Failed to get access token for {page.page_name}", level="error") 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 continue
url = f"https://graph.facebook.com/v22.0/{page.page_id}/subscribed_apps" url = f"https://graph.facebook.com/v22.0/{page.page_id}/subscribed_apps"
data = { data = _graph_get(url, {"access_token": page_access_token}) or {}
"subscribed_fields": "feed,mention", entries = data.get("data", [])
"access_token": page_access_token
} 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,
)
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.")
except requests.RequestException as e: except requests.RequestException as e:
self.message_user(request, f"Failed to subscribe {page.page_name}: {e}", level="error") self.message_user(
request, f"[{page.page_name}] Check failed: {e}", level=messages.ERROR
)
subscribe_to_webhook.short_description = "Subscribe selected pages to webhooks" check_subscription_status.short_description = "Check webhook subscription fields on selected Pages"
def check_subscription_status(self, request, queryset): # =====================================================================
# ACTION 3: Probe Messenger access (lightweight)
# =====================================================================
def probe_messenger_access(self, request, queryset):
""" """
Checks whether selected pages are subscribed to Facebook webhooks using FacebookService. 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) fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN)
for page in queryset: for page in queryset:
page_access_token = fb_service.get_page_access_token(page.page_id) 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: if not page_access_token:
self.message_user(request, f"Failed to get access token for {page.page_name}", level="error") self.message_user(
request,
f"[{page.page_name}] Unable to get Page Access Token.",
level=messages.ERROR,
)
continue continue
url = f"https://graph.facebook.com/v22.0/{page.page_id}/subscribed_apps?access_token={page_access_token}" 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 youll only see app-role users here.",
level=messages.SUCCESS,
)
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.")
else:
page.is_subscribed = False
self.message_user(request, f"{page.page_name} is NOT subscribed.", level="warning")
page.save()
except requests.RequestException as e: except requests.RequestException as e:
self.message_user(request, f"Failed to check subscription for {page.page_name}: {e}", level="error") # 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,
)
check_subscription_status.short_description = "Check webhook subscription status" probe_messenger_access.short_description = "Probe Messenger access on selected Pages"
from .models import EventType
@admin.register(EventType) @admin.register(EventType)
class EventTypeAdmin(admin.ModelAdmin): class EventTypeAdmin(admin.ModelAdmin):
list_display = ("code", "label") list_display = ("code", "label")
search_fields = ("code", "label") search_fields = ("code", "label")
from .models import BotInteraction
@admin.register(BotInteraction) @admin.register(BotInteraction)
class BotInteractionAdmin(admin.ModelAdmin): class BotInteractionAdmin(admin.ModelAdmin):
list_display = ("page", "object_id", "parent_object_id", "platform", "created_at") list_display = ("page", "object_id", "parent_object_id", "platform", "created_at")
search_fields = ("object_id", "prompt", "bot_response") search_fields = ("object_id", "prompt", "bot_response")
list_filter = ("platform",) list_filter = ("platform",)