This commit is contained in:
parent
2572b15711
commit
d1149ff471
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
0
pxy_messenger/__init__.py
Normal file
0
pxy_messenger/__init__.py
Normal file
7
pxy_messenger/admin.py
Normal file
7
pxy_messenger/admin.py
Normal 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
6
pxy_messenger/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PxyMessengerConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'pxy_messenger'
|
25
pxy_messenger/migrations/0001_initial.py
Normal file
25
pxy_messenger/migrations/0001_initial.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
pxy_messenger/migrations/__init__.py
Normal file
0
pxy_messenger/migrations/__init__.py
Normal file
11
pxy_messenger/models.py
Normal file
11
pxy_messenger/models.py
Normal 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
3
pxy_messenger/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
6
pxy_messenger/urls.py
Normal file
6
pxy_messenger/urls.py
Normal 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
113
pxy_messenger/views.py
Normal 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")
|
@ -1 +0,0 @@
|
|||||||
../../pxy_dashboard/templates/pxy_dashboard/account/*
|
|
Loading…
x
Reference in New Issue
Block a user