Dieser Artikel wird automatisch aus der README des Projekts erzeugt. Änderungen bitte in der README vornehmen und npm run wiki:sync ausführen.
Technische Doku
Aufbau des Projekts, Installation und Betrieb — automatisch aus der README erzeugt.
Ein Discord-Bot, den Kunden per Klick einladen, im Browser konfigurieren und im Abo bezahlen. Ein einziger Bot bedient alle Server; freigeschaltet wird pro Server über den gebuchten Plan.
Erstes fertiges Modul: der Embed-Builder — portiert aus dem Bradley-Bot, mit Live-Vorschau, gespeicherten Vorlagen, Link-Buttons, zwei Layouts (klassisches Embed und Components-V2-Karte mit Bild oben) und der Möglichkeit, bereits gepostete Nachrichten nachträglich zu bearbeiten.
Was aus Bradley übernommen wurde
| Aus dem alten Bot | Im neuen Bot |
|---|---|
/embed → Kanalauswahl → Modal mit Bild/Titel/Text/Footer/Button |
/embed quick — identischer Ablauf, identische fünf Felder |
Components-V2-Container (Bild oben, ## Titel, -# Footer, Link-Button) |
Layout card, gerendert von toComponentsV2() |
/editembed <message_id> |
/embed edit nachricht_id:<id> |
Rechte über embed_role_ids + permissions.json |
Discord-Befehlsrechte (Nachrichten verwalten), pro Rolle und Kanal einstellbar |
Drei Fehler aus dem alten Code sind dabei behoben:
editembedlas ein Farbfeld aus, das im Modal gar nicht existierte — der Aufruf brach jedes Mal ab.editembedbaute ein klassisches Embed, während/embedeine Components-V2-Karte sendete. Dadurch meldete das Bearbeiten „That message has no embed". Jetzt liestfromDiscordMessage()beide Formen zurück.- Die Farbe war fest auf Gold verdrahtet (
const raw = ''— der Wert wurde nie gelesen). Farbe ist jetzt wählbar; das alte Gold liegt alsBRADLEY_GOLDbereit.
Überblick
| Teil | Was es ist | Technik |
|---|---|---|
apps/bot |
Der Discord-Bot | Node 20+, discord.js v14, TypeScript |
apps/web |
Homepage, Wiki, Dashboard, Bezahlung | Next.js 14 (App Router), Tailwind, NextAuth, Stripe |
packages/db |
Datenmodell und geteilte Logik | Prisma, PostgreSQL |
Warum ein geteiltes Paket? Feature-Katalog, Pläne, Limits und das Embed-Format stehen einmal
in packages/db/src und werden von Bot und Website importiert. Ein neues Modul einzutragen
verändert damit automatisch Landingpage, Preistabelle, Dashboard, Wiki und das Plan-Gating im Bot.
Verkaufsmodell
Ein Preis pro Server: 8,99 € im Monat oder 89 € im Jahr. Dafür ist jedes Modul frei — auch
jedes zukünftige. Die eine Wahrheit dafür steht in packages/db/src/plans.ts.
Vier Entscheidungen dahinter, die man an den Zahlen nicht sieht:
Drei Module sind kostenlos — Embed-Builder, Moderation und Fishbot-Angeln, dauerhaft und ohne
Vertrag. Ein Bot, den man nicht ausprobieren kann, wird nicht eingeladen; und ohne Einladungen gibt
es nichts zu verkaufen. pricing.test.ts wacht darüber, dass es bei genau diesen dreien bleibt.
Es gab hier einmal einen Baukasten — Grundpreis plus jedes Modul einzeln, gedeckelt bei 12,99 €. Er rechnete richtig und war trotzdem der falsche Weg: Er verlangte vom Besucher eine Entscheidung, bevor der überhaupt entschieden hatte, ob er uns vertraut. Wer zwischen zwei Bots schwankt, weiß nicht, ob er ein Logbuch braucht. Nebenbei brachte ein durchschnittlicher Warenkorb weniger ein als der heutige Festpreis, und jedes neue Modul musste einzeln verkauft werden, statt ein Grund zu sein zu bleiben.
Spiel- und Branchenmodule kosten 0,99 € extra und sind in keinem Vertrag enthalten. Das ist die einzige Ausnahme. Sie werden für einen Bruchteil der Server gebaut und fragen dauerhaft fremde Gameserver ab — im Preis enthalten hieße, dass jeder für jedes Spiel mitbezahlt, das ihn nichts angeht. Der Betrag ist bewusst klein: Ein Aufpreis in dieser Größe ist keine Kaufentscheidung mehr, sondern ein Haken, den man setzt.
Der Betrag geht als eine Position an Stripe, gerechnet in cartPrice(), statt als feste
Preis-ID. Mit Zusatzmodulen gäbe es sonst für jede Kombination eine eigene ID, die jemand von Hand
pflegen müsste.
Altverträge
Free, Solo, Pro und Ultimate stehen weiter in PLANS, werden aber nicht mehr verkauft
(OFFERED_PLANS). Wer einen hat, behält ihn samt Umfang — ein gelöschter Plan würde
Bestandskunden mitten im bezahlten Zeitraum auf Free zurückwerfen. Intern heißt der eine verkaufte
Plan weiter PRO, damit bestehende Datenbankeinträge gültig bleiben; nach außen ist er „Fishbot".
Wo beides zusammenläuft
In genau einer Funktion:
featureAllowed(plan, key, soloFeature, bookedModules)
Vier Wege führen zu einem Ja: Das Modul ist kostenlos, es ist einzeln gebucht, der Plan schließt es ein, oder es ist das Solo-Modul. Bot, Dashboard und Kasse müssen deshalb nie wissen, wie jemand bezahlt hat — nur, ob er darf.
Zahlarten
Abo monatlich oder jährlich über PayPal, SEPA-Lastschrift und Karte; Einmalkäufe zusätzlich mit Klarna. Auf den deutschsprachigen Markt zugeschnitten — Karte allein wäre hier zu wenig.
Installation
Voraussetzungen
- Node.js 20 oder neuer
- PostgreSQL (lokal am schnellsten per Docker)
- Eine Discord-Anwendung: https://discord.com/developers/applications
- Ein Stripe-Konto (für die Zahlung; zum Entwickeln reicht der Testmodus)
1. Abhängigkeiten
npm install
2. Datenbank
docker run -d --name botdb -e POSTGRES_PASSWORD=botpw -p 5432:5432 postgres:16
3. Konfiguration
cp .env.example .env
Dann .env ausfüllen:
| Variable | Wo du sie findest |
|---|---|
DISCORD_TOKEN |
Developer Portal → Bot → Reset Token |
DISCORD_CLIENT_ID |
Developer Portal → General Information → Application ID |
DISCORD_CLIENT_SECRET |
Developer Portal → OAuth2 → Client Secret |
DEV_GUILD_ID |
Rechtsklick auf deinen Testserver → Server-ID kopieren |
NEXTAUTH_SECRET |
openssl rand -base64 32 |
STRIPE_* |
Stripe-Dashboard → Entwickler → API-Schlüssel bzw. Produkte |
Im Developer Portal zusätzlich unter OAuth2 → Redirects eintragen:
http://localhost:3000/api/auth/callback/discord
4. Schema anlegen
npm run db:generate
npm run db:push
5. Slash-Commands registrieren
npm run deploy:commands
Mit gesetzter DEV_GUILD_ID sind sie sofort da. Ohne dauert die globale Registrierung bis zu
einer Stunde.
6. Starten
npm run dev:bot # der Bot
npm run dev:web # die Website auf http://localhost:3000
Vorschau-Modus
Zum Anschauen und Kommentieren gibt es einen Modus, der ohne alles läuft — ohne Datenbank, ohne Discord-Anmeldung, ohne Stripe-Konto:
npm run demo # Website mit Beispieldaten auf http://localhost:3000
/dashboard zeigt dann drei Beispielserver, davon einer im Pro-Plan mit eingeschalteten
Modulen, gespeicherten Embeds und einem Verlauf. Alles ist bedienbar: Module lassen sich
an- und abschalten, Einstellungen speichern, Embeds anlegen. Die Änderungen liegen im
Arbeitsspeicher und sind beim nächsten Start wieder auf Anfang.
Was in der Vorschau bewusst nicht passiert: Es wird nichts nach Discord gepostet und nichts abgerechnet. Ein Kauf stellt den Plan direkt um, damit sich ansehen lässt, wie das Dashboard danach aussieht.
Der Modus verlangt DEMO_MODE=1 und einen Entwicklungs-Build (next dev). In einem
Produktions-Build lässt er sich nicht einschalten — auch nicht versehentlich über die
Umgebungsvariable. Der ganze Umschalter steht in apps/web/src/lib/demo.ts; die Seiten
selbst wissen nichts davon, sie fragen apps/web/src/lib/dashboard-data.ts.
Projektstruktur
apps/
bot/
src/
index.ts Einstiegspunkt, Interaktions-Router
deploy-commands.ts Slash-Commands bei Discord registrieren
core/
registry.ts alle Commands und Component-Handler
subscriptions.ts Plan pro Guild ermitteln (mit Cache)
settings.ts Einstellungen lesen/schreiben (mit Cache)
moduleCommand.ts der geteilte `setup`-Unterbefehl
reply.ts einheitliche Antworten inkl. Upgrade-Hinweis
types.ts Command- und Handler-Interfaces
commands/ ein Modul pro Datei
features/
settings/
panel.ts das Panel für ALLE Module
handlers.ts alle Klicks darin
moderation/
actions.ts Rechte, Fälle, Modlog, Fallakte
embeds/
session.ts Zwischenstand eines offenen Builders
ui.ts Buttons, Modals, Selects, Vorschau
handlers.ts gesamte Button-/Modal-Logik
web/
content/wiki/ Wiki-Artikel als Markdown
scripts/sync-wiki.ts erzeugt Wiki-Seiten aus README und Feature-Katalog
src/
app/ Seiten und API-Routen
components/ Bausteine der Oberfläche
lib/ Auth, Discord-API, Stripe, Wiki, Zugriffsschutz
packages/
db/
prisma/schema.prisma Datenmodell
src/plans.ts Feature-Katalog, Pläne, Limits
src/embed.ts Embed-Format, Validierung, Umwandlung,
Components-V2-Karte, Buttons, Bradley-Schnellmodus
Das Modul-Muster
Jedes Modul funktioniert für den Kunden gleich — und wird deshalb auch gleich gebaut. Die Oberfläche schreibst du nicht: du beschreibst nur, welche Einstellungen es gibt.
packages/db/src/settings.ts ← hier steht, WAS ein Modul einstellen kann
│
├──▶ Discord-Panel (apps/bot/src/features/settings/panel.ts)
├──▶ Dashboard-Formular (apps/web/src/components/settings-form.tsx)
├──▶ Anzeige in Klartext (formatValue)
└──▶ Prüfung beim Speichern (validateField / validateSettings)
Ein Feld hat einen Typ, und der Typ bestimmt überall die Darstellung:
| Typ | Im Discord | Im Dashboard |
|---|---|---|
role / roles |
Discords Rollenauswahl | Suchbare Liste mit Rollenfarben |
channel / channels |
Discords Kanalauswahl, gefiltert nach channelKinds |
Suchbare Liste |
toggle |
Zwei Knöpfe | Schalter |
choice |
Dropdown | Karten zum Anklicken |
text / longtext |
Eingabefenster | Textfeld |
number / duration |
Eingabefenster mit Hinweis | Zahlenfeld mit Klartext-Anzeige |
Ein neues Modul in fünf Schritten
Feature eintragen in
packages/db/src/plans.ts→FEATURES. Damit erscheint es sofort auf der Landingpage, in der Preistabelle, im Dashboard, im Wiki und in/setup.Einstellungen beschreiben in
packages/db/src/settings.ts→MODULE_SETTINGS. Damit existieren Discord-Panel und Dashboard-Seite bereits vollständig — ohne eine Zeile Oberfläche.Command anlegen unter
apps/bot/src/commands/mitfeature: "<key>"und als erstem UnterbefehlsetupSubcommand():.addSubcommand(setupSubcommand()) // …und in execute(): if (sub === "setup") return openModulePanel(interaction, "<key>", ctx.guildId);Werte lesen mit
loadSettings(guildId, "<key>")— liefert{ enabled, values }inklusive Vorgaben.Registrieren in
apps/bot/src/core/registry.ts, dannnpm run deploy:commands.
Interaktive Bausteine, die über Einstellungen hinausgehen (Buttons, Modals), kommen als
ComponentHandler mit eigenem Präfix dazu — so wie eb beim Embed-Builder.
Immer verfügbare Befehle
| Befehl | Zweck | Rechte |
|---|---|---|
/setup |
Module einrichten | Server verwalten |
/hilfe |
Was kann der Bot? Übersicht und Details je Modul | keine |
/check |
Selbsttest: Rechte, Rollenposition, gelöschte Kanäle, fehlende Einstellungen | Server verwalten |
/status |
Plan, Antwortzeit, Dashboard-Link | keine |
/check ist der Support-Sparer: Er nennt zu jedem Problem gleich die Lösung. Die geprüften Rechte
stehen je Modul in botPermissions (packages/db/src/plans.ts) — ein neues Modul wird damit
automatisch mitgeprüft.
Missbrauchsschutz
apps/bot/src/core/ratelimit.ts. Grenzen im Speicher, bewusst so hoch, dass echte Nutzer sie nie
sehen:
| Bereich | Grenze |
|---|---|
| pro Person | 20 Aktionen / 30 s |
| pro Server | 60 Aktionen / 30 s |
| gesendete Nachrichten | 10 / min pro Server |
| Massenlöschen | 5 / min pro Server |
Die allgemeine Prüfung läuft vor der Plan-Prüfung, damit ein Skript nicht über Fehlermeldungen Last erzeugt.
Beim Einladen und beim Rauswurf
GuildCreate schickt der Person, die den Bot geholt hat, eine kurze Anleitung per DM (Fallback:
Systemkanal). GuildDelete schaltet alle Module ab und hält den Zeitpunkt fest; die Einstellungen
bleiben erhalten, damit beim erneuten Einladen nichts verloren ist.
Reaktionsrollen
Mitglieder holen sich ihre Rollen selbst — über Reaktionen, Knöpfe oder eine Auswahlliste.
Das Modul folgt bewusst dem Embed-Muster: Ein Panel ist ein Embed plus eine Liste
Emoji → Rolle. Deshalb liegt die Nachricht auch als EmbedPayload in der Datenbank.
Aufbau
| Datei | Aufgabe |
|---|---|
packages/db/src/rolepanel.ts |
Format, Emoji-Erkennung, Prüfung, Komponenten — geteilt mit der Website |
apps/bot/src/commands/roles.ts |
/rollen mit derselben Grammatik wie /embed |
apps/bot/src/features/reactionroles/ui.ts |
Baukasten: Vorschau, Knöpfe, Dialoge |
apps/bot/src/features/reactionroles/handlers.ts |
Baukasten (rrb) und öffentliches Panel (rr) |
apps/bot/src/features/reactionroles/apply.ts |
Die einzige Stelle, an der Rollen vergeben werden |
apps/bot/src/features/reactionroles/store.ts |
Datenbankzugriff |
Die beiden Präfixe sind getrennt, weil daran unterschiedliche Rechte hängen: Am Baukasten klebt eine Session und ein Besitzer, am öffentlichen Panel klebt nichts.
Sicherheit
applyEntry() prüft bei jedem Klick neu — nicht beim Anlegen, sondern bei jeder
Vergabe. Ein Panel, das vor drei Monaten gebaut wurde, kann heute eine Rolle enthalten, die
inzwischen Adminrechte hat:
- Modul im Plan enthalten und eingeschaltet?
- Rolle existiert, ist nicht
managed, ist nicht@everyone? - Rolle steht unter der Bot-Rolle?
- Rolle steht nicht in
blockedRoles(Einstellung des Servers)?
Fällt eine Prüfung durch, passiert nichts und die Person bekommt den Grund im Klartext. Bei Reaktionen gibt es niemanden zum Antworten — dort landet der Grund im Log.
Intents
Das Modul bringt GuildMessageReactions mit (nicht privilegiert, kein Antrag nötig) plus
Partials für Message, Channel, Reaction und User. Ohne die Partials meldet Discord
keine Reaktionen auf Nachrichten, die nicht im Cache liegen — und ein Panel steht meist seit
Wochen im Kanal. Auf MessageContent verzichtet der Bot weiterhin.
Grenzen
20 Rollen pro Panel. Das ist Discords Reaktionsgrenze, und sie gilt hier für alle drei Arten, damit sich die Art umstellen lässt, ohne dass Einträge verloren gehen.
Spiel- und Branchenmodule
Ein neues Spiel soll kein neues Programm sein. Deshalb stehen Spielmodule nicht im Code, sondern
in der Datenbank (GameModule) und werden beim Start in denselben Modulkatalog eingehängt wie alles
andere — über registerGameModules() in packages/db/src/gamemodules.ts.
Aus einer Definition entstehen dabei drei Dinge automatisch:
- ein Katalogeintrag (
gameModuleToFeature) — damit greifen Preisseite, Dashboard und Freischaltung ohne Sonderfall; - ein Einstellungsschema (
gameModuleSettings) — damit funktionieren Discord-Panel und Web-Formular ohne eine Zeile Oberflächen-Code; - Verhalten im Bot — Serverstatus, Countdown und Infopanel aus
features/games/.
Der Baukasten kann bewusst nur diese drei Bausteine. Ein Spiel, das eine eigene Statistik- Schnittstelle oder ein Handelssystem braucht, braucht weiterhin echten Code — ein Baukasten, der alles kann, ist eine Programmiersprache.
Warum derselbe Katalog und nicht ein zweiter Weg? Weil alles daran hängt: Panel, Modulseite, Preisseite, Wiki, Gating. Ein paralleler Pfad für Spielmodule hieße, das alles ein zweites Mal zu bauen — und beim nächsten Umbau eines davon zu vergessen.
Zwei Fallen, beide beim Bauen zugeschnappt und jetzt abgesichert:
Die Registrierung liegt am globalThis, nicht im Modul. Next bündelt jede Route einzeln und kann
dieselbe Datei mehrfach instanziieren; lag die Liste im Modul, hatte jede Route ihre eigene, meist
leere — Spielmodule waren mal da und mal nicht.
Client-Komponenten dürfen den Katalog nicht selbst lesen. Im Browser-Bündel ist die Registrierung leer, weil sie serverseitig passiert. Modulrechner und Buchungsformular bekommen ihre Liste deshalb als Eigenschaft von der Serverseite gereicht.
Preislich stehen sie neben dem Plan (tier: "spezial"). featureAllowed() schaltet sie
ausschließlich über bookedModules frei — kein Plan deckt sie ab, auch nicht die alten
Ultimate-Verträge. Ohne diese Ausnahme bekäme ausgerechnet das aufwendigste Modul mit den
wenigsten Abnehmern jeder geschenkt.
Angelegt werden sie unter /admin/spiele. npm run seed:games legt WARDOGS, Rust und Minecraft als
Startpunkt an.
Die Module im Überblick
Neun Module sind gebaut. Was sie technisch besonders macht — der Rest folgt dem Muster oben:
| Modul | Der interessante Teil |
|---|---|
| Eigene Befehle | Echte Slash-Befehle, zur Laufzeit per REST bei Discord angemeldet. Deshalb kein Präfix und kein MessageContent. sync.ts gleicht beim Start ab, weil deploy:commands mit DEV_GUILD_ID alle Guild-Befehle ersetzt. |
| Automatisierungen | Auslöser + bis zu drei Aktionen, mehr nicht. validateAutomation() erkennt Regeln, die sich selbst auslösen würden. Die Sendegrenze aus ratelimit.ts deckelt zusätzlich, was durchrutscht. |
| Einladungen | Vorher-Nachher-Vergleich der Einladungszähler. Bei zwei gleichzeitigen Beitritten wird bewusst nichts gezählt statt falsch. Braucht ManageGuild. |
| Gewinnspiele | draw.ts ist rein und mit injizierbarem Zufall — die Ziehung ist der einzige Teil, den man nicht "plausibel" testen darf. Der Zeitplan holt Verpasstes nach dem Neustart nach. |
| Geburtstage | Jahr optional. Die Geburtstagsrolle wird nicht per zweitem Zeitplan abgenommen, sondern bei jedem Lauf abgeglichen — robust gegen Neustarts. |
| Streams | Drei sehr unterschiedliche Anbieter in providers.ts: Twitch mit API, YouTube über den offenen RSS-Feed, Kick über den inoffiziellen Endpunkt. Jeder mit eigenem try/catch, damit einer die anderen nicht mitreißt. |
Der Taktgeber
apps/bot/src/core/scheduler.ts. Drei Module brauchen einen Hintergrundlauf; statt drei
Timern melden sie eine Aufgabe an. Beim Start läuft jede Aufgabe einmal sofort — so wird
nachgeholt, was während eines Neustarts fällig geworden wäre. Ein Fehler in einer Aufgabe
reißt die anderen nicht mit.
| Aufgabe | Abstand |
|---|---|
| Gewinnspiele beenden | 1 Minute |
| Geplante Nachrichten senden | 1 Minute |
| Streams abfragen | 5 Minuten |
| Geburtstage prüfen | 15 Minuten |
| Eigene Befehle abgleichen | 1 Stunde |
Intents
| Intent | Wofür | Privilegiert |
|---|---|---|
| Guilds | Grundlage | nein |
| GuildMessageReactions | Reaktionsrollen, Reaktions-Auslöser | nein |
| GuildMembers | Beitritte, Austritte, Rollenwechsel | ja |
| GuildInvites | Einladungen zählen | nein |
| GuildMessages | Level & Ranking (nur: dass eine Nachricht kam) | nein |
GuildMembers muss im Discord-Entwicklerportal eingeschaltet werden ("Server Members
Intent"). Unter 100 Servern reicht der Schalter.
MessageContent bleibt bewusst draußen. Das ist der Grund, warum eigene Befehle echte Slash-Befehle sind und warum es keinen Schlüsselwort-Auslöser gibt. Beides wäre bequemer gewesen und hätte die Erlaubnis gekostet, jede Nachricht auf jedem Kundenserver mitzulesen.
Fishbot-Modul
Das Minispiel zum Namen: ein Angelspiel als ganz normales Modul im bestehenden Bot — kein zweiter Prozess, keine zweite Datenbank, kein eigener Startbefehl.
Spielprinzip
Man wirft die Angel aus (/fisch angeln), wartet einen Cooldown ab und fängt einen Fisch. Jeder
Fang hat eine Seltenheit und manchmal eine Variante, bringt Punkte für die Rangliste und
lässt sich für Spielwährung verkaufen. Vom Geld kauft man bessere Ruten und Köder, die die
Chancen verbessern und die Wartezeit verkürzen. Mit steigender Fangzahl schalten sich neue
Gewässer frei, in denen es andere und seltenere Arten gibt. Was man gefangen hat, sammelt das
Fischbuch. Dazu kommen tägliche Belohnungen, tägliche Aufgaben und Saisons.
Befehle
| Befehl | Was er tut | Rechte |
|---|---|---|
/fisch angeln |
Wirft die Angel aus | keine |
/fisch profil [mitglied] |
Anglerprofil: Geld, Fänge, Ausrüstung, Punkte | keine |
/fisch buch |
Das Fischbuch mit allen gefangenen Arten | keine |
/fisch gewaesser |
Gewässer ansehen und wechseln | keine |
/fisch shop |
Ruten und Köder kaufen und ausrüsten | keine |
/fisch daily |
Tagesbelohnung abholen | keine |
/fisch aufgaben |
Die drei Aufgaben für heute | keine |
/fisch rangliste [zeitraum] |
Bestenliste (Saison oder alle Zeiten) | keine |
/fisch saison |
Neue Saison starten, Wertung zurücksetzen | Server verwalten + Pro |
/fisch setup |
Modul einrichten | Server verwalten |
Unter jedem Fang stehen drei Buttons: Nochmal angeln, Verkaufen und Fischbuch.
Voraussetzungen
Keine zusätzlichen Abhängigkeiten. Das Modul nutzt discord.js und Prisma, die das Projekt ohnehin
schon mitbringt. Der Bot braucht in den Spielkanälen Nachrichten senden und Links einbetten
— /check prüft das mit.
Installation und Aktivierung
Das Modul ist Teil des Bots und wird beim normalen Start mitgeladen. Zu tun ist nur:
npm install # falls noch nicht geschehen
npm run db:generate
npm run db:push # legt die vier neuen Tabellen an
npm run deploy:commands
npm run dev:bot
Danach auf dem Server einschalten: /setup → Fishbot-Angeln → Modul einschalten.
Oder direkt /fisch setup.
Umgebungsvariablen
Das Modul bringt eine eigene Variable mit, alle anderen sind die des Bots:
| Variable | Zweck | Beispielwert |
|---|---|---|
DISABLED_MODULES |
Module global abschalten, kommagetrennt | Beispiel: fishing |
NEXT_PUBLIC_APP_URL |
wird schon verwendet; von hier lädt Discord die Fischbilder | Beispiel: https://fishbot.example |
Ist DISABLED_MODULES=fishing gesetzt, wird /fisch bei Discord gar nicht erst registriert
und die Buttons des Moduls werden nicht beantwortet.
Datenbankmigration
Vier neue Tabellen, alle an die bestehende Guild gehängt: FishingProfile, FishBookEntry,
FishingItem, FishingSeason. Es wird keine bestehende Tabelle verändert und keine Spalte
entfernt — vorhandene Daten bleiben unangetastet.
npm run db:push
Registrierung im bestehenden Bot
Angepasst wurde genau eine zentrale Datei:
apps/bot/src/core/registry.ts
Dort kamen zwei Zeilen dazu — der Command und die Component-Handler:
import fish from "../commands/fish.js";
import { fishingComponentHandlers } from "../features/fishing/handlers.js";
export const commands: Command[] = [ping, help, check, setup, embed, mod, fish];
export const componentHandlers: ComponentHandler[] = [
...settingsComponentHandlers,
...helpComponentHandlers,
...embedComponentHandlers,
...fishingComponentHandlers,
];
Dazu ein Eintrag im Feature-Katalog (packages/db/src/plans.ts) und das Einstellungs-Schema
(packages/db/src/settings.ts). Alles andere liegt gekapselt unter
apps/bot/src/features/fishing/.
Konfiguration
Alles über /fisch setup oder das Dashboard — nichts davon steht im Code:
| Einstellung | Bedeutung | Standard |
|---|---|---|
| Wartezeit zwischen zwei Würfen | Cooldown in Sekunden | 90 |
| Fangglück | knapp · normal · grosszuegig |
normal |
| Pity nach … Fängen | garantiert danach etwas Seltenes, 0 = aus |
25 |
| Name der Währung | frei wählbar | Muscheln |
| Erlaubte Kanäle | leer = überall | leer |
| Kanal für seltene Fänge | öffentliche Meldung (Pro) | leer |
| Ab welcher Seltenheit melden | selten · episch · legendaer · mythisch |
episch |
| Saisons verwenden | Rangliste zurücksetzbar (Pro) | aus |
| Tägliche Belohnung & Aufgaben | an/aus | an |
Pro-Vorteile: Cooldown unter 60 Sekunden, Meldekanal für seltene Fänge, Saison-Verwaltung. Free-Server sind bei 60 Sekunden gedeckelt — das verhindert, dass der Bot zur Spam-Maschine wird.
Verzeichnisstruktur der Bilder
apps/web/public/fishing/
├── species/ ein Bild je Fischart (z. B. hecht.png)
└── banner/ ein Bild je Gewässer (z. B. teich.png)
Empfehlung: Arten 512×512 px PNG mit Transparenz, Banner 1200×400 px. Die Dateinamen stehen in
content.ts bei image bzw. banner. Fehlt ein Bild, zeigt Discord an der Stelle nichts an —
der Fang funktioniert trotzdem, der Bot stürzt nicht ab.
Eigene Fischarten und Gewässer hinzufügen
Alles Inhaltliche steht in einer Datei: apps/bot/src/features/fishing/data/content.ts.
Eine neue Art ist ein Eintrag plus ein Bild — keine Migration, kein Datenbankeingriff:
{
key: "stichling",
name: "Stichling",
rarity: "gewoehnlich",
waters: ["teich", "fluss"],
minCm: 3,
maxCm: 11,
basePrice: 2,
emoji: "🐟",
image: "stichling.png",
flavor: "Klein, stachelig, erstaunlich mutig.",
}
Danach apps/web/public/fishing/species/stichling.png ablegen und den Bot neu starten. Für ein
neues Gewässer genauso in WATERS, mit unlockAtCatches und einem Banner. Der Test
„Inhalte sind in sich stimmig" prüft anschließend automatisch, ob Gewässer, Größen und Bildnamen
zusammenpassen.
Tests, Typecheck und Linter
npm run test # Vitest, 38 Tests
npm run typecheck # TypeScript über alle Pakete
npm run lint # ESLint (Bot und packages/; die Web-App nutzt next lint)
npm run check # alle drei nacheinander
Die Spiellogik in engine.ts ist bewusst frei von Discord und Datenbank und nimmt ihren
Zufallsgenerator als Parameter — dadurch sind Dropchancen, Pity, Cooldown und Wertberechnung mit
festem Seed reproduzierbar prüfbar.
Fehlerbehebung
| Problem | Ursache und Lösung |
|---|---|
| Fischbilder fehlen | Datei liegt nicht unter apps/web/public/fishing/species/, oder NEXT_PUBLIC_APP_URL zeigt nicht auf eine öffentlich erreichbare Adresse. Lokal (localhost) kann Discord die Bilder grundsätzlich nicht laden. |
/fisch taucht nicht auf |
npm run deploy:commands vergessen; oder DISABLED_MODULES enthält fishing; oder das Modul ist auf dem Server nicht eingeschaltet (/setup). |
| „Das Angeln ist ausgeschaltet" | Modul ist da, aber auf diesem Server aus — /fisch setup → Modul einschalten. |
| Datenbankfehler beim Start | npm run db:generate und npm run db:push ausführen. Ohne generierten Prisma-Client fehlen die neuen Tabellen. |
| Buttons antworten nicht | Der Bot wurde neu gestartet und die Nachricht ist alt — nicht schlimm, einfach /fisch angeln neu aufrufen. |
Ausführliche Anleitung für Spieler und Admins: Wiki-Seite zum Fischmodul
(Quelle: apps/web/content/wiki/fishbot.md).
Befehlsgrammatik
Für den Kunden gilt überall dasselbe Muster:
/setup Übersicht aller Module
/setup modul:<name> direkt in ein Modul
/<modul> setup dasselbe Panel aus dem Modul heraus
/<modul> <aktion> … die eigentliche Arbeit
Wiki und Dokumentation
Das Wiki liegt unter /wiki und besteht aus Markdown-Dateien in apps/web/content/wiki/.
Zwei Artikel werden erzeugt statt geschrieben:
cd apps/web && npm run wiki:sync
technische-doku.md— dieser README-Text, für die Website aufbereitetmodule-uebersicht.md— Module, Pläne, Limits und Befehle aus dem Feature-Katalog
Beide tragen generated: true und zeigen im Wiki einen Hinweis, dass Änderungen in die Quelle
gehören. Alle anderen Artikel schreibst du frei — sie sollen die Funktionen in einfacher Sprache
erklären, nicht den Code.
Betrieb
Der Bot
Läuft als dauerhafter Prozess, z. B. mit PM2:
pm2 start "npm run start" --name fishbot --cwd apps/bot
pm2 save
Die Website
Vercel ist der kürzeste Weg (Repo verbinden, Umgebungsvariablen setzen, fertig). Danach in Stripe
einen Webhook auf https://deine-domain/api/stripe/webhook einrichten und diese Ereignisse
abonnieren:
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.payment_failed
Das erzeugte Signing-Secret gehört in STRIPE_WEBHOOK_SECRET.
Lokal Zahlungen testen
stripe listen --forward-to localhost:3000/api/stripe/webhook
Sicherheitsprinzipien
- Pläne werden nur im Stripe-Webhook gesetzt, nie durch eine Anfrage aus dem Browser.
- Jede Dashboard-Route und jede API-Route prüft über
guardGuild(), ob der eingeloggte Nutzer diesen Server tatsächlich verwalten darf — die Prüfung fragt Discord, nicht die eigene Datenbank. - Der Bot hat das Message-Content-Intent nicht aktiviert und liest keine Chatnachrichten.
- Embeds werden vor dem Senden gegen die Limits von Discord geprüft, damit die API keine unverständlichen Fehler zurückgibt.
Fahrplan
Der verbindliche Stand steht in packages/db/src/plans.ts — diese Tabelle ist nur die
Zusammenfassung. Was dort auf planned steht, wird gar nicht erst bei Discord angemeldet.
| Rubrik | Modul | Stand |
|---|---|---|
| Server Core | Embed-Builder | fertig |
| Server Core | Reaktionsrollen | fertig |
| Server Core | Eigene Befehle | fertig |
| Server Core | Automatisierungen | fertig |
| Server Core | Einladungen | fertig |
| Server Core | Welcome & Verify | fertig |
| Server Core | Geplante Nachrichten | fertig |
| Server Core | Ticket-System | geplant |
| Moderation | Moderation | fertig |
| Social | Stream-Benachrichtigungen (Twitch, YouTube, Kick) | fertig |
| Fun | Fishbot-Angeln | fertig |
| Fun | Gewinnspiele | fertig |
| Fun | Geburtstage | fertig |
| Fun | Level & Ranking | fertig |
| Gameserver | Gameserver-Module | geplant |
Lizenz
Privat und unveröffentlicht. Kein offizielles Discord-Produkt; Discord ist eine Marke von Discord Inc.
Zuletzt aktualisiert: 09. Oktober 2026