Upload files to "/"
This commit is contained in:
commit
713bc5fdf8
1 changed files with 301 additions and 0 deletions
301
app.py
Normal file
301
app.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
# -*- 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue