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)
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: try:
response = requests.post(url, data=data) # 1) token
response.raise_for_status() page_token = getattr(fb_service, "get_page_access_token", None)
page.is_subscribed = True if callable(page_token):
page.save() page_access_token = page_token(page.page_id)
self.message_user(request, f"Successfully subscribed {page.page_name} to webhooks.") 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: 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): 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) 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)
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: try:
response = requests.get(url) # token
response.raise_for_status() page_token = getattr(fb_service, "get_page_access_token", None)
data = response.json() if callable(page_token):
page_access_token = page_token(page.page_id)
if "data" in data and len(data["data"]) > 0:
page.is_subscribed = True
self.message_user(request, f"{page.page_name} is subscribed.")
else: else:
page.is_subscribed = False page_access_token = fb_service._get_page_access_token(page.page_id) # noqa
self.message_user(request, f"{page.page_name} is NOT subscribed.", level="warning")
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: 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 youll 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) @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",)