From 5e59c4d8b25bf78e3d1b7edb2c388aa4d8815d62 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Wed, 2 Apr 2025 19:24:33 +0200 Subject: [PATCH] Refactor webhooks --- src/anonchat/routes.py | 113 ++++++-------------- src/anonchat/tasks.py | 6 -- src/anonchat/templates/admin_dashboard.html | 24 ++--- src/anonchat/templates/admin_webhook.html | 8 +- src/anonchat/webhooks.py | 93 ++++++++++++++++ 5 files changed, 134 insertions(+), 110 deletions(-) create mode 100644 src/anonchat/webhooks.py diff --git a/src/anonchat/routes.py b/src/anonchat/routes.py index 3b0824d..6eec876 100644 --- a/src/anonchat/routes.py +++ b/src/anonchat/routes.py @@ -1,13 +1,9 @@ from flask import request, jsonify, render_template, redirect, url_for, flash, session from functools import wraps -from . import app, db, limiter, csrf +from . import app, db, limiter, csrf, webhooks +from .webhooks import WebhookError from .models import Inquiry, Message, Settings, Admin import os -import urllib.request -import urllib.error -import hmac -import hashlib -import json from datetime import datetime, timedelta def is_admin(): @@ -22,50 +18,6 @@ def admin_required(f): return f(*args, **kwargs) return decorated_function -def send_webhook(event_type, data): - """Send webhook if enabled with proper signature""" - settings = Settings.query.first() - if not settings or not settings.webhook_enabled or not settings.webhook_url: - return False - - payload = { - "event_type": event_type, - "timestamp": datetime.utcnow().isoformat(), - "data": data - } - - # Convert payload to JSON - payload_str = json.dumps(payload).encode('utf-8') - - # Create request with headers - headers = {'Content-Type': 'application/json'} - if settings.webhook_secret: - signature = hmac.new( - settings.webhook_secret.encode(), - payload_str, - hashlib.sha256 - ).hexdigest() - headers['X-Webhook-Signature'] = signature - - try: - # Create request object - req = urllib.request.Request( - settings.webhook_url, - data=payload_str, - headers=headers, - method='POST' - ) - - # Set timeout - with urllib.request.urlopen(req, timeout=5) as response: - return response.status == 200 - except urllib.error.URLError as e: - app.logger.error(f"Webhook error: {str(e)}") - return False - except Exception as e: - app.logger.error(f"Webhook error: {str(e)}") - return False - @app.route('/', methods=['GET']) def index(): return render_template('create_inquiry.html') @@ -91,10 +43,10 @@ def create_inquiry(): db.session.commit() # Send webhook for new inquiry - send_webhook('new_inquiry', { - 'inquiry_id': new_inquiry.id, - 'message': initial_message - }) + try: + webhooks.inquiry_created(new_inquiry.id, initial_message) + except WebhookError as e: + app.logger.error(f"Error sending webhook for new inquiry: {str(e)}") return redirect(url_for('inquiry', inquiry_id=new_inquiry.id)) @@ -112,9 +64,10 @@ def inquiry(inquiry_id): db.session.commit() # Send webhook for reopened inquiry - send_webhook('inquiry_reopened', { - 'inquiry_id': inquiry_id - }) + try: + webhooks.inquiry_reopened(inquiry_id) + except WebhookError as e: + app.logger.error(f"Error sending webhook for reopened inquiry: {str(e)}") flash('Inquiry reopened successfully.') @@ -129,12 +82,11 @@ def inquiry(inquiry_id): db.session.add(message) db.session.commit() - if not is_admin: - send_webhook('new_message', { - 'inquiry_id': inquiry_id, - 'message': message_content, - 'is_admin': is_admin - }) + if not is_admin: # Admins don't need to be notified of their own messages + try: + webhooks.inquiry_message(inquiry_id, message_content) + except WebhookError as e: + app.logger.error(f"Error sending webhook for message: {str(e)}") return redirect(url_for('inquiry', inquiry_id=inquiry_id)) @@ -169,7 +121,6 @@ def admin_login_post(): flash('Invalid username or password. Try again in 1 minute.', 'error') return redirect(url_for('admin_login')) - @app.route('/admin/logout') def admin_logout(): @@ -217,13 +168,11 @@ def admin_webhook(): # Test webhook if enabled and URL is provided if settings.webhook_enabled and settings.webhook_url: - test_success = send_webhook('webhook_test', { - 'message': 'This is a test webhook' - }) - if test_success: + try: + webhooks.inquiry_created('1234abcd1234abcd', 'This is a test message') flash('Webhook settings saved and test sent successfully') - else: - flash('Webhook settings saved but test failed. Check the URL and connection') + except WebhookError as e: + flash(f'Webhook test failed: {str(e)}') else: flash('Webhook settings saved') @@ -265,6 +214,7 @@ def admin_settings(): return render_template('admin_settings.html') @app.route('/inquiry//delete', methods=['POST']) +@limiter.limit("10 per hour", deduct_when=lambda response: response.status_code != 200) def delete_inquiry(inquiry_id): inquiry = Inquiry.query.get_or_404(inquiry_id) @@ -272,11 +222,6 @@ def delete_inquiry(inquiry_id): db.session.delete(inquiry) db.session.commit() - # Send webhook for deleted inquiry - send_webhook('inquiry_deleted', { - 'inquiry_id': inquiry_id - }) - # Check if user is admin and redirect accordingly if 'admin_authenticated' in session and session['admin_authenticated']: flash('Inquiry deleted successfully') @@ -334,7 +279,7 @@ def get_inquiry_messages(inquiry_id): }) @app.route('/inquiry//close', methods=['POST']) -@admin_required +@limiter.limit("10 per minute") def close_inquiry(inquiry_id): inquiry = Inquiry.query.get_or_404(inquiry_id) @@ -344,9 +289,10 @@ def close_inquiry(inquiry_id): db.session.commit() # Send webhook for closed inquiry - send_webhook('inquiry_closed', { - 'inquiry_id': inquiry_id - }) + try: + webhooks.inquiry_closed(inquiry_id) + except WebhookError as e: + app.logger.error(f"Error sending webhook for closed inquiry: {str(e)}") # Redirect back to the inquiry or admin dashboard based on referrer referrer = request.referrer @@ -356,7 +302,7 @@ def close_inquiry(inquiry_id): return redirect(url_for('inquiry', inquiry_id=inquiry_id)) @app.route('/inquiry//reopen', methods=['POST']) -@admin_required +@limiter.limit("10 per minute") def reopen_inquiry(inquiry_id): inquiry = Inquiry.query.get_or_404(inquiry_id) @@ -366,9 +312,10 @@ def reopen_inquiry(inquiry_id): db.session.commit() # Send webhook for reopened inquiry - send_webhook('inquiry_reopened', { - 'inquiry_id': inquiry_id - }) + try: + webhooks.inquiry_reopened(inquiry_id) + except WebhookError as e: + app.logger.error(f"Error sending webhook for reopened inquiry: {str(e)}") flash('Inquiry reopened successfully.') diff --git a/src/anonchat/tasks.py b/src/anonchat/tasks.py index 10ea7c4..8325b3e 100644 --- a/src/anonchat/tasks.py +++ b/src/anonchat/tasks.py @@ -17,12 +17,6 @@ def check_and_delete_expired_inquiries(): # Delete the inquiry db.session.delete(inquiry) deleted_count += 1 - - # Send webhook for automatically deleted inquiry - from .routes import send_webhook - send_webhook('inquiry_auto_deleted', { - 'inquiry_id': inquiry.id - }) if deleted_count > 0: db.session.commit() diff --git a/src/anonchat/templates/admin_dashboard.html b/src/anonchat/templates/admin_dashboard.html index a8378e4..a99bc6c 100644 --- a/src/anonchat/templates/admin_dashboard.html +++ b/src/anonchat/templates/admin_dashboard.html @@ -25,18 +25,18 @@ {% for item in inquiries_with_data %} - + {{ item.inquiry.id }} {% if item.inquiry.is_closed %} - CLOSED + CLOSED {% if item.inquiry.closing_timestamp %} - + Closed on {{ item.inquiry.closing_timestamp.strftime('%Y-%m-%d %H:%M') }} {% endif %} {% else %} - OPEN + OPEN {% endif %} {{ item.message_count }} @@ -55,23 +55,11 @@ {% endif %} - View/Respond - - {% if item.inquiry.is_closed %} -
- - -
- {% else %} -
- - -
- {% endif %} + View
- +
diff --git a/src/anonchat/templates/admin_webhook.html b/src/anonchat/templates/admin_webhook.html index fe1e754..88d8e57 100644 --- a/src/anonchat/templates/admin_webhook.html +++ b/src/anonchat/templates/admin_webhook.html @@ -13,8 +13,10 @@

