297 lines
12 KiB
Python
297 lines
12 KiB
Python
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",)
|