Bewerbungen zusätzlich per Discord-Webhook direkt in echte Kanäle posten

Ticketanizers Inbound-Ticket-API erzeugt bei API-Tickets nachweislich
keinen echten Discord-Kanal (nur Web-Inbox + Rollen-Ping im Log-Kanal,
auch nach manuellem Uebernehmen nicht) - das wurde heute gemeinsam mit
Dogi live in der Ticketanizer-Inbox und in Discord verifiziert. Deshalb
zusaetzlich ein normaler Discord-Webhook pro Zielkanal, der die komplette
Bewerbung garantiert sichtbar direkt postet.

Kanal-Zuordnung (von Dogi aus Discord kopiert):
  modi        -> #modi-bewerbung        (DISCORD_WEBHOOK_MODI)
  kooperation -> #kooperations-bewerbung (DISCORD_WEBHOOK_KOOPERATION)
  scout + creator -> #scout-bewerbung gemeinsam (DISCORD_WEBHOOK_SCOUT_MANAGER)

- Neu: cloudflare-worker/src/lib/discord-webhooks.js
- applications.js: postet jetzt parallel zu Ticketanizer (Promise.all,
  weiterhin ueber ctx.waitUntil im Hintergrund, blockiert nie die Antwort)
- Alle 3 Secrets bereits live gesetzt und deployed
- Getestet mit allen 4 Bewerbungstypen (modi/kooperation/scout/creator) -
  laut Log alle erfolgreich gepostet, Testdaten aus D1 wieder entfernt
- README.md: Einschraenkung von Ticketanizer dokumentiert + neue
  Webhook-Einrichtungsanleitung
This commit is contained in:
2026-08-01 14:27:08 +02:00
parent aa219922fd
commit 0d31fb8bb0
3 changed files with 170 additions and 5 deletions
+42
View File
@@ -143,6 +143,48 @@ Ticketanizer-Dashboard unter **Panels**:
eröffnen). Das Nachladen von Staff-Antworten (Schritt 3, "pollen") ist eröffnen). Das Nachladen von Staff-Antworten (Schritt 3, "pollen") ist
hier nicht eingebaut, da eine Bewerbung ein einmaliger Vorgang ist und hier nicht eingebaut, da eine Bewerbung ein einmaliger Vorgang ist und
kein dauerhaft laufender Chat. 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 ## Wer den Owner-Code kennen darf
@@ -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);
}
}
+13 -5
View File
@@ -8,6 +8,7 @@ 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 { bewerbungAlsTicketAnlegen } from "../lib/ticketanizer.js"; import { bewerbungAlsTicketAnlegen } from "../lib/ticketanizer.js";
import { bewerbungAlsDiscordNachrichtPosten } from "../lib/discord-webhooks.js";
const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"]; 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()) .bind(id, type, JSON.stringify(data), nowIso())
.run(); .run();
// Zusätzlich (rein informativ, blockiert die Bewerbung selbst nie) als // Zusätzlich (rein informativ, blockiert die Bewerbung selbst nie):
// echtes Ticket bei Ticketanizer im passenden Bereich anlegen. // 1) echtes Ticket bei Ticketanizer im passenden Bereich anlegen
const ticketVersand = bewerbungAlsTicketAnlegen(env, type, data); // (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") { if (ctx && typeof ctx.waitUntil === "function") {
ctx.waitUntil(ticketVersand); ctx.waitUntil(zusatzVersand);
} else { } else {
await ticketVersand.catch(() => {}); await zusatzVersand.catch(() => {});
} }
return json({ ok: true }); return json({ ok: true });