Fishbot
Start/Wiki

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:

  1. editembed las ein Farbfeld aus, das im Modal gar nicht existierte — der Aufruf brach jedes Mal ab.
  2. editembed baute ein klassisches Embed, während /embed eine Components-V2-Karte sendete. Dadurch meldete das Bearbeiten „That message has no embed". Jetzt liest fromDiscordMessage() beide Formen zurück.
  3. Die Farbe war fest auf Gold verdrahtet (const raw = '' — der Wert wurde nie gelesen). Farbe ist jetzt wählbar; das alte Gold liegt als BRADLEY_GOLD bereit.

Ü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

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

  1. 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.

  2. 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.

  3. Command anlegen unter apps/bot/src/commands/ mit feature: "<key>" und als erstem Unterbefehl setupSubcommand():

    .addSubcommand(setupSubcommand())
    // …und in execute():
    if (sub === "setup") return openModulePanel(interaction, "<key>", ctx.guildId);
    
  4. Werte lesen mit loadSettings(guildId, "<key>") — liefert { enabled, values } inklusive Vorgaben.

  5. Registrieren in apps/bot/src/core/registry.ts, dann npm 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:

  1. Modul im Plan enthalten und eingeschaltet?
  2. Rolle existiert, ist nicht managed, ist nicht @everyone?
  3. Rolle steht unter der Bot-Rolle?
  4. 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:

  1. ein Katalogeintrag (gameModuleToFeature) — damit greifen Preisseite, Dashboard und Freischaltung ohne Sonderfall;
  2. ein Einstellungsschema (gameModuleSettings) — damit funktionieren Discord-Panel und Web-Formular ohne eine Zeile Oberflächen-Code;
  3. 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 aufbereitet
  • module-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