diff --git a/cloudflare-worker/README.md b/cloudflare-worker/README.md index aeb2cb5..7c1ab2d 100644 --- a/cloudflare-worker/README.md +++ b/cloudflare-worker/README.md @@ -143,6 +143,48 @@ Ticketanizer-Dashboard unter **Panels**: 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. +- **Wichtige Einschränkung (2026-08-01 festgestellt):** Über die Inbound- + Ticket-API angelegte Tickets landen bei Ticketanizer nur in der + **Web-Inbox** (eigene Weboberfläche, siehe Dashboard → "Inbox") und lösen + lediglich einen Rollen-Ping im Log-Kanal aus — es entsteht **kein** + echter, sichtbarer Ticket-Kanal in Discord (auch nicht nach manuellem + "Übernehmen" in der Inbox). Deshalb zusätzlich der Discord-Webhook-Weg + weiter unten, der die Bewerbung garantiert sichtbar direkt in den + passenden Kanal postet. + +## Bewerbungen direkt in Discord-Kanälen (Webhooks) + +Zusätzlich zu Ticketanizer wird jede Bewerbung als formatierte Nachricht +(Embed) direkt per normalem Discord-Webhook in den passenden Kanal gepostet — +das ist der Weg, der garantiert sichtbar in Discord ankommt (siehe +Einschränkung oben). + +### Kanal-Zuordnung (von Dogi aus Discord kopiert, 2026-08-01) + +| Bewerbung auf der Website | Discord-Kanal | Secret | +|---|---|---| +| Modi | `#modi-bewerbung` | `DISCORD_WEBHOOK_MODI` | +| Kooperation | `#kooperations-bewerbung` | `DISCORD_WEBHOOK_KOOPERATION` | +| Scout **und** Creator | `#scout-bewerbung` (gemeinsam) | `DISCORD_WEBHOOK_SCOUT_MANAGER` | + +### Einrichtung (pro Kanal ca. 1 Minute) + +1. Im jeweiligen Discord-Kanal: Kanal bearbeiten → **Integrationen** → + **Webhooks** → **Neuer Webhook** → **Webhook-URL kopieren**. +2. Als Secret hinterlegen: + +```bash +npx wrangler secret put DISCORD_WEBHOOK_MODI +npx wrangler secret put DISCORD_WEBHOOK_KOOPERATION +npx wrangler secret put DISCORD_WEBHOOK_SCOUT_MANAGER +``` + +3. Danach einmal `npx wrangler deploy`. + +Genau wie bei Ticketanizer: **rein informativ, kein Ausfallpunkt.** Fehlt +eine Webhook-URL oder ist Discord kurz nicht erreichbar, wird die Bewerbung +trotzdem ganz normal gespeichert. Die Zuordnung Typ → Webhook steht in +`src/lib/discord-webhooks.js` (`WEBHOOK_ENV_KEYS`). ## Wer den Owner-Code kennen darf diff --git a/cloudflare-worker/src/lib/discord-webhooks.js b/cloudflare-worker/src/lib/discord-webhooks.js new file mode 100644 index 0000000..e5f01fe --- /dev/null +++ b/cloudflare-worker/src/lib/discord-webhooks.js @@ -0,0 +1,115 @@ +/* ===================================================================== + lib/discord-webhooks.js — Bewerbungen zusätzlich als ECHTE, sichtbare + Nachricht direkt in den passenden Discord-Kanal posten. + + Hintergrund: Die Ticketanizer-Inbound-Ticket-API (siehe ticketanizer.js) + legt zwar zuverlässig ein Ticket in der Web-Inbox an und pingt eine Rolle + im Log-Kanal, erzeugt aber KEINEN echten, sichtbaren Ticket-Kanal in + Discord (das ist offenbar bewusstes Ticketanizer-Verhalten für + API-Tickets, siehe README). Deshalb zusätzlich hierüber ein stinknormaler + Discord-Webhook, der die komplette Bewerbung als Embed direkt in den + Zielkanal postet — 100% garantiert sichtbar, unabhängig von Ticketanizer. + + Kanal-Zuordnung (von Dogi aus Discord kopiert, 2026-08-01): + modi → #modi-bewerbung + kooperation → #kooperations-bewerbung + scout → #scout-bewerbung (gemeinsam mit creator) + creator → #scout-bewerbung (gemeinsam mit scout — Wunsch von Dogi) + + Secrets (siehe README.md): + DISCORD_WEBHOOK_MODI + DISCORD_WEBHOOK_KOOPERATION + DISCORD_WEBHOOK_SCOUT_MANAGER (für scout UND creator) + + Rein informativ, kein Ausfallpunkt: Fehlt eine Webhook-URL, oder ist + Discord kurz nicht erreichbar, wird die Bewerbung trotzdem ganz normal + in der eigenen Datenbank gespeichert. + ===================================================================== */ + +const WEBHOOK_ENV_KEYS = { + modi: "DISCORD_WEBHOOK_MODI", + kooperation: "DISCORD_WEBHOOK_KOOPERATION", + scout: "DISCORD_WEBHOOK_SCOUT_MANAGER", + creator: "DISCORD_WEBHOOK_SCOUT_MANAGER", +}; + +const TYPE_LABELS = { + modi: "🩵 Neue Bewerbung — Modi (Team Dogi)", + kooperation: "🤝 Neue Kooperationsanfrage", + scout: "🔎 Neue Bewerbung — Scout (Spicy Media)", + creator: "🎬 Neue Bewerbung — Creator / Manager-Bewerbung (Spicy Media)", +}; + +// Farbe je Bewerbungsart, passend zur Farbwelt auf der Website +// (babyblau = Dogfather/Team-Dogi-Community, lila = Kooperation, rot = Spicy Media). +const TYPE_COLORS = { + modi: 0x8fd9ea, + kooperation: 0x8b5cf6, + scout: 0xd94a5c, + creator: 0xd94a5c, +}; + +// Menschlich lesbare Feldbeschriftungen für alle Formularfelder, die aktuell +// auf der Website vorkommen (siehe bewerben-*.html, jeweils name="..."). +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 bewerbungAlsDiscordNachrichtPosten(env, type, data) { + const envKey = WEBHOOK_ENV_KEYS[type]; + const webhookUrl = envKey && env[envKey]; + if (!webhookUrl) { + console.error(`[discord-webhook] Keine Webhook-URL für "${type}" hinterlegt (${envKey}) — übersprungen.`); + return; + } + + 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 { + const antwort = await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ embeds: [embed] }), + }); + if (!antwort.ok) { + const antwortText = await antwort.text().catch(() => ""); + console.error(`[discord-webhook] Fehler beim Posten (${type}): HTTP ${antwort.status} — ${antwortText}`); + } else { + console.log(`[discord-webhook] Nachricht gepostet (${type}).`); + } + } catch (fehler) { + // Discord nicht erreichbar o.ä. — die Bewerbung selbst ist trotzdem + // sicher in der eigenen Datenbank gelandet, das darf hier nicht scheitern. + console.error(`[discord-webhook] Ausnahme beim Posten (${type}):`, fehler && fehler.message); + } +} diff --git a/cloudflare-worker/src/routes/applications.js b/cloudflare-worker/src/routes/applications.js index 73d86dd..62f4249 100644 --- a/cloudflare-worker/src/routes/applications.js +++ b/cloudflare-worker/src/routes/applications.js @@ -8,6 +8,7 @@ import { hasPermission } from "../lib/permissions.js"; import { logAction } from "../lib/audit.js"; import { json } from "../lib/http.js"; import { bewerbungAlsTicketAnlegen } from "../lib/ticketanizer.js"; +import { bewerbungAlsDiscordNachrichtPosten } from "../lib/discord-webhooks.js"; const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"]; @@ -46,13 +47,20 @@ export async function submitApplication(request, env, ctx) { .bind(id, type, JSON.stringify(data), nowIso()) .run(); - // Zusätzlich (rein informativ, blockiert die Bewerbung selbst nie) als - // echtes Ticket bei Ticketanizer im passenden Bereich anlegen. - const ticketVersand = bewerbungAlsTicketAnlegen(env, type, data); + // Zusätzlich (rein informativ, blockiert die Bewerbung selbst nie): + // 1) echtes Ticket bei Ticketanizer im passenden Bereich anlegen + // (Web-Inbox zum Bearbeiten/Antworten) + // 2) direkte, garantiert sichtbare Discord-Nachricht in den passenden + // Kanal posten (Ticketanizer erzeugt bei API-Tickets keinen echten + // Discord-Kanal, siehe discord-webhooks.js) + const zusatzVersand = Promise.all([ + bewerbungAlsTicketAnlegen(env, type, data), + bewerbungAlsDiscordNachrichtPosten(env, type, data), + ]); if (ctx && typeof ctx.waitUntil === "function") { - ctx.waitUntil(ticketVersand); + ctx.waitUntil(zusatzVersand); } else { - await ticketVersand.catch(() => {}); + await zusatzVersand.catch(() => {}); } return json({ ok: true });