Admin for Suscribing to Messenger Servers
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
c954488c28
commit
212ce3b129
@ -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 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)
|
@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",)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user