Refactor webhooks

This commit is contained in:
Minecon724 2025-04-02 19:24:33 +02:00
commit 5e59c4d8b2
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
5 changed files with 134 additions and 110 deletions

View file

@ -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/<inquiry_id>/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/<inquiry_id>/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/<inquiry_id>/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.')

View file

@ -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()

View file

@ -25,18 +25,18 @@
</thead>
<tbody>
{% for item in inquiries_with_data %}
<tr {% if item.inquiry.is_closed %}style="background-color: #f8f9fa; opacity: 0.8;"{% endif %}>
<tr {% if item.inquiry.is_closed %}style="background-color: #2a2a2a; opacity: 0.9;"{% endif %}>
<td>{{ item.inquiry.id }}</td>
<td>
{% if item.inquiry.is_closed %}
<span style="color: #6c757d; font-size: 0.8rem; padding: 0.2rem 0.4rem; background-color: #e9ecef; border-radius: 0.25rem;">CLOSED</span>
<span style="color: #d3d3d3; font-size: 0.8rem; padding: 0.2rem 0.4rem; background-color: #444444; border-radius: 0.25rem;">CLOSED</span>
{% if item.inquiry.closing_timestamp %}
<span style="display: block; font-size: 0.7rem; margin-top: 0.2rem;">
<span style="display: block; font-size: 0.7rem; margin-top: 0.2rem; color: #a9a9a9;">
Closed on {{ item.inquiry.closing_timestamp.strftime('%Y-%m-%d %H:%M') }}
</span>
{% endif %}
{% else %}
<span style="color: #28a745; font-size: 0.8rem; padding: 0.2rem 0.4rem; background-color: #e9ecef; border-radius: 0.25rem;">OPEN</span>
<span style="color: #4caf50; font-size: 0.8rem; padding: 0.2rem 0.4rem; background-color: #333333; border-radius: 0.25rem;">OPEN</span>
{% endif %}
</td>
<td>{{ item.message_count }}</td>
@ -55,23 +55,11 @@
{% endif %}
</td>
<td>
<a href="{{ url_for('inquiry', inquiry_id=item.inquiry.id) }}">View/Respond</a>
{% if item.inquiry.is_closed %}
<form method="POST" action="{{ url_for('reopen_inquiry', inquiry_id=item.inquiry.id) }}" style="display: inline; margin-left: 0.5rem;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" style="background-color: #28a745; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Reopen</button>
</form>
{% else %}
<form method="POST" action="{{ url_for('close_inquiry', inquiry_id=item.inquiry.id) }}" style="display: inline; margin-left: 0.5rem;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" style="background-color: #6c757d; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Close</button>
</form>
{% endif %}
<a href="{{ url_for('inquiry', inquiry_id=item.inquiry.id) }}" style="float: left; color: #61afef;">View</a>
<form method="POST" action="{{ url_for('delete_inquiry', inquiry_id=item.inquiry.id) }}" style="display: inline; margin-left: 0.5rem;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" onclick="return confirm('Are you sure you want to delete this inquiry? This action cannot be undone.')" style="background-color: #dc3545; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Delete</button>
<button type="submit" onclick="return confirm('Are you sure you want to delete this inquiry? This action cannot be undone.')" style="background-color: #e06c75; color: #282c34; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Delete</button>
</form>
</td>
</tr>

View file

@ -13,8 +13,10 @@
<p>Configure webhook settings to receive notifications for new inquiries and responses.</p>
<p>Events will be sent as POST requests with JSON payloads. Event types include:</p>
<ul>
<li><strong>new_inquiry</strong> - Triggered when a new inquiry is created</li>
<li><strong>new_message</strong> - Triggered when a user adds a message to an existing inquiry</li>
<li><strong>inquiry_created</strong> - Triggered when a new inquiry is created. Contains the inquiry ID and the initial message.</li>
<li><strong>inquiry_reopened</strong> - Triggered when an inquiry is reopened. Contains the inquiry ID.</li>
<li><strong>inquiry_closed</strong> - Triggered when an inquiry is closed. Contains the inquiry ID.</li>
<li><strong>inquiry_message</strong> - Triggered when a user adds a message to an existing inquiry. Contains the inquiry ID and the message content.</li>
</ul>
</div>
@ -46,7 +48,7 @@
<h3>Webhook Payload Example</h3>
<pre style="background-color: #1e1e1e; padding: 1rem; overflow-x: auto; border-radius: 0.25rem; color: #d4d4d4;">
{
"event_type": "new_inquiry",
"event_type": "inquiry_created",
"timestamp": "2023-04-01T12:34:56.789Z",
"data": {
"inquiry_id": "abcdef1234567890",

93
src/anonchat/webhooks.py Normal file
View file

@ -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
})