diff --git a/cloudflare-worker/README.md b/cloudflare-worker/README.md index 3c63883..4861beb 100644 --- a/cloudflare-worker/README.md +++ b/cloudflare-worker/README.md @@ -84,10 +84,62 @@ npx wrangler secret put POSTFACH_CODE # z.B. erzeugen mit: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" npx wrangler secret put ENCRYPTION_KEY -# 7. Deployen +# 7. (Optional) Bewerbungen zusätzlich automatisch in Discord posten — +# siehe Abschnitt "Bewerbungen auf Discord (Ticketanizer)" unten. +# Ohne diesen Schritt läuft alles wie gehabt, es fehlt nur die +# Discord-Benachrichtigung. + +# 8. Deployen npx wrangler deploy ``` +## Bewerbungen auf Discord (Ticketanizer) + +Jede eingehende Bewerbung wird zusätzlich zur Speicherung in der Datenbank als +schön formatierte Nachricht (Embed) in den passenden Discord-Kanal gepostet — +pro Bewerbungsbereich ein eigener Kanal/Webhook, exakt wie auf der Website +getrennt (Modi, Scout, Creator, Kooperation). + +**Das läuft komplett über normale Discord-Webhooks — kein Bot, kein +zusätzlicher Code auf Discord-Seite nötig.** Falls Ticketanizer selbst in +seinen Board-/Kanal-Einstellungen bereits eine eigene Ziel-Webhook-URL +anzeigt, kann diese genauso eingetragen werden wie eine normale +Discord-Webhook-URL — beide funktionieren identisch (Discord-Webhook-Format). + +### Einrichtung (pro Kanal ca. 1 Minute) + +1. Im Discord-Kanal (z.B. dem Modi-Bewerbungskanal): Kanal bearbeiten → + **Integrationen** → **Webhooks** → **Neuer Webhook**. +2. Der Webhook kann beliebig heißen (z.B. "Homepage-Bewerbungen"). **Webhook-URL + kopieren.** +3. Als Secret im Worker hinterlegen (einmal pro Bewerbungsbereich, nur die + URLs eintragen, die schon vorhanden sind — fehlende werden einfach + übersprungen, es gibt dafür keine Fehlermeldung): + +```bash +npx wrangler secret put DISCORD_WEBHOOK_MODI +npx wrangler secret put DISCORD_WEBHOOK_SCOUT +npx wrangler secret put DISCORD_WEBHOOK_CREATOR +npx wrangler secret put DISCORD_WEBHOOK_KOOPERATION +``` + +Jeweils die kopierte Webhook-URL einfügen, wenn danach gefragt wird. + +4. Danach einmal `npx wrangler deploy`, damit der Worker die neuen Secrets + kennt (Secrets selbst brauchen keinen Deploy, aber schadet nicht). + +### Wichtig + +- **Discord ist rein informativ, kein Ausfallpunkt.** Fehlt eine Webhook-URL + oder ist Discord kurz nicht erreichbar, wird die Bewerbung trotzdem ganz + normal in der Datenbank gespeichert — nur die Zusatz-Nachricht in Discord + entfällt dann. +- Secrets stehen **nie** im Code oder in `wrangler.toml` (die ist eingecheckt), + sondern ausschließlich verschlüsselt bei Cloudflare — genau wie + `POSTFACH_CODE` und `ENCRYPTION_KEY`. +- Ändern/Ersetzen einer URL: den jeweiligen `wrangler secret put ...`-Befehl + erneut ausführen, überschreibt den alten Wert. + ## Wer den Owner-Code kennen darf Nur du. Er ist dein Hauptadministrator-Zugang — sieht ausnahmslos alles, kann Zugänge/Rollen/Codes diff --git a/cloudflare-worker/src/lib/discord.js b/cloudflare-worker/src/lib/discord.js new file mode 100644 index 0000000..6f2232f --- /dev/null +++ b/cloudflare-worker/src/lib/discord.js @@ -0,0 +1,97 @@ +/* ===================================================================== + lib/discord.js — Bewerbungen als Discord-Webhook-Nachricht weiterleiten + (Ticketanizer-Kanäle auf dem Discord). + + Rein optional und niemals ein Ausfallpunkt: Fehlt die Webhook-URL für + einen Bewerbungstyp, oder schlägt der Versand an Discord fehl (Discord + down, falsche URL, ...), wird die Bewerbung trotzdem ganz normal in der + Datenbank gespeichert — Discord ist nur ein zusätzlicher Kanal, kein + Ausfallpunkt für die eigentliche Bewerbung. + + Pro Bewerbungsart eine eigene Webhook-URL, als Secret gesetzt (NICHT in + wrangler.toml/git, siehe README.md): + wrangler secret put DISCORD_WEBHOOK_MODI + wrangler secret put DISCORD_WEBHOOK_SCOUT + wrangler secret put DISCORD_WEBHOOK_CREATOR + wrangler secret put DISCORD_WEBHOOK_KOOPERATION + ===================================================================== */ + +const WEBHOOK_ENV_KEYS = { + modi: "DISCORD_WEBHOOK_MODI", + scout: "DISCORD_WEBHOOK_SCOUT", + creator: "DISCORD_WEBHOOK_CREATOR", + kooperation: "DISCORD_WEBHOOK_KOOPERATION", +}; + +const TYPE_LABELS = { + modi: "🩵 Neue Bewerbung — Modi (Team Dogi)", + scout: "🔎 Neue Bewerbung — Scout (Spicy Media)", + creator: "🎬 Neue Bewerbung — Creator (Spicy Media)", + kooperation: "🤝 Neue Kooperationsanfrage", +}; + +// Farbe je Bewerbungsart, passend zur Farbwelt auf der Website +// (babyblau = Dogfather/Team-Dogi-Community, rot = Spicy Media, lila = Kooperation). +const TYPE_COLORS = { + modi: 0x8fd9ea, + scout: 0xd94a5c, + creator: 0xd94a5c, + kooperation: 0x8b5cf6, +}; + +// Menschlich lesbare Feldbeschriftungen für alle Formularfelder, die aktuell +// auf der Website vorkommen (siehe bewerben-*.html, jeweils name="..."). +// Unbekannte Feldnamen (z.B. nach künftigen Formular-Änderungen) werden +// einfach unverändert als Beschriftung übernommen statt zu fehlen. +const FIELD_LABELS = { + name: "Name", + email: "E-Mail", + kontakt: "Kontakt (Discord/TikTok)", + erfahrung: "Erfahrung", + verfuegbarkeit: "Verfügbarkeit", + warum: "Warum möchtest du dabei sein?", + platform: "Plattform", + content: "Content-Art", + firma: "Firma / Projekt", + idee: "Idee / Anliegen", +}; + +function feldWert(wert) { + const text = String(wert ?? "").trim(); + if (!text) return "_(leer)_"; + // Discord-Embed-Feldwerte sind auf 1024 Zeichen begrenzt. + return text.length > 1000 ? text.slice(0, 1000) + "…" : text; +} + +export async function bewerbungAnDiscordSenden(env, type, data) { + const envKey = WEBHOOK_ENV_KEYS[type]; + const webhookUrl = envKey && env[envKey]; + if (!webhookUrl) return; // kein Webhook für diesen Bereich eingerichtet — einfach überspringen + + const fields = Object.entries(data || {}) + .filter(([, v]) => String(v ?? "").trim() !== "") + .map(([key, value]) => ({ + name: FIELD_LABELS[key] || key, + value: feldWert(value), + inline: String(value).length < 40, + })); + + const embed = { + title: TYPE_LABELS[type] || "Neue Bewerbung", + color: TYPE_COLORS[type] ?? 0x8fd9ea, + fields: fields.length ? fields : [{ name: "Hinweis", value: "Keine Angaben übermittelt." }], + footer: { text: "DOGFATHER UNIVERSE — automatisch von der Homepage" }, + timestamp: new Date().toISOString(), + }; + + try { + await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ embeds: [embed] }), + }); + } catch { + // Discord nicht erreichbar o.ä. — die Bewerbung selbst ist trotzdem + // sicher in der Datenbank gelandet, das darf hier nicht scheitern. + } +} diff --git a/cloudflare-worker/src/routes/applications.js b/cloudflare-worker/src/routes/applications.js index d82614e..619f246 100644 --- a/cloudflare-worker/src/routes/applications.js +++ b/cloudflare-worker/src/routes/applications.js @@ -7,6 +7,7 @@ import { generateId, nowIso } from "../lib/crypto.js"; import { hasPermission } from "../lib/permissions.js"; import { logAction } from "../lib/audit.js"; import { json } from "../lib/http.js"; +import { bewerbungAnDiscordSenden } from "../lib/discord.js"; const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"]; @@ -24,7 +25,7 @@ function viewPermissionForType(type) { return type === "creator" ? "APPLICATIONS_VIEW_CREATOR" : "APPLICATIONS_VIEW_MANAGER_SCOUT"; } -export async function submitApplication(request, env) { +export async function submitApplication(request, env, ctx) { let body; try { body = await request.json(); @@ -45,6 +46,15 @@ export async function submitApplication(request, env) { .bind(id, type, JSON.stringify(data), nowIso()) .run(); + // Zusätzlich (rein informativ, blockiert die Bewerbung selbst nie) als + // Nachricht in den passenden Discord-Kanal (Ticketanizer) weiterleiten. + const discordVersand = bewerbungAnDiscordSenden(env, type, data); + if (ctx && typeof ctx.waitUntil === "function") { + ctx.waitUntil(discordVersand); + } else { + await discordVersand.catch(() => {}); + } + return json({ ok: true }); } diff --git a/cloudflare-worker/src/worker.js b/cloudflare-worker/src/worker.js index ff57281..0179513 100644 --- a/cloudflare-worker/src/worker.js +++ b/cloudflare-worker/src/worker.js @@ -228,7 +228,7 @@ const AUTHED_ROUTES = { }; export default { - async fetch(request, env) { + async fetch(request, env, ctx) { const url = new URL(request.url); const routeKey = `${request.method} ${url.pathname}`; @@ -240,7 +240,7 @@ export default { return json({ ok: true, service: "dogfather-universe-interner-bereich" }); } if (url.pathname === "/submit" && request.method === "POST") { - return submitApplication(request, env); + return submitApplication(request, env, ctx); } if (url.pathname === "/live-status" && request.method === "GET") { return handleLiveStatusGet(env);