Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions website/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def ready(self):
last_active_signal_from_answer,
last_active_signal_from_reply,
home_cache_invalidator,
webhook_on_create,
webhook_on_delete,
)

post_save.connect(
Expand Down Expand Up @@ -53,4 +55,26 @@ def ready(self):
home_cache_invalidator,
sender=AnswerComment,
dispatch_uid='home_cache_invalidator_answercomment_delete',
)

# Webhook signals for social
post_save.connect(
webhook_on_create,
sender=Question,
dispatch_uid='webhook_question_created',
)
post_save.connect(
webhook_on_create,
sender=Answer,
dispatch_uid='webhook_answer_created',
)
post_delete.connect(
webhook_on_delete,
sender=Question,
dispatch_uid='webhook_question_deleted',
)
post_delete.connect(
webhook_on_delete,
sender=Answer,
dispatch_uid='webhook_answer_deleted',
)
115 changes: 114 additions & 1 deletion website/signals.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import json
import logging
import threading

import requests
from django.conf import settings
from django.utils import timezone
from django.core.cache import cache

logger = logging.getLogger(__name__)


HOME_CACHE_KEYS = [
'home:categories',
Expand Down Expand Up @@ -35,4 +43,109 @@ def last_active_signal_from_reply(sender, instance, created, **kwargs):


def home_cache_invalidator(sender, instance, **kwargs):
clear_home_cache()
clear_home_cache()


# webhook helpers for social

def _send_webhook(payload):
"""Fire-and-forget POST to the Social platform's webhook endpoint.

Runs in a daemon thread so the forum response is never delayed.
Fails silently — the forum must keep working even if the Social app is down.
"""

webhook_url = "https://social.edupyramids.org/api/webhooks/forum"

if not webhook_url:
return

headers = {
'Content-Type': 'application/json',
}

def _post():
try:
resp = requests.post(
webhook_url,
data=json.dumps(payload),
headers=headers,
timeout=10,
)
logger.info(
'Webhook sent: %s → %s (status %s)',
payload.get('event'), webhook_url, resp.status_code,
)
except requests.RequestException as exc:
logger.warning('Webhook delivery failed: %s', exc)

thread = threading.Thread(target=_post, daemon=True)
thread.start()


def _build_payload(event_type, instance, sender):
"""Build a consistent webhook payload for both Question and Answer events."""
from .models import Question, Answer
from django.contrib.auth import get_user_model

User = get_user_model()


user_email = ""
try:
user = User.objects.get(id=instance.uid)
user_email = user.email
except User.DoesNotExist:
pass

payload = {
'event': event_type,
'user_id': instance.uid,
'email': user_email,
'resource_id': instance.id,
'timestamp': timezone.now().isoformat(),
}

if sender == Question:
payload['category'] = instance.category
payload['tutorial'] = instance.tutorial
payload['title'] = instance.title
elif sender == Answer:
payload['question_id'] = instance.question_id
payload['category'] = instance.question.category
payload['tutorial'] = instance.question.tutorial

return payload


def webhook_on_create(sender, instance, created, **kwargs):
"""Signal handler: fires when a Question or Answer is saved for the first time."""
if not created:
return # We only care about new objects, not updates

from .models import Question, Answer

if sender == Question:
event = 'question_created'
elif sender == Answer:
event = 'answer_created'
else:
return

payload = _build_payload(event, instance, sender)
_send_webhook(payload)


def webhook_on_delete(sender, instance, **kwargs):
"""Signal handler: fires when a Question or Answer is deleted."""
from .models import Question, Answer

if sender == Question:
event = 'question_deleted'
elif sender == Answer:
event = 'answer_deleted'
else:
return

payload = _build_payload(event, instance, sender)
_send_webhook(payload)