Refactor webhooks
This commit is contained in:
parent
c86b0670a3
commit
5e59c4d8b2
5 changed files with 134 additions and 110 deletions
|
@ -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.')
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
93
src/anonchat/webhooks.py
Normal 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
|
||||
})
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue