Bewerbungen als echte Tickets bei Ticketanizer anlegen (statt Discord-Webhook)
Umgestellt von der ursprünglichen Discord-Webhook-Idee auf die offizielle Ticketanizer-Inbound-Ticket-API (api.ticketanizer.com/v1/tickets) - erzeugt echte Tickets statt nur Nachrichten, landen dadurch direkt im richtigen Ticketanizer-Bereich in Discord. - Neu: cloudflare-worker/src/lib/ticketanizer.js. panel_id pro Bewerbungs- bereich fest hinterlegt (aus dem Ticketanizer-Dashboard abgelesen): Modi=4, Kooperation=5, Scout=6, Creator=7 (dort "Manager-Bewerbung") - Alte discord.js entfernt (Ansatz verworfen) - applications.js: Aufruf entsprechend umbenannt/angepasst - README.md: Einrichtungsanleitung aktualisiert (TICKETANIZER_API_KEY statt 4x DISCORD_WEBHOOK_*) - Ticketanizer bleibt rein informativ, kein Ausfallpunkt - Bewerbung wird immer in der eigenen Datenbank gespeichert, unabhängig vom Ticket-Erfolg - Secret TICKETANIZER_API_KEY bereits live gesetzt und deployed; End-to- End mit 3 Test-Bewerbungen geprüft (HTTP Ok geloggt), Testeinträge aus der DB wieder entfernt
This commit is contained in:
+40
-36
@@ -84,61 +84,65 @@ 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. (Optional) Bewerbungen zusätzlich automatisch in Discord posten —
|
# 7. (Optional) Bewerbungen zusätzlich automatisch als Ticket bei
|
||||||
# siehe Abschnitt "Bewerbungen auf Discord (Ticketanizer)" unten.
|
# Ticketanizer anlegen (dort in Discord sichtbar) — siehe Abschnitt
|
||||||
# Ohne diesen Schritt läuft alles wie gehabt, es fehlt nur die
|
# "Bewerbungen als Ticketanizer-Tickets" unten. Ohne diesen Schritt
|
||||||
# Discord-Benachrichtigung.
|
# läuft alles wie gehabt, es fehlt nur das automatische Ticket.
|
||||||
|
|
||||||
# 8. Deployen
|
# 8. Deployen
|
||||||
npx wrangler deploy
|
npx wrangler deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bewerbungen auf Discord (Ticketanizer)
|
## Bewerbungen als Ticketanizer-Tickets
|
||||||
|
|
||||||
Jede eingehende Bewerbung wird zusätzlich zur Speicherung in der Datenbank als
|
Jede eingehende Bewerbung wird zusätzlich zur Speicherung in der eigenen
|
||||||
schön formatierte Nachricht (Embed) in den passenden Discord-Kanal gepostet —
|
Datenbank als **echtes Ticket** bei Ticketanizer angelegt — im passenden
|
||||||
pro Bewerbungsbereich ein eigener Kanal/Webhook, exakt wie auf der Website
|
Bereich (Modi/Scout/Creator/Kooperation), exakt wie auf der Website getrennt.
|
||||||
getrennt (Modi, Scout, Creator, Kooperation).
|
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
|
### Einrichtung (einmalig, ca. 2 Minuten)
|
||||||
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 Ticketanizer-Dashboard: **API & Webhooks → API-Keys → Key erstellen**
|
||||||
|
(Name z.B. "DOGFATHER UNIVERSE Homepage"). Den Key **sofort kopieren** —
|
||||||
1. Im Discord-Kanal (z.B. dem Modi-Bewerbungskanal): Kanal bearbeiten →
|
wird meist nur einmal im Klartext angezeigt.
|
||||||
**Integrationen** → **Webhooks** → **Neuer Webhook**.
|
2. Als Secret im Worker hinterlegen:
|
||||||
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
|
```bash
|
||||||
npx wrangler secret put DISCORD_WEBHOOK_MODI
|
npx wrangler secret put TICKETANIZER_API_KEY
|
||||||
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.
|
3. Danach einmal `npx wrangler deploy`.
|
||||||
|
|
||||||
4. Danach einmal `npx wrangler deploy`, damit der Worker die neuen Secrets
|
Die Zuordnung Bewerbungsbereich → Ticketanizer-Panel ist fest in
|
||||||
kennt (Secrets selbst brauchen keinen Deploy, aber schadet nicht).
|
`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
|
### Wichtig
|
||||||
|
|
||||||
- **Discord ist rein informativ, kein Ausfallpunkt.** Fehlt eine Webhook-URL
|
- **Ticketanizer ist rein informativ, kein Ausfallpunkt.** Fehlt der API-Key
|
||||||
oder ist Discord kurz nicht erreichbar, wird die Bewerbung trotzdem ganz
|
oder ist Ticketanizer kurz nicht erreichbar, wird die Bewerbung trotzdem
|
||||||
normal in der Datenbank gespeichert — nur die Zusatz-Nachricht in Discord
|
ganz normal in der eigenen Datenbank gespeichert — nur das Zusatz-Ticket
|
||||||
entfällt dann.
|
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
|
sondern ausschließlich verschlüsselt bei Cloudflare — genau wie
|
||||||
`POSTFACH_CODE` und `ENCRYPTION_KEY`.
|
`POSTFACH_CODE` und `ENCRYPTION_KEY`.
|
||||||
- Ändern/Ersetzen einer URL: den jeweiligen `wrangler secret put ...`-Befehl
|
- Ändern/Ersetzen des Keys: `wrangler secret put TICKETANIZER_API_KEY` erneut
|
||||||
erneut ausführen, überschreibt den alten Wert.
|
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
|
## Wer den Owner-Code kennen darf
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +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";
|
import { bewerbungAlsTicketAnlegen } from "../lib/ticketanizer.js";
|
||||||
|
|
||||||
const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"];
|
const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"];
|
||||||
|
|
||||||
@@ -47,12 +47,12 @@ export async function submitApplication(request, env, ctx) {
|
|||||||
.run();
|
.run();
|
||||||
|
|
||||||
// Zusätzlich (rein informativ, blockiert die Bewerbung selbst nie) als
|
// Zusätzlich (rein informativ, blockiert die Bewerbung selbst nie) als
|
||||||
// Nachricht in den passenden Discord-Kanal (Ticketanizer) weiterleiten.
|
// echtes Ticket bei Ticketanizer im passenden Bereich anlegen.
|
||||||
const discordVersand = bewerbungAnDiscordSenden(env, type, data);
|
const ticketVersand = bewerbungAlsTicketAnlegen(env, type, data);
|
||||||
if (ctx && typeof ctx.waitUntil === "function") {
|
if (ctx && typeof ctx.waitUntil === "function") {
|
||||||
ctx.waitUntil(discordVersand);
|
ctx.waitUntil(ticketVersand);
|
||||||
} else {
|
} else {
|
||||||
await discordVersand.catch(() => {});
|
await ticketVersand.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({ ok: true });
|
return json({ ok: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user