ProjekteDiscord-Bot
Discord-Bot
Ein Bot in TypeScript, der Discord-Server aus JSON-Vorlagen aufbaut, moderiert und Tickets bis zum Transkript führt. Vorlagen aus einer Datei und Entwürfe per KI laufen durch dieselbe Schemaprüfung mit zod.
Läuft auf dem eigenen Server, nicht öffentlich erreichbar

Eckdaten
Stand
- Umfang
- rund 7.000 Zeilen in 60 TypeScript-Dateien, 13 Dateien Doku
- Tests
- 43 mit node --test
- Datenbank
- 10 Migrationen
- Betrieb
- Docker-Container ohne offenen Port
Ein Server aus einer Datei
Rollen, Kategorien, Kanäle und ihre Rechte stehen in einer JSON-Vorlage. /serveraufbau wendet sie in einem von drei Modi an: ergänzen legt nur an, was fehlt, ersetzen gleicht auch Vorhandenes an die Vorlage an, leeren räumt den Server ab und baut ihn neu auf.
Vor jedem Lauf zeigt der Bot die geplanten Schritte, was gelöscht würde, steht dort mit Namen. Erst ein Knopfdruck startet den Lauf.
Jeder Lauf schreibt ein Journal. Darüber nimmt der Bot Angelegtes wieder weg und setzt geänderte Rechte zurück. Gelöschtes bringt er nicht zurück, ein Lauf mit leeren lässt sich gar nicht zurücknehmen.
Der Bot läuft als Docker-Container ohne offenen Port und unabhängig vom Spielserver, gebaut für den Discord-Server zu den FiveM-Ressourcen.
- Vorlage als Datei oder als Entwurf aus /serveraufbau erzeugen
- Prüfung mit zod, für Datei und Entwurf dieselbe
- Vorschau mit allen Schritten, Löschungen mit Namen
- Lauf als ergänzen, ersetzen oder leeren
- Journal nimmt Angelegtes und geänderte Rechte zurück, nicht nach leeren
Entwurf aus einer Beschreibung
/serveraufbau erzeugen macht aus einer Beschreibung in Freitext eine Vorlage, über Anthropic oder Gemini. Die Antwort des Modells läuft durch dieselbe Schemaprüfung wie eine Datei, einen eigenen Prüfweg für das Modell gibt es nicht.
Hält der Entwurf nicht, meldet der Bot die ersten zehn Befunde mit ihrem Pfad in der Vorlage. Antwortet Gemini mit 404, 429 oder 503, versucht es der Bot mit einem Ersatzmodell.
Das Modell entwirft nur Vorlagen. Moderation und Tickets laufen ohne KI.
Tickets
Ein Panel mit Knöpfen öffnet Tickets der Arten Support, Kauf, Beschwerde und Tebex. Jedes Ticket bekommt einen eigenen Kanal, den nur der Ersteller, das Team und wen das Team dazuholt sehen.
Beim Schließen legt der Bot ein Transkript im Archiv der jeweiligen Art ab. Danach lässt sich das Ticket mit einem bis fünf Sternen und einem Kommentar bewerten.
/ticket statistik zeigt je Teammitglied die bearbeiteten Tickets, die mittlere Bewertung und die mittlere Dauer.

