Messenger app
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Ekaropolus 2025-09-06 02:33:53 -06:00
parent 2572b15711
commit d1149ff471
14 changed files with 186 additions and 1 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ pxy_city_digital_twins/__backup__/
Dockerfile.dev Dockerfile.dev
docker-compose.override.yml docker-compose.override.yml
docker-compose.override.yml docker-compose.override.yml
pxy_meta_pages.zip

View File

@ -51,6 +51,7 @@ INSTALLED_APPS = [
"pxy_dashboard.components", "pxy_dashboard.components",
"pxy_dashboard.layouts", "pxy_dashboard.layouts",
"pxy_building_digital_twins", "pxy_building_digital_twins",
"pxy_messenger",
# Third-party apps # Third-party apps
"crispy_forms", "crispy_forms",
@ -176,3 +177,8 @@ EMAIL_USE_SSL = True
EMAIL_HOST_USER = "noreply@polisplexity.tech" # Cambia esto por tu correo real EMAIL_HOST_USER = "noreply@polisplexity.tech" # Cambia esto por tu correo real
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") # Mejor usar .env EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") # Mejor usar .env
DEFAULT_FROM_EMAIL = "Polisplexity <noreply@polisplexity.tech>" DEFAULT_FROM_EMAIL = "Polisplexity <noreply@polisplexity.tech>"
MESSENGER_VERIFY_TOKEN = os.getenv("MESSENGER_VERIFY_TOKEN", "dev-change-me")
FACEBOOK_APP_SECRET = os.getenv("FACEBOOK_APP_SECRET", "") # set this in .env for prod

View File

@ -37,6 +37,7 @@ urlpatterns = [
include(("pxy_building_digital_twins.urls", "pxy_building_digital_twins"), include(("pxy_building_digital_twins.urls", "pxy_building_digital_twins"),
namespace="pxy_building_digital_twins"), namespace="pxy_building_digital_twins"),
), ),
path("messenger/", include("pxy_messenger.urls")),
] ]

View File

@ -44,6 +44,13 @@ EXEMPT_URLS += [
re.compile(r"^pxy_meta_pages/webhook/?$"), re.compile(r"^pxy_meta_pages/webhook/?$"),
] ]
# Webhook de Facebook Messenger
EXEMPT_URLS += [
"messenger/webhook/", # exact string match (no leading slash)
re.compile(r"^messenger/webhook/?$"), # regex with optional trailing slash
]
class LoginRequiredMiddleware(MiddlewareMixin): class LoginRequiredMiddleware(MiddlewareMixin):
def process_request(self, request): def process_request(self, request):
if not request.user.is_authenticated: if not request.user.is_authenticated:

View File

7
pxy_messenger/admin.py Normal file
View File

@ -0,0 +1,7 @@
from django.contrib import admin
from .models import MessengerEvent
@admin.register(MessengerEvent)
class MessengerEventAdmin(admin.ModelAdmin):
list_display = ("mid", "sender_id", "page_id", "created_at")
search_fields = ("mid", "sender_id", "page_id")

6
pxy_messenger/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PxyMessengerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'pxy_messenger'

View File

@ -0,0 +1,25 @@
# Generated by Django 5.0.3 on 2025-09-06 07:50
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='MessengerEvent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mid', models.CharField(db_index=True, max_length=255, unique=True)),
('sender_id', models.CharField(max_length=64)),
('page_id', models.CharField(blank=True, default='', max_length=64)),
('payload', models.JSONField(blank=True, default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
]

View File

11
pxy_messenger/models.py Normal file
View File

@ -0,0 +1,11 @@
from django.db import models
class MessengerEvent(models.Model):
mid = models.CharField(max_length=255, unique=True, db_index=True) # message id
sender_id = models.CharField(max_length=64)
page_id = models.CharField(max_length=64, blank=True, default="")
payload = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.mid

3
pxy_messenger/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

6
pxy_messenger/urls.py Normal file
View File

@ -0,0 +1,6 @@
from django.urls import path
from . import views
urlpatterns = [
path("webhook/", views.webhook, name="messenger_webhook"),
]

113
pxy_messenger/views.py Normal file
View File

@ -0,0 +1,113 @@
# pxy_messenger/views.py
import hashlib, hmac, json, logging, os
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
# Optional: if you created a model to dedupe events (you have one in admin!)
from .models import MessengerEvent # keep if present
import requests
log = logging.getLogger(__name__)
GRAPH_API = os.getenv("META_GRAPH_API", "https://graph.facebook.com/v18.0")
def _verify_get(request):
mode = request.GET.get("hub.mode")
token = request.GET.get("hub.verify_token")
chal = request.GET.get("hub.challenge", "")
if mode == "subscribe" and token == getattr(settings, "MESSENGER_VERIFY_TOKEN", ""):
return HttpResponse(chal)
return HttpResponseForbidden("Bad verify token")
def _verify_signature(request):
app_secret = getattr(settings, "FACEBOOK_APP_SECRET", "")
if not app_secret:
return True # dev mode (no signature check)
provided = request.headers.get("X-Hub-Signature-256", "")
body = request.body
# keep a copy for troubleshooting
try:
with open("/tmp/m_sig_body.bin", "wb") as f:
f.write(body)
except Exception:
pass
calc_hex = hmac.new(app_secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
expected = "sha256=" + calc_hex
log.info("[messenger] sig check: provided=%r expected=%r body_len=%d", provided, expected, len(body))
return hmac.compare_digest(provided, expected)
def _send_text(psid: str, text: str) -> bool:
token = getattr(settings, "PAGE_ACCESS_TOKEN", "")
if not token:
log.warning("[messenger] no PAGE_ACCESS_TOKEN; would send to %s: %s", psid, text)
return False
url = f"{GRAPH_API}/me/messages"
params = {"access_token": token}
payload = {
"messaging_type": "RESPONSE",
"recipient": {"id": psid},
"message": {"text": text},
}
try:
r = requests.post(url, params=params, json=payload, timeout=10)
if r.status_code != 200:
log.error("[messenger] Send API error %s: %s", r.status_code, r.text)
return False
log.info("[messenger] sent to %s ok: %s", psid, r.text)
return True
except Exception as e:
log.exception("[messenger] Send API exception: %s", e)
return False
@csrf_exempt
def webhook(request):
if request.method == "GET":
return _verify_get(request)
if request.method != "POST":
return HttpResponse(status=405)
if not _verify_signature(request):
return HttpResponseForbidden("Invalid signature")
try:
payload = json.loads(request.body.decode("utf-8") or "{}")
except Exception:
payload = {}
log.info("[messenger] payload: %s", json.dumps(payload)[:2000])
# Iterate entries and messaging events
for entry in payload.get("entry", []):
for m in entry.get("messaging", []):
sender = (m.get("sender") or {}).get("id")
recipient = (m.get("recipient") or {}).get("id")
# dedupe by message mid when present
mid = (m.get("message") or {}).get("mid") \
or (m.get("delivery") or {}).get("mids", [None])[0]
if mid:
# store if not seen (your admin shows MessengerEvent already)
obj, created = MessengerEvent.objects.get_or_create(
mid=mid,
defaults={
"sender_id": sender or "",
"page_id": recipient or "",
},
)
if not created:
log.info("[messenger] duplicate mid %s — skipping", mid)
continue
# Basic text echo
if "message" in m and "text" in m["message"] and sender:
txt = m["message"]["text"].strip()
reply = f"You said: {txt}"
_send_text(sender, reply)
# must 200 quickly
return HttpResponse("OK")

View File

@ -1 +0,0 @@
../../pxy_dashboard/templates/pxy_dashboard/account/*