Bewerbungen zusätzlich automatisch in Discord (Ticketanizer) posten

Jede eingehende Bewerbung landet weiterhin wie bisher in der D1-Datenbank,
wird zusätzlich aber (falls eine Webhook-URL hinterlegt ist) als formatierte
Discord-Nachricht in den passenden Kanal gepostet - pro Bewerbungsbereich
(Modi/Scout/Creator/Kooperation) eine eigene Webhook-URL, exakt wie auf der
Website getrennt. Läuft über normale Discord-Webhooks, kompatibel mit
Ticketanizer, falls dessen Kanäle eigene Ziel-URLs im selben Format anbieten.

- Neu: cloudflare-worker/src/lib/discord.js (Embed-Aufbau, Feld-Labels
  pro Formular, Farbe je Bewerbungsart)
- applications.js: submitApplication ruft den Versand nach dem Speichern
  auf, über ctx.waitUntil() damit die Antwort an den Nutzer nicht wartet
- worker.js: ctx wird jetzt an fetch() durchgereicht
- Discord ist rein informativ - fehlt die Webhook-URL oder ist Discord
  kurz nicht erreichbar, wird die Bewerbung trotzdem ganz normal
  gespeichert
- README.md: Einrichtungsanleitung (wrangler secret put DISCORD_WEBHOOK_*)
This commit is contained in:
2026-08-01 12:24:32 +02:00
parent d8664e7476
commit 992e3de446
4 changed files with 163 additions and 4 deletions
+53 -1
View File
@@ -84,10 +84,62 @@ npx wrangler secret put POSTFACH_CODE
# z.B. erzeugen mit: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" # z.B. erzeugen mit: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
npx wrangler secret put ENCRYPTION_KEY 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 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 ## Wer den Owner-Code kennen darf
Nur du. Er ist dein Hauptadministrator-Zugang — sieht ausnahmslos alles, kann Zugänge/Rollen/Codes Nur du. Er ist dein Hauptadministrator-Zugang — sieht ausnahmslos alles, kann Zugänge/Rollen/Codes
+97
View File
@@ -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.
}
}
+11 -1
View File
@@ -7,6 +7,7 @@ import { generateId, nowIso } from "../lib/crypto.js";
import { hasPermission } from "../lib/permissions.js"; import { hasPermission } from "../lib/permissions.js";
import { logAction } from "../lib/audit.js"; import { logAction } from "../lib/audit.js";
import { json } from "../lib/http.js"; import { json } from "../lib/http.js";
import { bewerbungAnDiscordSenden } from "../lib/discord.js";
const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"]; const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"];
@@ -24,7 +25,7 @@ function viewPermissionForType(type) {
return type === "creator" ? "APPLICATIONS_VIEW_CREATOR" : "APPLICATIONS_VIEW_MANAGER_SCOUT"; 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; let body;
try { try {
body = await request.json(); body = await request.json();
@@ -45,6 +46,15 @@ export async function submitApplication(request, env) {
.bind(id, type, JSON.stringify(data), nowIso()) .bind(id, type, JSON.stringify(data), nowIso())
.run(); .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 }); return json({ ok: true });
} }
+2 -2
View File
@@ -228,7 +228,7 @@ const AUTHED_ROUTES = {
}; };
export default { export default {
async fetch(request, env) { async fetch(request, env, ctx) {
const url = new URL(request.url); const url = new URL(request.url);
const routeKey = `${request.method} ${url.pathname}`; const routeKey = `${request.method} ${url.pathname}`;
@@ -240,7 +240,7 @@ export default {
return json({ ok: true, service: "dogfather-universe-interner-bereich" }); return json({ ok: true, service: "dogfather-universe-interner-bereich" });
} }
if (url.pathname === "/submit" && request.method === "POST") { if (url.pathname === "/submit" && request.method === "POST") {
return submitApplication(request, env); return submitApplication(request, env, ctx);
} }
if (url.pathname === "/live-status" && request.method === "GET") { if (url.pathname === "/live-status" && request.method === "GET") {
return handleLiveStatusGet(env); return handleLiveStatusGet(env);