Configure webhook settings to receive notifications for new inquiries and responses.

Events will be sent as POST requests with JSON payloads. Event types include:

    -
  • new_inquiry - Triggered when a new inquiry is created
  • -
  • new_message - Triggered when a user adds a message to an existing inquiry
  • +
  • inquiry_created - Triggered when a new inquiry is created. Contains the inquiry ID and the initial message.
  • +
  • inquiry_reopened - Triggered when an inquiry is reopened. Contains the inquiry ID.
  • +
  • inquiry_closed - Triggered when an inquiry is closed. Contains the inquiry ID.
  • +
  • inquiry_message - Triggered when a user adds a message to an existing inquiry. Contains the inquiry ID and the message content.
@@ -46,7 +48,7 @@

Webhook Payload Example

 {
-  "event_type": "new_inquiry",
+  "event_type": "inquiry_created",
   "timestamp": "2023-04-01T12:34:56.789Z",
   "data": {
     "inquiry_id": "abcdef1234567890",
diff --git a/src/anonchat/webhooks.py b/src/anonchat/webhooks.py
new file mode 100644
index 0000000..07ce38b
--- /dev/null
+++ b/src/anonchat/webhooks.py
@@ -0,0 +1,93 @@
+import urllib.request
+import urllib.error
+import hmac
+import hashlib
+import json
+from datetime import datetime
+from flask import current_app
+from .models import Settings
+
+class WebhookError(Exception):
+    """Exception raised for errors in the webhook process.
+    
+    Attributes:
+        message -- explanation of the error
+        status_code -- HTTP status code (optional)
+        original_exception -- the original exception that caused this error (optional)
+    """
+    
+    def __init__(self, message, status_code=None, original_exception=None):
+        self.message = message
+        self.status_code = status_code
+        self.original_exception = original_exception
+        super().__init__(self.message)
+
+
+def _send_webhook(event_type, data):
+    """Send webhook if enabled with proper signature"""
+    settings = Settings.query.first()
+    if not settings or not settings.webhook_enabled or not settings.webhook_url:
+        return False
+    
+    payload = {
+        "event_type": event_type,
+        "timestamp": datetime.utcnow().isoformat(),
+        "data": data
+    }
+    
+    # Convert payload to JSON
+    payload_str = json.dumps(payload).encode('utf-8')
+    
+    # Create request with headers
+    headers = {'Content-Type': 'application/json'}
+    if settings.webhook_secret:
+        signature = hmac.new(
+            settings.webhook_secret.encode(),
+            payload_str,
+            hashlib.sha256
+        ).hexdigest()
+        headers['X-Webhook-Signature'] = signature
+    
+    try:
+        # Create request object
+        req = urllib.request.Request(
+            settings.webhook_url,
+            data=payload_str,
+            headers=headers,
+            method='POST'
+        )
+        
+        # Set timeout
+        with urllib.request.urlopen(req, timeout=5) as response:
+            if response.status != 200:
+                raise WebhookError(f"Webhook error: {response.status}", status_code=response.status)
+    except Exception as e:
+        status_code = None
+        if hasattr(e, 'status_code'):
+            status_code = e.status_code
+        raise WebhookError(f"Webhook error: {str(e)}", status_code=status_code, original_exception=e)
+    
+def inquiry_created(inquiry_id, message):
+    _send_webhook('inquiry_created', {
+        'inquiry_id': inquiry_id,
+        'message': message
+    })
+    
+def inquiry_reopened(inquiry_id):
+    _send_webhook('inquiry_reopened', {
+        'inquiry_id': inquiry_id
+    })
+    
+    
+def inquiry_closed(inquiry_id):
+    _send_webhook('inquiry_closed', {
+        'inquiry_id': inquiry_id
+    })
+    
+def inquiry_message(inquiry_id, message):
+    _send_webhook('inquiry_message', {
+        'inquiry_id': inquiry_id,
+        'message': message
+    })
+    
+    
\ No newline at end of file