301 lines
16 KiB
Python
301 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Een Flask-applicatie die elke Discord-webhook op een dynamisch pad kan ontvangen,
|
|
weergeeft als HTML, en doorstuurt naar een dynamisch gegenereerde URL
|
|
op basis van een .env configuratie.
|
|
|
|
Vereisten:
|
|
pip install Flask python-dotenv requests Markdown waitress jinja2
|
|
|
|
Hoe te gebruiken:
|
|
1. Maak een `.env` bestand aan in dezelfde map.
|
|
2. Definieer 'OUTGOING_WEBHOOK_BASE_URL' en optioneel 'OUTGOING_WEBHOOK_PARAMS'.
|
|
3. Sla dit bestand op als `app.py`.
|
|
4. Voer het uit vanuit je terminal: `python app.py`
|
|
5. Stuur een POST-verzoek naar een willekeurig endpoint,
|
|
bijv. http://127.0.0.1:5001/webhook/any-string-you-want
|
|
"""
|
|
import os
|
|
import datetime
|
|
import markdown
|
|
import requests
|
|
import json
|
|
from flask import Flask, request, render_template, jsonify, abort, url_for
|
|
from dotenv import load_dotenv
|
|
from jinja2 import DictLoader
|
|
|
|
# Laad de omgevingsvariabelen uit het .env-bestand
|
|
load_dotenv()
|
|
|
|
# Initialiseer de Flask-applicatie
|
|
app = Flask(__name__)
|
|
|
|
# Lees de configuratie uit de omgevingsvariabelen.
|
|
OUTGOING_WEBHOOK_BASE_URL = os.getenv('OUTGOING_WEBHOOK_BASE_URL')
|
|
OUTGOING_WEBHOOK_PARAMS = os.getenv('OUTGOING_WEBHOOK_PARAMS', '') # Standaard lege string
|
|
|
|
if not OUTGOING_WEBHOOK_BASE_URL:
|
|
print("WAARSCHUWING: OUTGOING_WEBHOOK_BASE_URL niet gevonden in .env. Doorsturen van webhooks is uitgeschakeld.")
|
|
|
|
# In-memory opslag voor de laatst ontvangen webhook-data per ID.
|
|
# Deze wordt dynamisch gevuld als er nieuwe webhooks binnenkomen.
|
|
webhook_data_store = {}
|
|
|
|
# --- HTML TEMPLATES (ongewijzigd) ---
|
|
BASE_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html lang="nl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Discord Webhook Weergave</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root { --discord-dark-bg: #36393f; --discord-embed-bg: #2f3136; --discord-text: #dcddde; --discord-text-muted: #72767d; --discord-text-link: #0096cf; --discord-border: rgba(255, 255, 255, 0.1); }
|
|
body { background-color: var(--discord-dark-bg); color: var(--discord-text); font-family: 'Inter', sans-serif; margin: 0; padding: 20px; font-size: 16px; line-height: 1.4; }
|
|
.container { max-width: 700px; margin: auto; }
|
|
.discord-message { display: flex; padding: 10px 0; }
|
|
.avatar { width: 40px; height: 40px; border-radius: 50%; margin-right: 16px; flex-shrink: 0; }
|
|
.message-content { display: flex; flex-direction: column; width: 100%; }
|
|
.author-info { display: flex; align-items: center; margin-bottom: 4px; }
|
|
.author-name { font-weight: 500; color: #fff; margin-right: 8px; }
|
|
.timestamp { font-size: 12px; color: var(--discord-text-muted); }
|
|
.content-text { white-space: pre-wrap; word-wrap: break-word; }
|
|
.content-text a { color: var(--discord-text-link); text-decoration: none; }
|
|
.content-text a:hover { text-decoration: underline; }
|
|
.embed { background-color: var(--discord-embed-bg); border-left: 4px solid var(--discord-text-muted); border-radius: 4px; padding: 0.5rem 1rem; margin-top: 8px; display: grid; grid-template-columns: 1fr auto; gap: 1rem; }
|
|
.embed-main { grid-column: 1 / 2; display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
|
|
.embed-thumbnail { grid-column: 2 / 3; grid-row: 1 / 2; align-self: start; }
|
|
.embed-thumbnail img { width: 80px; height: 80px; border-radius: 4px; object-fit: contain; }
|
|
.embed-image, .embed-footer { grid-column: 1 / -1; }
|
|
.embed-author { display: flex; align-items: center; font-weight: 500; font-size: 14px; }
|
|
.embed-author-icon { width: 24px; height: 24px; border-radius: 50%; margin-right: 8px; }
|
|
.embed-author a { color: #fff; text-decoration: none; }
|
|
.embed-author a:hover { text-decoration: underline; }
|
|
.embed-title { font-weight: 700; font-size: 16px; }
|
|
.embed-title a { color: var(--discord-text-link); text-decoration: none; }
|
|
.embed-title a:hover { text-decoration: underline; }
|
|
.embed-description { font-size: 14px; font-weight: 400; }
|
|
.embed-description p, .embed-description ul, .embed-description ol { margin: 0; }
|
|
.embed-fields { display: grid; gap: 8px 16px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
|
|
.field { font-size: 14px; }
|
|
.field.full-width { grid-column: 1 / -1; }
|
|
.field-name { font-weight: 700; color: #fff; margin-bottom: 2px; }
|
|
.field-value p { margin: 0; }
|
|
.embed-image img { max-width: 100%; max-height: 400px; border-radius: 4px; margin-top: 1rem; }
|
|
.embed-footer { display: flex; align-items: center; font-size: 12px; color: var(--discord-text-muted); margin-top: 0.5rem; }
|
|
.embed-footer-icon { width: 20px; height: 20px; border-radius: 50%; margin-right: 8px; }
|
|
h1, h2 { color: #fff; border-bottom: 1px solid var(--discord-border); padding-bottom: 10px; margin-bottom: 20px; }
|
|
.instructions { background-color: var(--discord-embed-bg); padding: 15px; border-radius: 5px; font-size: 14px; line-height: 1.6; }
|
|
code { background-color: #202225; padding: 2px 4px; border-radius: 3px; font-family: Consolas, monospace; }
|
|
.webhook-list { list-style: none; padding: 0; }
|
|
.webhook-list li a { display: flex; justify-content: space-between; align-items: center; background-color: var(--discord-embed-bg); padding: 15px; margin-bottom: 10px; border-radius: 5px; text-decoration: none; color: var(--discord-text); transition: background-color 0.2s; }
|
|
.webhook-list li a:hover { background-color: #40444b; }
|
|
.webhook-list .url { font-size: 12px; color: var(--discord-text-muted); font-family: Consolas, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 50%; }
|
|
.back-link { display: inline-block; margin-bottom: 20px; color: var(--discord-text-link); text-decoration: none; }
|
|
.back-link:hover { text-decoration: underline; }
|
|
</style>
|
|
</head>
|
|
<body><div class="container">{% block content %}{% endblock %}</div></body>
|
|
</html>
|
|
"""
|
|
HOME_TEMPLATE = """
|
|
{% extends "base.html" %}
|
|
{% block content %}
|
|
<h1>Ontvangen Webhooks</h1>
|
|
<div class="instructions">
|
|
<p>Hieronder staat een lijst van alle webhooks die ten minste één keer een bericht hebben ontvangen. Klik op een webhook om de laatste data te zien.</p>
|
|
<p>Inkomende URL's hebben de vorm <code>/webhook/<naam></code>.</p>
|
|
<p>Uitgaande URL: <code>{{ base_url or 'Niet geconfigureerd' }}<naam>{{ params }}</code></p>
|
|
</div>
|
|
<ul class="webhook-list">
|
|
{% for id, data in webhooks.items() %}
|
|
<li>
|
|
<a href="{{ url_for('display_webhook', webhook_id=id) }}">
|
|
<span>{{ id | capitalize }}</span>
|
|
<span class="url">Laatst gezien: {{ data.received_at }}</span>
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li>Nog geen webhooks ontvangen. Stuur een POST verzoek naar <code>/webhook/<jouw-id></code>.</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endblock %}
|
|
"""
|
|
FORWARD_WEBHOOK_TEMPLATE = """
|
|
{% if data and data.username != 'Nog geen data' %}
|
|
<div class="discord-message">
|
|
<img src="{{ data.avatar_url or 'https://cdn.discordapp.com/embed/avatars/0.png' }}" alt="Avatar" class="avatar" onerror="this.onerror=null;this.src='https://cdn.discordapp.com/embed/avatars/0.png';">
|
|
<div class="message-content">
|
|
<div class="author-info">
|
|
<span class="author-name">{{ data.username or 'Onbekende Bot' }}</span>
|
|
<span class="timestamp">{{ data.received_at }}</span>
|
|
</div>
|
|
{% if data.content %}<div class="content-text">{{ data.content }}</div>{% endif %}
|
|
{% for embed in data.embeds %}
|
|
<div class="embed" style="border-left-color: #{'%06x' % embed.color if embed.color else '4f545c'};">
|
|
<div class="embed-main">
|
|
{% if embed.author %}<div class="embed-author">{% if embed.author.icon_url %}<img src="{{ embed.author.icon_url }}" alt="" class="embed-author-icon">{% endif %}{% if embed.author.url %}<a href="{{ embed.author.url }}" target="_blank" rel="noopener noreferrer">{{ embed.author.name }}</a>{% else %}<span>{{ embed.author.name }}</span>{% endif %}</div>{% endif %}
|
|
{% if embed.title %}<div class="embed-title">{% if embed.url %}<a href="{{ embed.url }}" target="_blank" rel="noopener noreferrer">{{ embed.title }}</a>{% else %}<span>{{ embed.title }}</span>{% endif %}</div>{% endif %}
|
|
{% if embed.description %}<div class="embed-description">{{ embed.description | markdown }}</div>{% endif %}
|
|
{% if embed.fields %}
|
|
<div class="embed-fields">
|
|
{% for field in embed.fields %}
|
|
<div class="field {% if not field.inline %}full-width{% endif %}">
|
|
<div class="field-name">{{ field.name }}</div>
|
|
<div class="field-value">{{ field.value | markdown }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if embed.thumbnail and embed.thumbnail.url %}
|
|
<div class="embed-thumbnail">
|
|
<img src="{{ embed.thumbnail.url }}" alt="Thumbnail">
|
|
</div>
|
|
{% endif %}
|
|
{% if embed.image and embed.image.url %}
|
|
<div class="embed-image">
|
|
<img src="{{ embed.image.url }}" alt="Embed Image">
|
|
</div>
|
|
{% endif %}
|
|
{% if embed.footer %}
|
|
<div class="embed-footer">
|
|
{% if embed.footer.icon_url %}<img src="{{ embed.footer.icon_url }}" alt="" class="embed-footer-icon">{% endif %}<span>{{ embed.footer.text }}{% if embed.timestamp %} • {{ embed.timestamp }}{% endif %}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<p style="color: var(--discord-text-muted);">{{ data.content }}</p>
|
|
{% endif %}
|
|
"""
|
|
UI_WEBHOOK_VIEW_TEMPLATE = """
|
|
{% extends "base.html" %}
|
|
{% block content %}
|
|
<a href="{{ url_for('list_webhooks') }}" class="back-link">← Terug naar overzicht</a>
|
|
<h2>Laatste bericht voor: {{ webhook_id | capitalize }}</h2>
|
|
{{ content | safe }}
|
|
{% endblock %}
|
|
"""
|
|
|
|
# Configureer de Jinja2 loader
|
|
app.jinja_loader = DictLoader({
|
|
'base.html': BASE_TEMPLATE,
|
|
'home.html': HOME_TEMPLATE,
|
|
'forward_webhook.html': FORWARD_WEBHOOK_TEMPLATE,
|
|
'ui_webhook_view.html': UI_WEBHOOK_VIEW_TEMPLATE,
|
|
})
|
|
|
|
@app.template_filter('markdown')
|
|
def markdown_filter(s):
|
|
"""Rendert markdown en verwijdert de omringende <p> tags."""
|
|
if not s:
|
|
return ""
|
|
html = markdown.markdown(s, extensions=['fenced_code', 'nl2br'])
|
|
if html.startswith('<p>') and html.endswith('</p>'):
|
|
html = html[3:-4]
|
|
return html
|
|
|
|
def forward_webhook(data, target_url, webhook_id):
|
|
"""Genereert simpele HTML en stuurt dit door naar de opgegeven target_url."""
|
|
if not target_url:
|
|
print(f"Skipping forwarding voor webhook '{webhook_id}': geen uitgaande URL geconfigureerd.")
|
|
return
|
|
|
|
try:
|
|
html_parts = []
|
|
for embed in data.get('embeds', []):
|
|
if embed.get('author') and embed.get('author').get('name'):
|
|
html_parts.append(f"<strong>{embed.get('author').get('name')}</strong>")
|
|
if embed.get('title'):
|
|
title, url = embed.get('title'), embed.get('url')
|
|
html_parts.append(f"<strong><a href='{url}'>{title}</a></strong>" if url else f"<strong>{title}</strong>")
|
|
if embed.get('description'):
|
|
html_parts.append(markdown_filter(embed.get('description')))
|
|
if embed.get('fields'):
|
|
for field in embed.get('fields'):
|
|
html_parts.append(f"<br><strong>{field.get('name', '')}</strong><br>{markdown_filter(field.get('value', ''))}")
|
|
if embed.get('image') and embed.get('image').get('url'):
|
|
html_parts.append(f"<br><img src='{embed['image']['url']}' alt='Embed Image' style='max-width:100%; height:auto;'>")
|
|
html_content = "<br>".join(html_parts)
|
|
|
|
plain_text_parts = []
|
|
if data.get('username'): plain_text_parts.append(data.get('username'))
|
|
if data.get('content'): plain_text_parts.append(data.get('content'))
|
|
for embed in data.get('embeds', []):
|
|
if embed.get('title'): plain_text_parts.append(embed.get('title'))
|
|
if embed.get('description'): plain_text_parts.append(embed.get('description'))
|
|
if embed.get('fields'):
|
|
for field in embed.get('fields'): plain_text_parts.append(f"{field.get('name', '')}: {field.get('value', '')}")
|
|
plain_text_body = "\n".join(plain_text_parts)
|
|
|
|
simple_payload = {"text": plain_text_body, "html": html_content, "username": data.get('username')}
|
|
headers = {'Content-Type': 'application/json'}
|
|
|
|
response = requests.post(target_url, json=simple_payload, headers=headers, timeout=10)
|
|
response.raise_for_status()
|
|
print(f"Webhook '{webhook_id}' succesvol doorgestuurd als VEREENVOUDIGDE JSON naar {target_url}. Status: {response.status_code}")
|
|
|
|
except Exception as e:
|
|
print(f"FOUT: Kon webhook '{webhook_id}' niet doorsturen. Error: {e}")
|
|
|
|
@app.route('/')
|
|
def list_webhooks():
|
|
"""Toont de lijst met ontvangen webhooks."""
|
|
return render_template(
|
|
'home.html',
|
|
webhooks=webhook_data_store,
|
|
base_url=OUTGOING_WEBHOOK_BASE_URL,
|
|
params=OUTGOING_WEBHOOK_PARAMS
|
|
)
|
|
|
|
@app.route('/view/<webhook_id>')
|
|
def display_webhook(webhook_id):
|
|
"""Toont de laatst ontvangen data voor een specifieke webhook."""
|
|
data = webhook_data_store.get(webhook_id)
|
|
if not data:
|
|
# Toon een wacht-bericht als er nog nooit data is ontvangen voor deze ID
|
|
data = {
|
|
"username": "Nog geen data",
|
|
"content": f"Wacht op de eerste webhook op /webhook/{webhook_id}...",
|
|
"embeds": [],
|
|
"received_at": "Nooit"
|
|
}
|
|
|
|
content = render_template('forward_webhook.html', data=data)
|
|
return render_template('ui_webhook_view.html', content=content, webhook_id=webhook_id)
|
|
|
|
@app.route('/webhook/<webhook_id>', methods=['POST'])
|
|
def receive_webhook(webhook_id):
|
|
"""Ontvangt de JSON-payload, slaat deze op en stuurt deze door."""
|
|
if not request.is_json:
|
|
return jsonify({"error": "Request must be JSON"}), 400
|
|
|
|
data = request.json
|
|
data['received_at'] = datetime.datetime.now().strftime('%d-%m-%Y %H:%M:%S')
|
|
data['webhook_id'] = webhook_id
|
|
|
|
# Sla de data op in de store. Nieuwe ID's worden automatisch toegevoegd.
|
|
webhook_data_store[webhook_id] = data
|
|
|
|
# Bouw de uitgaande URL dynamisch op, alleen als de base URL is geconfigureerd.
|
|
if OUTGOING_WEBHOOK_BASE_URL:
|
|
# Zorg ervoor dat er maar één slash tussen de base url en de id zit.
|
|
target_url = f"{OUTGOING_WEBHOOK_BASE_URL.rstrip('/')}/{webhook_id}{OUTGOING_WEBHOOK_PARAMS}"
|
|
forward_webhook(data, target_url, webhook_id)
|
|
|
|
return jsonify({"status": "succes"}), 200
|
|
|
|
if __name__ == '__main__':
|
|
port = 5001
|
|
print(f"Server wordt gestart op http://127.0.0.1:{port}")
|
|
print("De app accepteert nu POST-verzoeken op ELK pad onder /webhook/...")
|
|
print("Bijvoorbeeld: http://127.0.0.1:5001/webhook/jellyfin")
|
|
print("Druk op CTRL+C om te stoppen.")
|
|
from waitress import serve
|
|
serve(app, host='0.0.0.0', port=port)
|