diff --git a/cloudflare-worker/README.md b/cloudflare-worker/README.md index 4861beb..aeb2cb5 100644 --- a/cloudflare-worker/README.md +++ b/cloudflare-worker/README.md @@ -84,61 +84,65 @@ 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. (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. +# 7. (Optional) Bewerbungen zusätzlich automatisch als Ticket bei +# Ticketanizer anlegen (dort in Discord sichtbar) — siehe Abschnitt +# "Bewerbungen als Ticketanizer-Tickets" unten. Ohne diesen Schritt +# läuft alles wie gehabt, es fehlt nur das automatische Ticket. # 8. Deployen npx wrangler deploy ``` -## Bewerbungen auf Discord (Ticketanizer) +## Bewerbungen als Ticketanizer-Tickets -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). +Jede eingehende Bewerbung wird zusätzlich zur Speicherung in der eigenen +Datenbank als **echtes Ticket** bei Ticketanizer angelegt — im passenden +Bereich (Modi/Scout/Creator/Kooperation), exakt wie auf der Website getrennt. +Das läuft über die offizielle Ticketanizer-Inbound-Ticket-API +(`POST https://api.ticketanizer.com/v1/tickets`), kein Discord-Bot-Code nötig. -**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 (einmalig, ca. 2 Minuten) -### 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): +1. Im Ticketanizer-Dashboard: **API & Webhooks → API-Keys → Key erstellen** + (Name z.B. "DOGFATHER UNIVERSE Homepage"). Den Key **sofort kopieren** — + wird meist nur einmal im Klartext angezeigt. +2. Als Secret im Worker hinterlegen: ```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 +npx wrangler secret put TICKETANIZER_API_KEY ``` -Jeweils die kopierte Webhook-URL einfügen, wenn danach gefragt wird. +3. Danach einmal `npx wrangler deploy`. -4. Danach einmal `npx wrangler deploy`, damit der Worker die neuen Secrets - kennt (Secrets selbst brauchen keinen Deploy, aber schadet nicht). +Die Zuordnung Bewerbungsbereich → Ticketanizer-Panel ist fest in +`src/lib/ticketanizer.js` (`PANEL_IDS`) hinterlegt, abgelesen aus dem +Ticketanizer-Dashboard unter **Panels**: + +| Bewerbung auf der Website | Ticketanizer-Panel | panel_id | +|---|---|---| +| Modi | Modi-Bewerbung | 4 | +| Kooperation | Kooperationsbewerbung | 5 | +| Scout | Scout-Bewerbung | 6 | +| Creator | Manager-Bewerbung | 7 | + +Ändert sich ein Panel (neu angelegt, andere ID), einfach die Zahlen in +`PANEL_IDS` in `src/lib/ticketanizer.js` anpassen und neu deployen. ### 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 +- **Ticketanizer ist rein informativ, kein Ausfallpunkt.** Fehlt der API-Key + oder ist Ticketanizer kurz nicht erreichbar, wird die Bewerbung trotzdem + ganz normal in der eigenen Datenbank gespeichert — nur das Zusatz-Ticket entfällt dann. -- Secrets stehen **nie** im Code oder in `wrangler.toml` (die ist eingecheckt), +- Der Key steht **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. +- Ändern/Ersetzen des Keys: `wrangler secret put TICKETANIZER_API_KEY` erneut + ausführen, überschreibt den alten Wert. +- Es wird bewusst nur Schritt 1 der Ticketanizer-API genutzt (Ticket + eröffnen). Das Nachladen von Staff-Antworten (Schritt 3, "pollen") ist + hier nicht eingebaut, da eine Bewerbung ein einmaliger Vorgang ist und + kein dauerhaft laufender Chat. ## Wer den Owner-Code kennen darf diff --git a/cloudflare-worker/src/lib/discord.js b/cloudflare-worker/src/lib/discord.js deleted file mode 100644 index 6f2232f..0000000 --- a/cloudflare-worker/src/lib/discord.js +++ /dev/null @@ -1,97 +0,0 @@ -/* ===================================================================== - 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/lib/ticketanizer.js b/cloudflare-worker/src/lib/ticketanizer.js new file mode 100644 index 0000000..41a5b3b --- /dev/null +++ b/cloudflare-worker/src/lib/ticketanizer.js @@ -0,0 +1,95 @@ +/* ===================================================================== + lib/ticketanizer.js — Bewerbungen als echtes Ticket bei Ticketanizer + eröffnen (dort in Discord sichtbar, pro Bewerbungsbereich ein eigenes + Panel/Board). + + API-Doku (aus dem Ticketanizer-Dashboard, "API & Webhooks" → "API-Keys" → + "So nutzt du sie"): + Basis-URL: https://api.ticketanizer.com + POST /v1/tickets + Header: X-API-Key, Content-Type: application/json + Body: { panel_id, visitor_name, first_message } + + Wir nutzen hier bewusst NUR den ersten Schritt (Ticket eröffnen) — es + ist eine einmalige Bewerbung, kein Live-Chat, deshalb kein Nachladen + von Staff-Antworten nötig (das würde einen dauerhaft laufenden Prozess + brauchen, den ein Cloudflare Worker so nicht hat). + + panel_id pro Bewerbungsbereich (aus dem Ticketanizer-Dashboard → + "Panels", von Dogi am 2026-08-01 abgelesen): + 4 = Modi-Bewerbung + 5 = Kooperationsbewerbung + 6 = Scout-Bewerbung + 7 = Manager-Bewerbung (= unsere "Creator"-Bewerbung: Streamer sucht + einen Manager bei Spicy Media) + + Rein informativ, kein Ausfallpunkt: Fehlt der API-Key, oder schlägt der + Versand fehl (Ticketanizer down, falscher Key, ...), wird die Bewerbung + trotzdem ganz normal in der eigenen Datenbank gespeichert. + ===================================================================== */ + +const TICKETANIZER_BASIS_URL = "https://api.ticketanizer.com"; + +const PANEL_IDS = { + modi: 4, + kooperation: 5, + scout: 6, + creator: 7, // "Manager-Bewerbung" bei Ticketanizer +}; + +// 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 ersteNachrichtAufbauen(type, data) { + const zeilen = Object.entries(data || {}) + .filter(([, v]) => String(v ?? "").trim() !== "") + .map(([key, value]) => `**${FIELD_LABELS[key] || key}:** ${String(value).trim()}`); + + if (!zeilen.length) zeilen.push("_Keine weiteren Angaben übermittelt._"); + + // Ticketanizer-Nachrichten sind vermutlich nicht beliebig lang — sicherheitshalber + // deckeln, damit ein sehr langer Freitext (z.B. "warum") den Versand nicht sprengt. + const text = zeilen.join("\n"); + return text.length > 3500 ? text.slice(0, 3500) + "…" : text; +} + +export async function bewerbungAlsTicketAnlegen(env, type, data) { + const apiKey = env.TICKETANIZER_API_KEY; + const panelId = PANEL_IDS[type]; + if (!apiKey || !panelId) return; // nicht eingerichtet für diesen Bereich — einfach überspringen + + const visitorName = String(data?.name || data?.firma || "Unbekannt").trim().slice(0, 80) || "Unbekannt"; + const firstMessage = ersteNachrichtAufbauen(type, data); + + try { + await fetch(`${TICKETANIZER_BASIS_URL}/v1/tickets`, { + method: "POST", + headers: { + "X-API-Key": apiKey, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + panel_id: panelId, + visitor_name: visitorName, + first_message: firstMessage, + }), + }); + } catch { + // Ticketanizer nicht erreichbar o.ä. — die Bewerbung selbst ist trotzdem + // sicher in der eigenen Datenbank gelandet, das darf hier nicht scheitern. + } +} diff --git a/cloudflare-worker/src/routes/applications.js b/cloudflare-worker/src/routes/applications.js index 619f246..73d86dd 100644 --- a/cloudflare-worker/src/routes/applications.js +++ b/cloudflare-worker/src/routes/applications.js @@ -7,7 +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"; +import { bewerbungAlsTicketAnlegen } from "../lib/ticketanizer.js"; const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"]; @@ -47,12 +47,12 @@ export async function submitApplication(request, env, ctx) { .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); + // echtes Ticket bei Ticketanizer im passenden Bereich anlegen. + const ticketVersand = bewerbungAlsTicketAnlegen(env, type, data); if (ctx && typeof ctx.waitUntil === "function") { - ctx.waitUntil(discordVersand); + ctx.waitUntil(ticketVersand); } else { - await discordVersand.catch(() => {}); + await ticketVersand.catch(() => {}); } return json({ ok: true });