Make webhooks async

This commit is contained in:
Minecon724 2025-04-03 18:04:27 +02:00
commit 94e9d5ea75
Signed by untrusted user who does not match committer: m724
GPG key ID: A02E6E67AB961189
2 changed files with 74 additions and 54 deletions

View file

@ -96,18 +96,18 @@ def admin_settings_webhook():
settings.webhook_enabled = request.form.get('webhook_enabled') == 'on' settings.webhook_enabled = request.form.get('webhook_enabled') == 'on'
settings.webhook_url = request.form.get('webhook_url', '') settings.webhook_url = request.form.get('webhook_url', '')
settings.webhook_secret = request.form.get('webhook_secret', '') settings.webhook_secret = request.form.get('webhook_secret', '')
db.session.commit()
# Test webhook if enabled and URL is provided # Test webhook if enabled and URL is provided
if settings.webhook_enabled and settings.webhook_url: if settings.webhook_enabled and settings.webhook_url:
try: try:
inquiry_created('1234abcd1234abcd', 'This is a test message') inquiry_created('1234abcd1234abcd', 'This is a test message', is_async=False)
flash('Webhook settings saved and test sent successfully') flash('Webhook settings saved and test sent successfully')
except WebhookError as e: except WebhookError as e:
flash(f'Webhook test failed: {str(e)}') flash(f'Webhook test failed: {str(e)}')
else: else:
flash('Webhook settings saved') flash('Webhook settings saved')
db.session.commit()
return redirect(url_for('admin_settings')) return redirect(url_for('admin_settings'))
@ -125,7 +125,7 @@ def admin_settings():
@admin_required @admin_required
def admin_settings_password(): def admin_settings_password():
admin = Admin.query.filter_by(username=session['admin_username']).first() admin = Admin.query.filter_by(username=session['admin_username']).first()
current_password = request.form.get('current_password') current_password = request.form.get('current_password')
new_password = request.form.get('new_password') new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password') confirm_password = request.form.get('confirm_password')

View file

@ -3,6 +3,8 @@ import urllib.error
import hmac import hmac
import hashlib import hashlib
import json import json
import threading
import logging
from datetime import datetime from datetime import datetime
from flask import current_app from flask import current_app
from .models import Settings from .models import Settings
@ -23,71 +25,89 @@ class WebhookError(Exception):
super().__init__(self.message) super().__init__(self.message)
def _send_webhook(event_type, data): def _send_webhook(event_type, data, is_async=False):
"""Send webhook if enabled with proper signature""" app = current_app._get_current_object()
settings = Settings.query.first()
if not settings or not settings.webhook_enabled or not settings.webhook_url: if is_async:
return False _send_webhook_async(event_type, data, app)
else:
payload = { _send_webhook_worker(event_type, data, app)
"event_type": event_type,
"timestamp": datetime.utcnow().isoformat(), def _send_webhook_async(event_type, data, app):
"data": data """Queue webhook to be sent in a background thread"""
} thread = threading.Thread(target=_send_webhook_worker, args=(event_type, data, app))
thread.daemon = True # Thread will exit when main thread exits
# Convert payload to JSON thread.start()
payload_str = json.dumps(payload).encode('utf-8')
# Create request with headers def _send_webhook_worker(event_type, data, app):
headers = {'Content-Type': 'application/json'} """Worker function that sends the webhook in a background thread"""
if settings.webhook_secret:
signature = hmac.new(
settings.webhook_secret.encode(),
payload_str,
hashlib.sha256
).hexdigest()
headers['X-Webhook-Signature'] = signature
try: try:
# Create request object with app.app_context():
req = urllib.request.Request( settings = Settings.query.first()
settings.webhook_url, if not settings or not settings.webhook_enabled or not settings.webhook_url:
data=payload_str, return False
headers=headers,
method='POST' payload = {
) "event_type": event_type,
"timestamp": datetime.utcnow().isoformat(),
# Set timeout "data": data
with urllib.request.urlopen(req, timeout=5) as response: }
if response.status != 200:
raise WebhookError(f"Webhook error: {response.status}", status_code=response.status) # 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
# 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("Status code not 200", status_code=response.status)
except Exception as e: except Exception as e:
status_code = None app.logger.error(f"Webhook error: {e}")
if hasattr(e, 'status_code'):
status_code = e.status_code if isinstance(e, WebhookError):
raise WebhookError(f"Webhook error: {str(e)}", status_code=status_code, original_exception=e) raise e
def inquiry_created(inquiry_id, message): raise WebhookError(f"Exception: {e}", original_exception=e)
def inquiry_created(inquiry_id, message, is_async=True):
_send_webhook('inquiry_created', { _send_webhook('inquiry_created', {
'inquiry_id': inquiry_id, 'inquiry_id': inquiry_id,
'message': message 'message': message
}) }, is_async)
def inquiry_reopened(inquiry_id): def inquiry_reopened(inquiry_id, is_async=True):
_send_webhook('inquiry_reopened', { _send_webhook('inquiry_reopened', {
'inquiry_id': inquiry_id 'inquiry_id': inquiry_id
}) }, is_async)
def inquiry_closed(inquiry_id): def inquiry_closed(inquiry_id, is_async=True):
_send_webhook('inquiry_closed', { _send_webhook('inquiry_closed', {
'inquiry_id': inquiry_id 'inquiry_id': inquiry_id
}) }, is_async)
def inquiry_message(inquiry_id, message): def inquiry_message(inquiry_id, message, is_async=True):
_send_webhook('inquiry_message', { _send_webhook('inquiry_message', {
'inquiry_id': inquiry_id, 'inquiry_id': inquiry_id,
'message': message 'message': message
}) }, is_async)