Ekaropolus 0eb2b393f2
All checks were successful
continuous-integration/drone/push Build is passing
SAMI Functionality add
2025-09-16 16:18:45 -06:00

320 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# pxy_meta_pages/admin.py
from __future__ import annotations
import json
from typing import Optional, Dict, Any
import requests
from django.conf import settings
from django.contrib import admin, messages
from .models import FacebookPageAssistant, EventType, BotInteraction
from .services import FacebookService
# -----------------------------------------------------------------------------
# Config
# -----------------------------------------------------------------------------
FACEBOOK_API_VERSION: str = getattr(settings, "FACEBOOK_API_VERSION", "v22.0")
APP_ID: Optional[str] = getattr(settings, "FACEBOOK_APP_ID", None)
# Fields we require on every Page subscription (Page Feed + Messenger)
REQUIRED_FIELDS = [
# Page feed (comments/shares/mentions)
"feed",
"mention",
# Messenger
"messages",
"messaging_postbacks",
"message_reads",
"message_deliveries",
"message_reactions",
"message_echoes",
]
# -----------------------------------------------------------------------------
# Small Graph helpers with consistent error handling
# -----------------------------------------------------------------------------
def _graph_get(url: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""GET wrapper that raises RequestException on Graph errors."""
resp = requests.get(url, params=params, timeout=15)
data = resp.json() if resp.content else {}
if isinstance(data, dict) and "error" in data:
# Normalize to RequestException so callers can unify handling
raise requests.RequestException(json.dumps(data["error"]))
resp.raise_for_status()
return data or {}
def _graph_post(url: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""POST wrapper that raises RequestException on Graph errors."""
resp = requests.post(url, data=data, timeout=15)
payload = resp.json() if resp.content else {}
if isinstance(payload, dict) and "error" in payload:
raise requests.RequestException(json.dumps(payload["error"]))
resp.raise_for_status()
return payload or {}
def _decode_graph_error(e: requests.RequestException) -> str:
"""
Attempt to pretty-print a Graph API error dict, else return the raw message.
"""
msg = str(e)
try:
err = json.loads(msg)
# Typical Graph error shape
code = err.get("code")
sub = err.get("error_subcode")
text = err.get("message", "Graph error")
return f"Graph error (code={code}, subcode={sub}): {text}"
except Exception:
return msg
def _get_page_token(fb_service: FacebookService, page_id: str) -> Optional[str]:
"""
Works with either a public get_page_access_token or the private _get_page_access_token.
"""
getter = getattr(fb_service, "get_page_access_token", None)
if callable(getter):
return getter(page_id)
private_getter = getattr(fb_service, "_get_page_access_token", None)
if callable(private_getter):
return private_getter(page_id)
return None
# -----------------------------------------------------------------------------
# Admins
# -----------------------------------------------------------------------------
@admin.register(FacebookPageAssistant)
class FacebookPageAssistantAdmin(admin.ModelAdmin):
"""
Admin for wiring a Facebook Page to an OpenAI assistant and managing webhook subscriptions.
"""
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",
]
# ----- Counters ----------------------------------------------------------
def comment_count(self, obj: FacebookPageAssistant) -> int:
return obj.events.filter(event_type__code="comment").count()
comment_count.short_description = "Comments"
def share_count(self, obj: FacebookPageAssistant) -> int:
return obj.events.filter(event_type__code="share").count()
share_count.short_description = "Shares"
# ----- Action 1: Ensure required fields (feed + Messenger) --------------
def ensure_feed_and_messenger_subscription(self, request, queryset):
"""
For each selected Page:
1) Fetch the Page Access Token via FacebookService.
2) Read current subscribed_fields.
3) Add any missing REQUIRED_FIELDS in a single POST.
"""
fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN)
for page in queryset:
try:
page_access_token = _get_page_token(fb_service, page.page_id)
if not page_access_token:
self.message_user(
request,
f"[{page.page_name}] Unable to obtain Page Access Token.",
level=messages.ERROR,
)
continue
list_url = f"https://graph.facebook.com/{FACEBOOK_API_VERSION}/{page.page_id}/subscribed_apps"
current_data = _graph_get(list_url, {"access_token": page_access_token})
entries = current_data.get("data", [])
# If APP_ID is known, narrow to our app row; otherwise use first row if present
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_fields = set(app_entry.get("subscribed_fields", [])) if app_entry else set()
required = set(REQUIRED_FIELDS)
if required - current_fields:
new_fields_csv = ",".join(sorted(current_fields | required))
_graph_post(list_url, {
"subscribed_fields": new_fields_csv,
"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. Now includes: {new_fields_csv}",
level=messages.SUCCESS,
)
else:
page.is_subscribed = True
page.save(update_fields=["is_subscribed"])
self.message_user(
request,
f"[{page.page_name}] Already has required fields.",
level=messages.INFO,
)
except requests.RequestException as e:
self.message_user(
request,
f"[{page.page_name}] {_decode_graph_error(e)}",
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)"
# ----- Action 2: Check subscription status ------------------------------
def check_subscription_status(self, request, queryset):
"""
Shows the exact subscribed_fields currently active for each Page.
"""
fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN)
for page in queryset:
try:
page_access_token = _get_page_token(fb_service, page.page_id)
if not page_access_token:
self.message_user(
request,
f"[{page.page_name}] Unable to obtain Page Access Token.",
level=messages.ERROR,
)
continue
url = f"https://graph.facebook.com/{FACEBOOK_API_VERSION}/{page.page_id}/subscribed_apps"
data = _graph_get(url, {"access_token": page_access_token})
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 []
page.is_subscribed = bool(fields)
page.save(update_fields=["is_subscribed"])
has_all = set(REQUIRED_FIELDS).issubset(set(fields))
level = messages.SUCCESS if has_all else messages.WARNING
self.message_user(
request,
f"[{page.page_name}] Subscribed fields: {', '.join(fields) if fields else '(none)'}",
level=level,
)
except requests.RequestException as e:
self.message_user(
request,
f"[{page.page_name}] {_decode_graph_error(e)}",
level=messages.ERROR,
)
except Exception as e:
self.message_user(
request,
f"[{page.page_name}] Unexpected error: {e}",
level=messages.ERROR,
)
check_subscription_status.short_description = "Check webhook subscription fields"
# ----- Action 3: Probe Messenger access ---------------------------------
def probe_messenger_access(self, request, queryset):
"""
Light probe for Messenger perms using /{PAGE_ID}/conversations.
(In Dev Mode, youll only see app-role users here.)
"""
fb_service = FacebookService(user_access_token=settings.PAGE_ACCESS_TOKEN)
for page in queryset:
try:
page_access_token = _get_page_token(fb_service, page.page_id)
if not page_access_token:
self.message_user(
request,
f"[{page.page_name}] Unable to obtain Page Access Token.",
level=messages.ERROR,
)
continue
url = f"https://graph.facebook.com/{FACEBOOK_API_VERSION}/{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."
),
level=messages.SUCCESS,
)
except requests.RequestException as e:
msg = _decode_graph_error(e)
# Add quick hints for common codes
hint = ""
if '"code": 190' in msg or "Invalid OAuth 2.0" in msg:
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,
)
except Exception as e:
self.message_user(
request,
f"[{page.page_name}] Unexpected error: {e}",
level=messages.ERROR,
)
probe_messenger_access.short_description = "Probe Messenger access"
@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",)