Moderation
Der Werbefilter findet Einladungen auch in Schreibweisen wie „disc0rd . gg / abc“ oder mit unsichtbaren Zeichen zwischen den Buchstaben. Spam wird verwarnt, ein Raid-Verdacht landet im Mod-Log. Dazu kommt ein Wortfilter, dessen Muster der Bot vor dem Speichern gegen ReDoS prüft.
Mit der dritten Verwarnung folgt eine Stunde Stummschaltung, mit der fünften zwölf Stunden, mit der siebten ein Kick und mit der neunten ein Bann. Jede Maßnahme, automatisch oder von Hand, steht im Mod-Log.
Ausgenommen ist der Honigtopf-Kanal aus dem ersten Bild, dort schaltet der Bot sofort für sieben Tage stumm.
Im Code
Das Schema der Vorlage ist ein strictObject, unbekannte Felder fallen durch. Die Prüfung darüber meldet zwei Namen, die nach dem Normalisieren gleich sind, weil der Bot über den Namen zuordnet und nicht über den Schlüssel.
Die Stufen der Moderation vergleichen auf Gleichheit statt auf „ab“. Jede Verwarnung erhöht den Zähler um eins, so greift eine Stufe beim Erreichen und nicht bei jeder weiteren Verwarnung erneut.
Die Datenbank migriert über user_version, jede Migration in einer eigenen Transaktion. Ist die Datenbank neuer als der Build, bricht der Bot beim Start ab, statt sie mit altem Code zu beschreiben.
Auszüge aus dem echten Code, gekürzt.
Echter Code aus dem Discord-Bot
src/templates/schema.ts
export const VorlageBasis = z.strictObject({ version: z.literal(1), schluessel, name: z.string().min(1).max(100), …})export const Vorlage = VorlageBasis.superRefine((vorlage, ctx) => { … const gleich = normalisiere(name) const erster = namen.get(gleich) … ctx.addIssue({ code: 'custom', path: [...pfad, 'name'], …
src/commands/serveraufbau.ts
const geprueft = Vorlage.safeParse(roh)if (!geprueft.success) { const zeilen = geprueft.error.issues .slice(0, 10) .map(i => `\`${i.path.length > 0 ? i.path.join('.') : '(Wurzel)'}\` — ${i.message}`) await interaction.editReply({ embeds: [Embed.fehler( 'Die erzeugte Vorlage ist nicht gültig', … )], }) return}
src/moderation/escalation.ts
const stufen: Stufe[] = [ { ab: 3, aktion: 'stumm', dauerMs: 3_600_000 }, { ab: 5, aktion: 'stumm', dauerMs: 43_200_000 }, { ab: 7, aktion: 'kick' }, { ab: 9, aktion: 'bann' },]export function stufeFuer(anzahl: number): Stufe | null { return stufen.find(s => s.ab === anzahl) ?? null}
src/core/db.ts
function migriere(verbindung: DatabaseSync): void { const zeile = verbindung.prepare('PRAGMA user_version').get() as { … } const stand = zeile.user_version if (stand > migrationen.length) { throw new Error(`database is at schema version ${stand}, …`) } for (let i = stand; i < migrationen.length; i++) { verbindung.exec('BEGIN') try { verbindung.exec(migrationen[i]!) verbindung.exec(`PRAGMA user_version = ${i + 1}`) verbindung.exec('COMMIT') } catch (fehler) { verbindung.exec('ROLLBACK') …
Weitere Aufgaben
- Regeltor, zu sehen im Bild darunter: Ein Knopf unter den Regeln vergibt die Rolle für den Rest des Servers
- Begrüßung neuer Mitglieder als Embed
- Produktkanäle: /scripts anlegen legt für ein Paket aus dem Shop einen Kanal an, beim Serveraufbau entstehen sie für alle Pakete auf einmal. /scripts aktualisieren zieht Änderungen nach, beides über die Headless-API von Tebex

Technik
- Laufzeit
- Node 24, TypeScript ohne Build-Schritt, Node führt die .ts-Dateien direkt aus
- Discord
- discord.js 14
- Prüfung
- zod 4
- Daten
- node:sqlite, Volumes für die Datenbank und die erzeugten Vorlagen, Sicherung auf Zuruf
- KI
- @anthropic-ai/sdk und @google/genai, nur für Entwürfe von Vorlagen
- CI
- GitHub Actions mit Typecheck und Tests, Dependabot wöchentlich
- Betrieb
- Docker Compose auf dem eigenen Server, Dateisystem schreibgeschützt, alle Capabilities entzogen, no-new-privileges, Image per Digest gepinnt, läuft als Nutzer node
- Netz
- kein offener Port, alle Verbindungen gehen nach außen: zu Discord, zur Headless-API von Tebex und für Entwürfe zu Anthropic oder Gemini