diff --git a/.dockerignore b/.dockerignore
index 4795f0b105..e1e8aa5234 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -14,3 +14,4 @@ logs
.DS_Store
coverage
.agent-tools
+dist
diff --git a/.env.example b/.env.example
index c2eae4ad28..c76b370a14 100644
--- a/.env.example
+++ b/.env.example
@@ -23,11 +23,12 @@ CORS_ORIGIN=*
# Railway: use the private POSTGRES_URL / DATABASE_URL variable (includes SSL).
# Public proxy logs showing "invalid length of startup packet" or "SSL without ALPN"
# are usually internet scanners — not your bot.
-POSTGRES_URL=postgresql://postgres:yourpassword@localhost:5432/titanbot
+POSTGRES_URL=postgresql://postgres:yourpassword@localhost:5432/nexobot
+# Local Docker Postgres has no SSL: set POSTGRES_SSL=false (with NODE_ENV=production SSL is on by default).
POSTGRES_SSL=
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
-POSTGRES_DB=titanbot
+POSTGRES_DB=nexobot
POSTGRES_USER=postgres
POSTGRES_PASSWORD=yourpassword
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 28f9bc8b37..8236343112 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,2 +1,2 @@
# All files require review and approval from the repository owner
-* @codebymitch
+* @Robertwlt03
diff --git a/.github/workflows/migration-version-check.yml b/.github/workflows/migration-version-check.yml
index 971b9750b7..820073e2ba 100644
--- a/.github/workflows/migration-version-check.yml
+++ b/.github/workflows/migration-version-check.yml
@@ -16,24 +16,24 @@ jobs:
postgres:
image: postgres:16
env:
- POSTGRES_USER: titanbot
- POSTGRES_PASSWORD: titanbot
- POSTGRES_DB: titanbot
+ POSTGRES_USER: nexobot
+ POSTGRES_PASSWORD: nexobot
+ POSTGRES_DB: nexobot
ports:
- 5432:5432
options: >-
- --health-cmd "pg_isready -U titanbot -d titanbot"
+ --health-cmd "pg_isready -U nexobot -d nexobot"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
- POSTGRES_URL: postgresql://titanbot:titanbot@localhost:5432/titanbot
+ POSTGRES_URL: postgresql://nexobot:nexobot@localhost:5432/nexobot
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
- POSTGRES_DB: titanbot
- POSTGRES_USER: titanbot
- POSTGRES_PASSWORD: titanbot
+ POSTGRES_DB: nexobot
+ POSTGRES_USER: nexobot
+ POSTGRES_PASSWORD: nexobot
POSTGRES_SSL: "false"
SCHEMA_VERSION: "1"
SCHEMA_VERSION_LABEL: "baseline-v1"
@@ -51,6 +51,9 @@ jobs:
- name: Install dependencies
run: npm ci
+ - name: Build
+ run: npm run build
+
- name: Apply migrations
run: npm run migrate
diff --git a/.github/workflows/restore-drill.yml b/.github/workflows/restore-drill.yml
index 325045f7b3..c5a6f8777d 100644
--- a/.github/workflows/restore-drill.yml
+++ b/.github/workflows/restore-drill.yml
@@ -13,24 +13,24 @@ jobs:
postgres:
image: postgres:16
env:
- POSTGRES_USER: titanbot
- POSTGRES_PASSWORD: titanbot
- POSTGRES_DB: titanbot
+ POSTGRES_USER: nexobot
+ POSTGRES_PASSWORD: nexobot
+ POSTGRES_DB: nexobot
ports:
- 5432:5432
options: >-
- --health-cmd "pg_isready -U titanbot -d titanbot"
+ --health-cmd "pg_isready -U nexobot -d nexobot"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
- POSTGRES_URL: postgresql://titanbot:titanbot@localhost:5432/titanbot
+ POSTGRES_URL: postgresql://nexobot:nexobot@localhost:5432/nexobot
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
- POSTGRES_DB: titanbot
- POSTGRES_USER: titanbot
- POSTGRES_PASSWORD: titanbot
+ POSTGRES_DB: nexobot
+ POSTGRES_USER: nexobot
+ POSTGRES_PASSWORD: nexobot
POSTGRES_SSL: "false"
SCHEMA_VERSION: "1"
SCHEMA_VERSION_LABEL: "baseline-v1"
@@ -52,6 +52,9 @@ jobs:
- name: Install dependencies
run: npm ci
+ - name: Build
+ run: npm run build
+
- name: Apply migrations
run: npm run migrate
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 0b7e3d974b..0000000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Contributing to TitanBot
-
-Thank you for your interest in contributing to TitanBot! This guide covers local setup, project conventions, and what we look for in pull requests.
-
-## Ways to Contribute
-
-- Bug fixes and reliability improvements
-- New commands or enhancements to existing features
-- Documentation updates
-
-Before starting large features, open an issue or discuss in the [support server](https://discord.gg/8kJBYhTGW9) so we can align on scope and avoid duplicate work.
-
-## Getting Started
-
-### Prerequisites
-
-- **Node.js 20+** (Docker and CI use Node 20)
-- **PostgreSQL** (recommended for development; the bot can fall back to in-memory storage if PostgreSQL is unavailable)
-- A **Discord bot application** with the intents listed in [README.md](README.md#required-bot-intents)
-
-### Local Setup
-
-1. Fork and clone the repository.
-2. Install dependencies:
- ```bash
- npm install
- ```
-3. Copy the environment file and configure it:
- ```bash
- cp .env.example .env
- ```
- At minimum, set `DISCORD_TOKEN`, `CLIENT_ID`, and `GUILD_ID` for single-server development. See [README.md](README.md#manual-installation-steps) for full configuration options.
-4. Verify your database setup (when using PostgreSQL):
- ```bash
- npm run migrate:check
- ```
-5. Start the bot:
- ```bash
- npm start
- ```
-
-For Docker-based setup, see [README.md](README.md#docker-deployment-recommended).
-
-## Development Workflow
-
-1. **Fork the repository** and create a branch from `main`.
-2. **Make focused changes** — one logical change per pull request when possible.
-3. **Open a pull request** with a clear description of what changed and why.
-
-Use descriptive branch names, for example:
-
-- `fix/ticket-panel-refresh`
-- `feat/economy-shop-filter`
-- `docs/contributing-guide`
-
-## Database & Migrations
-
-TitanBot uses PostgreSQL as its primary store. If PostgreSQL is unreachable at startup, the bot can operate in a **degraded in-memory mode** — but that mode is not suitable for production and should not be the only way you test persistence-related changes.
-
-Useful commands:
-
-| Command | Purpose |
-| --- | --- |
-| `npm run migrate` | Apply migrations |
-| `npm run migrate:check` | Verify schema version matches expectations |
-| `npm run migrate:status` | Show current migration status |
-
-If your change modifies the database schema, update the expected schema version in `.env.example` (`SCHEMA_VERSION`, `SCHEMA_VERSION_LABEL`) and ensure `npm run migrate:check` passes locally.
-
-Test features that read or write guild data with **both** PostgreSQL and the memory fallback when feasible.
-
-## Code Guidelines
-
-- **Match existing style** — ES modules (`import`/`export`), async/await, and the conventions used in neighboring files.
-- **Handle errors gracefully** — catch failures, log with context, and send user-friendly embed replies where appropriate.
-- **Avoid breaking guild isolation** — guild-specific config and data must stay scoped per server (`guild:{guildId}:...` keys, `interaction.guildId`).
-- **Keep changes minimal** — prefer extending existing utilities and services over duplicating logic.
-- **Document user-facing behavior** — update README.md when setup steps or configuration change; mention new env vars in `.env.example`.
-
-There is no ESLint config in this repo today; consistency with surrounding code is the main bar.
-
-## Pull Request Checklist
-
-In your PR description, include:
-
-- **What** changed
-- **Why** the change is needed
-- **How to test** it manually (commands, config, or env vars to set)
-
-## Reporting Issues
-
-When reporting a bug, include:
-
-- Steps to reproduce
-- Expected vs. actual behavior
-- Relevant logs (`LOG_LEVEL=debug` can help locally)
-- Whether you use PostgreSQL or memory fallback
-- Bot version / commit hash if known
-
-## License
-
-By contributing, you agree that your contributions will be licensed under the same [MIT License](LICENSE) that covers this project.
diff --git a/Dockerfile b/Dockerfile
index 03b7bd00f1..1971275e61 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,14 @@
+FROM node:20-alpine AS build
+
+WORKDIR /usr/src/app
+
+COPY package*.json ./
+RUN npm ci
+
+COPY tsconfig.json ./
+COPY src ./src
+RUN npm run build
+
FROM node:20-alpine
WORKDIR /usr/src/app
@@ -5,10 +16,11 @@ WORKDIR /usr/src/app
ENV NODE_ENV=production
COPY package*.json ./
-RUN npm ci --omit=dev
+RUN npm ci --omit=dev && npm cache clean --force
-COPY . .
+COPY --from=build /usr/src/app/dist ./dist
+COPY lavalink ./lavalink
EXPOSE 3000
-CMD ["npm", "start"]
+CMD ["node", "dist/app.js"]
diff --git a/LICENSE b/LICENSE
index 8df7f6c941..3b2f04d781 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,6 @@
MIT License
+Copyright (c) 2026 Robertwlt03 (NexoBot)
Copyright (c) 2026 codebymitch
Permission is hereby granted, free of charge, to any person obtaining a copy
diff --git a/README.md b/README.md
index 700ac20739..5c8a5e3bf4 100644
--- a/README.md
+++ b/README.md
@@ -1,286 +1,151 @@
-# TitanBot - Ultimate Discord Bot
-
-**TitanBot** is a powerful, feature-rich Discord bot designed to enhance your server experience with comprehensive moderation tools, engaging economy systems, utility features, and much more. Built with modern Discord.js v14 and PostgreSQL for optimal performance and data persistence.
-
-[](https://discord.gg/8kJBYhTGW9)
-[](https://www.npmjs.com/package/discord.js)
-
-
-## Table of Contents
-
-- [Features Overview](#features-overview)
-- [Quick Setup](#quick-setup)
-- [Manual Installation Steps](#manual-installation-steps)
-- [Support Server](https://discord.gg/QnWNz2dKCE)
-- [Required Bot Intents](#bot-intents)
-- [Contributing](CONTRIBUTING.md)
-
-
-## Features Overview
-
-TitanBot offers a complete suite of tools for Discord server management and community engagement:
-
-
-
-
-
-### Moderation & Administration
-- **Mass Actions** - Bulk ban/kick capabilities
-- **User Notes** - Keep detailed moderation records
-- **Case Management** - View and track all mod actions
-
-### Economy System
-- **Shop & Inventory** - Buy and manage items
-- **Gambling** - Risk it for rewards
-- **Pay System** - Transfer money between users
-
-### Fun & Entertainment
-- **Random Facts** - Learn something new
-- **Wanted Poster** - Create fun wanted images
-- **Text Reversal** - Reverse any text
-
-### Advanced Ticket System
-- **Claim & Priority** - Staff ticket management
-- **Ticket Limits** - Prevent spam
-- **Transcript System** - Save ticket history
-
-### Server Stats
-- **Member Counter** - Live member count channels
-- **Voice Counters** - Track voice stats
-- **Dynamic Updates** - Real-time channel updates
-
-### Reaction Roles
-- **Role Assignment** - Self-assignable roles
-- **Emoji Selection** - Reaction-based system
-- **Multi-role Support** - Multiple role options
-
-
-
-
-### Leveling & XP System
-- **XP Tracking** - Automatic message-based XP
-- **Level Roles** - Auto-assign roles by level
-- **Custom Configuration** - Personalize leveling
-
-### Giveaways & Events
-- **Multiple Winners** - Support multi-winner giveaways
-- **Auto Picking** - Automatic winner selection
-- **Reroll System** - Pick new winners if needed
-
-### Birthday System
-- **Birthday Tracking** - Never miss a birthday
-- **Auto Announcements** - Celebrate automatically
-- **Timezone Support** - Accurate worldwide tracking
-
-### Utility Tools
-- **Report System** - Report issues to staff
-- **Todo Lists** - Personal task management
-- **First Message** - Jump to channel's first message
-
-### Welcome System
-- **Welcome Messages** - Greet new members
-- **Auto Roles** - Assign roles on join
-- **Custom Embeds** - Personalized messages
-
-### Music
-- **24/7 Mode** - Play music 24/7
-- **Interative Button System** - Manage music through buttons
-- **Supports EVERY platform** - Supports spotify, deezer, youtube, apple music
-
-
-
-
-
-
-## Quick Setup (Recommended for non-coders)
-
-### Video Tutorial
-For a detailed step-by-step setup guide, watch our comprehensive video tutorial:
-[**TitanBot Setup Tutorial**](https://www.youtube.com/@TouchDisc)
-
-## Docker Deployment (Recommended)
-
-TitanBot is fully containerized for easy deployment.
-
-1. **Clone the repository:**
- ```bash
- git clone https://github.com/codebymitch/TitanBot.git
- cd TitanBot
- ```
-
-2. **Configure environment variables:**
- ```bash
- cp .env.example .env
- ```
- Set at minimum `DISCORD_TOKEN`, `CLIENT_ID`, and `GUILD_ID`. Docker Compose also reads `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` from `.env` (defaults: `titanbot` / `password` / `titanbot`).
-
-3. **Build and start the containers:**
- ```bash
- docker compose up -d --build
- ```
-
-4. **Check status:**
- ```bash
- docker compose ps
- curl http://localhost:3000/health
- ```
-
-This starts the bot and PostgreSQL. The compose file sets `POSTGRES_SSL=false` and `AUTO_MIGRATE=true` for the bundled database. Music uses public Lavalink v4 nodes from `lavalink/nodes.json` by default.
-
-### Music
-
-Music uses [Lavalink v4](https://github.com/lavalink-devs/Lavalink) via [Riffy](https://github.com/riffy-rb/riffy), similar to [Musicify](https://github.com/codebymitch/Musicify).
-
-1. By default, the bot loads multiple public v4 SSL nodes from [`lavalink/nodes.json`](lavalink/nodes.json) (sourced from [lavalink.darrennathanael.com](https://lavalink.darrennathanael.com/SSL/Lavalink-SSL/)). Edit that file to add or remove nodes.
-2. To self-host Lavalink instead, run `docker compose --profile local-lavalink up -d` and set single-node env vars in `.env`:
- ```env
- LAVALINK_HOST=lavalink
- LAVALINK_PORT=2333
- LAVALINK_PASSWORD=youshallnotpass
- LAVALINK_SECURE=false
- ```
- Remove or rename `lavalink/nodes.json` so the bot falls back to those env vars.
-3. Override nodes inline with `LAVALINK_NODES` (JSON array) or point at another file with `LAVALINK_NODES_FILE`.
-4. Use `/play ` from a voice channel, or `/join` to connect without playing. Prefix shortcuts: `join`, `np`, `leave`, `pause`, `resume`, `skip`, `stop`, `volume <0-100>`, or `music `. Use `/nowplaying` and `/queue` for status; `/music` for loop, shuffle, seek, and other controls.
-
-### Using GitHub Container Registry
-
-The bot is automatically published to GitHub Container Registry on every push to main.
+
+
+
+
+
+
+
+
+
+
+
+**NexoBot** (Anzeigename „Nexo“) ist ein modularer Discord-Community-Bot mit Moderation, Economy, Leveling, Tickets, Musik, Giveaways, Verifizierung und vielen Werkzeugen. Geschrieben in TypeScript mit discord.js v14, PostgreSQL als Datenspeicher.
+
+> **Herkunft:** NexoBot basiert auf [TitanBot](https://github.com/codebymitch/TitanBot) von codebymitch (MIT-Lizenz) und wurde für diesen Bot überarbeitet: Rebranding, Umstellung auf TypeScript, zusammengefasste Commands und deutsche Texte.
+
+## Inhalt
+
+- [Funktionen](#funktionen)
+- [Commands](#commands)
+- [Einrichtung](#einrichtung)
+ - [Lokal](#lokal)
+ - [Docker](#docker)
+ - [Railway](#railway)
+- [Umgebungsvariablen](#umgebungsvariablen)
+- [Intents und Berechtigungen](#intents-und-berechtigungen)
+
+## Funktionen
+
+- **Moderation** – Ban, Kick, Timeout, Verwarnungen, Fälle, Massenaktionen, Kanal-Sperren, Nutzernotizen
+- **Economy** – Kontostand, Arbeit, Shop, Inventar, Glücksspiel, Überweisungen, Bestenliste
+- **Leveling** – XP für Nachrichten, Rangkarten, Level-Rollen, Bestenliste
+- **Tickets** – Panels, Claim, Priorität, Transkripte, Limits
+- **Musik** – Lavalink v4 über Riffy, Warteschlange, Buttons, 24/7-Modus
+- **Giveaways** – mehrere Gewinner, automatische Auslosung, Reroll
+- **Willkommen** – Begrüßung, Abschied, Auto-Rollen
+- **Verifizierung** – Button-Verifizierung und Auto-Verify
+- **Weitere Module** – Geburtstage, Reaktionsrollen, Join-to-Create, Server-Statistiken, Logging, Bewerbungen, Zählspiel, Todo-Listen, Suche und viele Werkzeuge
+
+Alle Funktionen lassen sich pro Server über `/configwizard` und `/commands` konfigurieren und einzeln sperren.
+
+## Commands
+
+NexoBot bündelt die Funktionen in thematische Commands mit Subcommands, zum Beispiel `/eco balance`, `/mod warn` oder `/music play`. Die vollständige Zuordnung der früheren Einzel-Commands steht in [docs/COMMAND_MIGRATION.md](docs/COMMAND_MIGRATION.md). `/help` zeigt alle Commands im Bot an.
+
+## Einrichtung
+
+Voraussetzungen: ein Bot im [Discord Developer Portal](https://discord.com/developers/applications) mit Token und Client-ID sowie den unten genannten Intents.
+
+### Lokal
+
+Benötigt Node.js 20.10 oder neuer und PostgreSQL.
+
+```bash
+git clone https://github.com/Robertwlt03/NexoBot.git
+cd NexoBot
+npm install
+cp .env.example .env # DISCORD_TOKEN, CLIENT_ID und Datenbank eintragen
+npm run build
+npm start
+```
+
+Für die Entwicklung startet `npm run dev` den Bot mit automatischem Neuladen direkt aus den TypeScript-Quellen. `npm run typecheck` prüft die Typen ohne Build. `npm test` führt die Tests aus.
+
+Eine lokale Datenbank lässt sich am einfachsten per Docker starten:
+
+```bash
+docker compose up -d db
+```
+
+Die Compose-Datenbank läuft dann auf `127.0.0.1:5432` (User/Passwort/DB aus `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`). Sie hat kein SSL: Setze `POSTGRES_SSL=false`, sonst schlägt die Verbindung mit `NODE_ENV=production` fehl und der Bot läuft im In-Memory-Modus.
+
+### Docker
```bash
-docker pull ghcr.io/codebymitch/titanbot:main
+cp .env.example .env # mindestens DISCORD_TOKEN, CLIENT_ID, GUILD_ID setzen
+docker compose up -d --build
```
-
-## Manual Installation Steps
-
-### Prerequisites
-- Node.js 20.10.0 or higher
-- PostgreSQL server (recommended) or memory storage fallback
-- Discord bot application with proper intents
-
-1. **Clone the Repository**
- ```bash
- git clone https://github.com/codebymitch/TitanBot.git
- cd TitanBot
- ```
-
-2. **Install Dependencies**
- ```bash
- npm install
- ```
-
-3. **Configure Environment Variables**
- ```bash
- cp .env.example .env
- ```
- Edit `.env` with your configuration (only the following variables require configuration, leave remaining variables as default):
- ```env
- # Discord Bot Configuration
- DISCORD_TOKEN=your_discord_bot_token_here
- CLIENT_ID=your_discord_client_id_here
- GUILD_ID=your_discord_guild_id_here
-
- # PostgreSQL Configuration (Primary Database)
- POSTGRES_URL=postgresql://postgres:yourpassword@localhost:5432/titanbot
- POSTGRES_HOST=localhost
- POSTGRES_PORT=5432
- POSTGRES_DB=titanbot
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=yourpassword
- ```
-
- Production note:
- - `NODE_ENV=production`
- - `LOG_LEVEL=warn` for a clean production console (critical issues + startup status)
- - `LOG_LEVEL=info` if you want more detailed operational logs
- - If your chosen `PORT` is already used, TitanBot automatically tries the next port(s)
-
- Environment options reference:
- - `NODE_ENV`: `development`, `production`, `test` (any non-`production` value is treated as non-production)
- - `LOG_LEVEL`: `error`, `warn`, `info`, `http`, `verbose`, `debug`, `silly`
- - Accepted aliases for `LOG_LEVEL` in this bot: `warns`, `warning`, `warnings` → `warn`
-
- Recommended production `.env` (easy mode + default mode):
- ```env
- NODE_ENV=production
- LOG_LEVEL=warn
- WEB_HOST=0.0.0.0
- PORT=3000
- PORT_RETRY_ATTEMPTS=5
- ```
- This gives clear startup/online status messages while keeping logs simple for non-technical operators.
- If port `3000` is busy, the bot tries the next available ports automatically (up to `PORT_RETRY_ATTEMPTS`).
-
-### Multiple servers
-
-Slash commands are registered **globally** on startup (via `CLIENT_ID`), so the bot works in every server it is invited to. `GUILD_ID` stays in the tutorial `.env` for setup steps but is not used for command registration.
-
-Notes:
-- Global slash commands may take up to about an hour to propagate on first deploy
-- Each server has **isolated** data: config, economy, tickets, leveling, dashboards, warnings, etc. (all keys are scoped as `guild:{guildId}:...`)
-- In the [Discord Developer Portal](https://discord.com/developers/applications), ensure your bot is not restricted to a single guild if you plan to invite it elsewhere
-- Generate an OAuth2 invite URL from the [Discord Developer Portal](https://discord.com/developers/applications) (OAuth2 → URL Generator, scopes: `bot` and `applications.commands`)
-
-4. **Setup PostgreSQL Database** (Optional but recommended)
- ```bash
- # Create database and user
- createdb titanbot
- createuser titanbot
- psql -c "ALTER USER titanbot PASSWORD 'yourpassword';"
- psql -c "GRANT ALL PRIVILEGES ON DATABASE titanbot TO titanbot;"
- ```
-
-5. **Verify Database Setup**
- ```bash
- npm run migrate:check
- ```
-
-6. **Start the Bot**
- ```bash
- npm start
- ```
-
-> **Note on database migrations:** Schema tables and legacy key migrations run
-> **automatically on startup**, so` managed hosts like **Railway** need no manual
-> migration step — just deploy/restart. To disable auto-migration set
-> `AUTO_MIGRATE=false`. You can still run a manual key migration locally with
-> `node scripts/migrate-keys.js --dry-run` (preview) or `node scripts/migrate-keys.js`.
-
-
-## Required Bot Intents
-TitanBot requires the following Discord intents:
-- **Guilds**
-- **Guild Messages**
-- **Message Content**
-- **Guild Members**
-- **Guild Message Reactions**
-- **Guild Voice States**
-- **Direct Messages**
-- **Bot**
-- **Applications.commands**
-
-### Required Permissions
-- **View Channels**
-- **Send Messages**
-- **Embed Links**
-- **Attach Files**
-- **Read Message History**
-- **Manage Messages**
-- **Manage Channels**
-- **Manage Roles**
-- **Kick Members**
-- **Manage Messages**
-- **Ban Members**
-- **Moderate Members**
-- **Connect**
-
-## License
-
-TitanBot is released under the MIT License. See [LICENSE](LICENSE) for details.
-
-## Thank You
-
-Thank you for choosing TitanBot for your Discord server! We're constantly working to improve and add new features based on community feedback.
-
-*Last updated: May 2026*
+Compose startet den Bot und PostgreSQL. Die Datenbank-Defaults (`nexobot` / `password` / `nexobot`) lassen sich über `POSTGRES_USER`, `POSTGRES_PASSWORD` und `POSTGRES_DB` in der `.env` überschreiben. Einen eigenen Lavalink-Knoten startet das Profil `local-lavalink`:
+
+```bash
+docker compose --profile local-lavalink up -d
+```
+
+Fertige Images: `ghcr.io/robertwlt03/nexobot`.
+
+### Railway
+
+1. Repository in Railway verbinden und einen PostgreSQL-Dienst hinzufügen.
+2. Im Bot-Dienst `DISCORD_TOKEN`, `CLIENT_ID` und `GUILD_ID` setzen und `POSTGRES_URL` bzw. `DATABASE_URL` auf die private Railway-Datenbank-URL verweisen lassen.
+3. Railway baut über das `Dockerfile`. Migrationen laufen beim Start automatisch (`AUTO_MIGRATE=true`).
+
+Healthchecks: `GET /health` (Prozess lebt) und `GET /ready` (Bot verbunden und Datenbank bereit).
+
+## Umgebungsvariablen
+
+Alle Variablen mit Beispielwerten stehen in [.env.example](.env.example). Die wichtigsten:
+
+| Variable | Pflicht | Beschreibung |
+| --- | --- | --- |
+| `DISCORD_TOKEN` | ja | Bot-Token aus dem Developer Portal |
+| `CLIENT_ID` | ja | Application-ID; Slash-Commands werden global registriert |
+| `GUILD_ID` | nein | Server-ID für die Einrichtung |
+| `OWNER_IDS` | nein | Kommagetrennte Discord-IDs der Bot-Besitzer |
+| `POSTGRES_URL` / `DATABASE_URL` | empfohlen | Verbindungs-URL zur Datenbank |
+| `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | alternativ | Einzelwerte statt URL |
+| `POSTGRES_SSL` | nein | SSL erzwingen (`true`/`false`) |
+| `AUTO_MIGRATE` | nein | Migrationen beim Start ausführen (Standard `true`) |
+| `PORT`, `WEB_HOST` | nein | Port und Host für `/health` und `/ready` (Standard `3000`, `0.0.0.0`) |
+| `LOG_LEVEL`, `LOG_TO_FILE` | nein | Log-Level und Datei-Logging |
+| `LAVALINK_NODES_FILE`, `LAVALINK_NODES` | nein | Lavalink-Knoten für Musik |
+| `TMDB_API_KEY` | nein | Für die Film- und Seriensuche |
+
+Ohne erreichbare Datenbank läuft der Bot in einem eingeschränkten In-Memory-Modus. Für den Produktivbetrieb ist PostgreSQL Pflicht.
+
+### Datenbank-Werkzeuge
+
+```bash
+npm run migrate # Schema-Migrationen anwenden
+npm run migrate:status # Stand anzeigen
+npm run backup:db # Backup erstellen (pg_dump)
+npm run restore:db # Backup einspielen
+```
+
+Die Skripte laufen aus `dist/scripts/`, also vorher `npm run build` ausführen.
+
+## Intents und Berechtigungen
+
+Im Developer Portal aktivieren (Privileged Gateway Intents):
+
+- **Server Members Intent**
+- **Message Content Intent**
+
+Der Bot nutzt außerdem die Intents Guilds, Guild Messages, Guild Message Reactions, Guild Voice States, Guild Moderation (Bans) und Direct Messages.
+
+Einladung mit den Scopes `bot` und `applications.commands` und diesen Berechtigungen:
+
+- Kanäle ansehen, Nachrichten senden, Links einbetten, Dateien anhängen, Nachrichtenverlauf lesen
+- Nachrichten verwalten, Kanäle verwalten, Rollen verwalten
+- Mitglieder kicken, Mitglieder bannen, Mitglieder moderieren (Timeout)
+- Verbinden und Sprechen (Musik)
+
+
+
+
+
+
+ Nexo · Ein Bot. Alle Funktionen.
+ © 2026 Robertwlt03 · Veröffentlicht unter der MIT-Lizenz
+ Basiert auf TitanBot von codebymitch (MIT) und für NexoBot überarbeitet
+
diff --git a/SECURITY.md b/SECURITY.md
deleted file mode 100644
index cc4d3a8b46..0000000000
--- a/SECURITY.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Security Policy — TitanBot
-
-We take security seriously. If you discover a vulnerability, please follow the policy below so we can triage and address it safely.
-
-## Summary / Self-hosting clarity
-TitanBot is distributed as open-source software for self-hosting. We (the maintainers) do not operate self-hosted instances and do not have access to user data or configurations on third-party deployments. Self-hosters are responsible for securing and operating their instances. This policy describes how to report vulnerabilities affecting the project code and guidance for self-hosters who discover security issues in their deployment.
-
-## Reporting a Vulnerability (preferred)
-- Preferred channel: Open a private GitHub Security Advisory for this repository (recommended).
-- If you cannot use GitHub Advisories, open a ticket: https:
-- Do NOT open a public issue with exploit details.
-
-If you found a problem in your self-hosted instance that appears to be due to misconfiguration, please contact the instance operator/host first. If you believe the issue is caused by a vulnerability in TitanBot code, follow the reporting steps above and indicate whether the report comes from a self-hosted deployment.
-
-## Response timelines (what to expect)
-- Acknowledgement: within 72 hours.
-- Triage & severity estimate: within 7 days.
-- Fix/migration plan:
- - Critical: aim to ship fix or mitigation within 7–14 days.
- - High: aim to ship within 30 days.
- - Medium/Low: addressed in a future release; communicated within 90 days.
-- Public disclosure: we will coordinate with the reporter and normally publish an advisory after a patch is released, or within 90 days if unresolved (unless the reporter requests otherwise).
-
-## Safe testing rules (researcher rules of engagement)
-- Only test services you own or have explicit permission to test.
-- Do not exfiltrate, destroy, or modify user data.
-- Do not attempt to escalate to or access Discord user tokens, DMs, or other private user content.
-- Provide minimal, safe PoC that reproduces the issue. Redact sensitive data (tokens, PII).
-- If the vulnerability requires intrusive testing, contact us first to agree on a plan.
-
-## What to include in your report
-- Component affected (e.g., database migration, command parser, OAuth).
-- Clear, minimal steps to reproduce.
-- Environment: commit SHA, release tag, Docker image tag, or version.
-- Whether the issue was observed on a self-hosted deployment (and the deployment configuration).
-- PoC (script, HTTP request, logs, screenshots) — sanitize secrets before sharing.
-- Impact (data exposure, privilege escalation, RCE, etc.).
-- Suggested mitigation (if any).
-- Contact info for follow-up.
-
-## Incident reporting from self-hosted deployments
-If you operate a self-hosted TitanBot instance and suffer a security incident:
-- Immediately rotate any exposed secrets (bot token, DB credentials, API keys).
-- Take a snapshot of logs/configuration for investigation (avoid sharing secrets).
-- If you need upstream help, file a private security advisory and include sanitized reproduction steps and the TitanBot version/commit.
-- The maintainers can only fix vulnerabilities in upstream code; we cannot rotate tokens, restore data, or remediate other hosts' deployments.
-
-## Scope
-- In-scope: this repository's code, authentication flows, webhooks provided by this project, and the intended deployment artifacts (Dockerfiles, scripts).
-- Out-of-scope: third-party services (Discord itself, hosted DB providers), and instances you do not own without owner consent.
-
-## Maintenance & Hardening advice for self-hosters (recommended defaults)
-- Use TLS for all exposed endpoints and webhooks (Let’s Encrypt is fine).
-- Do not expose Postgres to the public internet. Use an internal network, VPC, or SSH tunnels.
-- Run Postgres with authentication, strong passwords, and network restrictions.
-- Keep secrets out of the repository (use env vars, secret managers, or GitHub Secrets for CI).
-- Restrict the bot's Discord intents and permissions to the minimum required.
-- Enable automatic dependency updates (Dependabot) and security alerts.
-- Enable secret-scanning for your repo(s) and enable push protection if available.
-- Rotate bot tokens and API keys when suspected of compromise.
-- Regular backups for Postgres and test restores regularly.
-- Monitor logs and alert on suspicious activity (unexpected config changes, mass deletions).
-
-## Disclosure & Credits
-- We will credit researchers in release notes/advisories unless you request anonymity.
-- We may assign a CVE or coordinate with CERT/other bodies for high-severity issues.
-
-## Privacy & Telemetry
-- TitanBot does not phone home or collect usage data by default. (If you plan to add telemetry, it must be opt-in, documented, and transparent.)
-- Maintainers do not receive data from self-hosted instances. If you choose to enable any telemetry, document what is collected and how to opt out.
-
-## Contact
-- Preferred: GitHub Security Advisory for this repository
-- Ticket: https:
-
-Thank you for helping keep TitanBot safe.
diff --git a/assets/banner-commands.svg b/assets/banner-commands.svg
new file mode 100644
index 0000000000..bca4eecf5b
--- /dev/null
+++ b/assets/banner-commands.svg
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ COMMANDS
+ 99 → 36 · ALTE NAMEN BLEIBEN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/banner-footer.svg b/assets/banner-footer.svg
new file mode 100644
index 0000000000..72d7c00f45
--- /dev/null
+++ b/assets/banner-footer.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/banner.svg b/assets/banner.svg
new file mode 100644
index 0000000000..cdc7b64679
--- /dev/null
+++ b/assets/banner.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NEXO
+ EIN BOT. ALLE FUNKTIONEN.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/commands.svg b/assets/icons/commands.svg
new file mode 100644
index 0000000000..2338d10729
--- /dev/null
+++ b/assets/icons/commands.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/contents.svg b/assets/icons/contents.svg
new file mode 100644
index 0000000000..ee476ad1ff
--- /dev/null
+++ b/assets/icons/contents.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/database.svg b/assets/icons/database.svg
new file mode 100644
index 0000000000..eefcc536a0
--- /dev/null
+++ b/assets/icons/database.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/docker.svg b/assets/icons/docker.svg
new file mode 100644
index 0000000000..c8a879a433
--- /dev/null
+++ b/assets/icons/docker.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/env.svg b/assets/icons/env.svg
new file mode 100644
index 0000000000..8c46444bdc
--- /dev/null
+++ b/assets/icons/env.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/features.svg b/assets/icons/features.svg
new file mode 100644
index 0000000000..07ae836a91
--- /dev/null
+++ b/assets/icons/features.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/local.svg b/assets/icons/local.svg
new file mode 100644
index 0000000000..6bfeb55615
--- /dev/null
+++ b/assets/icons/local.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/mapping.svg b/assets/icons/mapping.svg
new file mode 100644
index 0000000000..52dbe652cd
--- /dev/null
+++ b/assets/icons/mapping.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/permissions.svg b/assets/icons/permissions.svg
new file mode 100644
index 0000000000..e538f7f3ef
--- /dev/null
+++ b/assets/icons/permissions.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/railway.svg b/assets/icons/railway.svg
new file mode 100644
index 0000000000..cbfadb447b
--- /dev/null
+++ b/assets/icons/railway.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/setup.svg b/assets/icons/setup.svg
new file mode 100644
index 0000000000..66c6a5af4d
--- /dev/null
+++ b/assets/icons/setup.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/unchanged-check.svg b/assets/icons/unchanged-check.svg
new file mode 100644
index 0000000000..b04041308b
--- /dev/null
+++ b/assets/icons/unchanged-check.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/icons/unchanged-lock.svg b/assets/icons/unchanged-lock.svg
new file mode 100644
index 0000000000..8e609d571c
--- /dev/null
+++ b/assets/icons/unchanged-lock.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 9de8fec548..f6dcfbb7cd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,7 @@
services:
bot:
build: .
- container_name: titanbot
+ container_name: nexobot
restart: unless-stopped
env_file:
- .env
@@ -10,12 +10,12 @@ services:
- DISCORD_TOKEN=${DISCORD_TOKEN}
- CLIENT_ID=${CLIENT_ID}
- GUILD_ID=${GUILD_ID}
- - DATABASE_URL=postgres://${POSTGRES_USER:-titanbot}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-titanbot}
- - POSTGRES_URL=postgres://${POSTGRES_USER:-titanbot}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-titanbot}
+ - DATABASE_URL=postgres://${POSTGRES_USER:-nexobot}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-nexobot}
+ - POSTGRES_URL=postgres://${POSTGRES_USER:-nexobot}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-nexobot}
- POSTGRES_HOST=db
- POSTGRES_PORT=5432
- - POSTGRES_DB=${POSTGRES_DB:-titanbot}
- - POSTGRES_USER=${POSTGRES_USER:-titanbot}
+ - POSTGRES_DB=${POSTGRES_DB:-nexobot}
+ - POSTGRES_USER=${POSTGRES_USER:-nexobot}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
- POSTGRES_SSL=false
- AUTO_MIGRATE=true
@@ -35,41 +35,44 @@ services:
retries: 3
start_period: 40s
networks:
- - titan-network
+ - nexo-network
lavalink:
profiles:
- local-lavalink
image: ghcr.io/lavalink-devs/lavalink:4
- container_name: titanbot-lavalink
+ container_name: nexobot-lavalink
restart: unless-stopped
environment:
- LAVALINK_PASSWORD=${LAVALINK_PASSWORD:-youshallnotpass}
volumes:
- ./lavalink/application.yml:/opt/Lavalink/application.yml
networks:
- - titan-network
+ - nexo-network
db:
image: postgres:15-alpine
- container_name: titanbot-db
+ container_name: nexobot-db
restart: unless-stopped
environment:
- - POSTGRES_USER=${POSTGRES_USER:-titanbot}
+ - POSTGRES_USER=${POSTGRES_USER:-nexobot}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
- - POSTGRES_DB=${POSTGRES_DB:-titanbot}
+ - POSTGRES_DB=${POSTGRES_DB:-nexobot}
volumes:
- postgres_data:/var/lib/postgresql/data
+ ports:
+ # Local access for `npm run dev` / `npm start` on the host (bound to localhost only).
+ - "127.0.0.1:${DB_HOST_PORT:-5432}:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
networks:
- - titan-network
+ - nexo-network
networks:
- titan-network:
+ nexo-network:
driver: bridge
volumes:
diff --git a/docs/COMMAND_MIGRATION.md b/docs/COMMAND_MIGRATION.md
new file mode 100644
index 0000000000..f04ea377bd
--- /dev/null
+++ b/docs/COMMAND_MIGRATION.md
@@ -0,0 +1,114 @@
+
+
+
+
+NexoBot fasst die früheren Einzel-Commands thematisch zusammen. Aus 99 Top-Level-Commands werden 36. Command-Namen innerhalb der Gruppen, Optionen und Buttons bleiben gleich.
+
+## Was gleich bleibt
+
+- **Gesperrte Commands:** Sperren aus `/commands` mit alten Namen gelten weiter. Ein gesperrtes `/balance` sperrt `/eco balance`, ein gesperrtes `/usernotes` sperrt `/msg notes`. Gesperrte Kategorien gelten auch für Commands, die in eine andere Kategorie gewandert sind (z. B. `/weather` aus Utility).
+- **Prefix-Commands:** Alte Namen und Aliase funktionieren weiter: `!bal` → `/eco balance`, `!unban` → `/ban remove`, `!usernotes view @User` → `/msg notes view @User`.
+- **Cooldowns:** Cooldowns und Missbrauchsschutz zählen weiter pro altem Command.
+- **`/commands disable balance`** akzeptiert alte Namen und sperrt den neuen Subcommand.
+
+Bei Namen, die es alt und neu gibt (`/ban`, `/kick`, `/rank`, `/welcome`), behält ein alter Eintrag seine alte Bedeutung: Ein gesperrtes `/ban` sperrt nur `/ban user`, ein gesperrtes `/welcome` nur `/welcome message`. Wer heute den ganzen neuen Command sperrt, sperrt alle Subcommands.
+
+## Unverändert
+
+`/help`, `/commands`, `/configwizard`, `/todo`, `/report`, `/wipedata`, `/apply`, `/app-admin`, `/count`, `/ticket`, `/cases`, `/verify`, `/birthday`, `/jointocreate`, `/logging`, `/reactroles`, `/search`, `/serverstats`
+
+Bestehende Subcommands bleiben außerdem direkt erhalten bei `/economy` (dashboard), `/music` (pause, resume, skip, …), `/level` (setup, dashboard) und `/verification` (setup, remove, dashboard).
+
+## Zuordnung
+
+`…` bedeutet: Die alten Subcommands bleiben unter der neuen Gruppe erhalten, z. B. `/usernotes add` → `/msg notes add`.
+
+| Alt | Neu | Alte Kategorie |
+| --- | --- | --- |
+| `/ban` | `/ban user` | Moderation |
+| `/unban` | `/ban remove` | Moderation |
+| `/massban` | `/ban mass` | Moderation |
+| `/ping` | `/bot ping` | Core |
+| `/stats` | `/bot stats` | Core |
+| `/support` | `/bot support` | Core |
+| `/uptime` | `/bot uptime` | Core |
+| `/lock` | `/channel lock` | Moderation |
+| `/unlock` | `/channel unlock` | Moderation |
+| `/balance` | `/eco balance` | Economy |
+| `/beg` | `/eco beg` | Economy |
+| `/buy` | `/eco buy` | Economy |
+| `/crime` | `/eco crime` | Economy |
+| `/daily` | `/eco daily` | Economy |
+| `/deposit` | `/eco deposit` | Economy |
+| `/eleaderboard` | `/eco leaderboard` | Economy |
+| `/fish` | `/eco fish` | Economy |
+| `/gamble` | `/eco gamble` | Economy |
+| `/inventory` | `/eco inventory` | Economy |
+| `/mine` | `/eco mine` | Economy |
+| `/pay` | `/eco pay` | Economy |
+| `/rob` | `/eco rob` | Economy |
+| `/shop` | `/eco shop` | Economy |
+| `/slut` | `/eco slut` | Economy |
+| `/withdraw` | `/eco withdraw` | Economy |
+| `/work` | `/eco work` | Economy |
+| `/shop-config …` | `/economy shop …` | Economy |
+| `/fight` | `/fun fight` | Fun |
+| `/flip` | `/fun flip` | Fun |
+| `/roll` | `/fun roll` | Fun |
+| `/gcreate` | `/giveaway create` | Giveaway |
+| `/gdelete` | `/giveaway delete` | Giveaway |
+| `/gend` | `/giveaway end` | Giveaway |
+| `/greroll` | `/giveaway reroll` | Giveaway |
+| `/avatar` | `/info avatar` | Utility |
+| `/serverinfo` | `/info server` | Utility |
+| `/userinfo` | `/info user` | Utility |
+| `/kick` | `/kick user` | Moderation |
+| `/masskick` | `/kick mass` | Moderation |
+| `/leveladd` | `/level add` | Leveling |
+| `/levelremove` | `/level remove` | Leveling |
+| `/levelset` | `/level set` | Leveling |
+| `/timeout` | `/mod timeout` | Moderation |
+| `/untimeout` | `/mod untimeout` | Moderation |
+| `/warn` | `/mod warn` | Moderation |
+| `/warnings` | `/mod warnings` | Moderation |
+| `/dm` | `/mod dm` | Moderation |
+| `/purge` | `/msg purge` | Moderation |
+| `/say` | `/msg say` | Moderation |
+| `/embedbuilder` | `/msg embed` | Tools |
+| `/usernotes …` | `/msg notes …` | Moderation |
+| `/join` | `/music join` | Music |
+| `/play` | `/music play` | Music |
+| `/queue` | `/music queue` | Music |
+| `/nowplaying` | `/music nowplaying` | Music |
+| `/rank` | `/rank view` | Leveling |
+| `/leaderboard` | `/rank leaderboard` | Leveling |
+| `/claim` | `/tickets claim` | Ticket |
+| `/close` | `/tickets close` | Ticket |
+| `/priority` | `/tickets priority` | Ticket |
+| `/baseconvert` | `/tools baseconvert` | Tools |
+| `/calculate` | `/tools calculate` | Tools |
+| `/countdown` | `/tools countdown` | Tools |
+| `/generatepassword` | `/tools generatepassword` | Tools |
+| `/hexcolor` | `/tools hexcolor` | Tools |
+| `/poll` | `/tools poll` | Tools |
+| `/randomuser` | `/tools randomuser` | Tools |
+| `/shorten` | `/tools shorten` | Tools |
+| `/time` | `/tools time` | Tools |
+| `/unixtime` | `/tools unixtime` | Tools |
+| `/weather` | `/tools weather` | Utility |
+| `/firstmsg` | `/tools firstmsg` | Utility |
+| `/autoverify …` | `/verification autoverify …` | Verification |
+| `/autorole …` | `/welcome autorole …` | Welcome |
+| `/goodbye …` | `/welcome goodbye …` | Welcome |
+| `/greet …` | `/welcome greet …` | Welcome |
+| `/welcome …` | `/welcome message …` | Welcome |
+
+
+
+
+
+
+ Nexo · Ein Bot. Alle Funktionen.
+ © 2026 Robertwlt03 · Veröffentlicht unter der MIT-Lizenz
+ Basiert auf TitanBot von codebymitch (MIT) und für NexoBot überarbeitet
+
diff --git a/index.js b/index.js
deleted file mode 100644
index fdb508fd35..0000000000
--- a/index.js
+++ /dev/null
@@ -1 +0,0 @@
-import './src/app.js';
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index b11b7cdec6..db1e8776d1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,13 @@
{
- "name": "titanbot",
- "version": "2.1.0",
+ "name": "nexobot",
+ "version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "titanbot",
- "version": "2.1.0",
+ "name": "nexobot",
+ "version": "1.0.0",
+ "license": "MIT",
"dependencies": {
"@discordjs/rest": "^2.6.1",
"axios": "^1.15.2",
@@ -20,6 +21,13 @@
"winston-daily-rotate-file": "^5.0.0",
"zod": "^3.25.76"
},
+ "devDependencies": {
+ "@types/express": "^5.0.6",
+ "@types/node": "^20.19.43",
+ "@types/pg": "^8.23.1",
+ "tsx": "^4.23.15",
+ "typescript": "^5.9.3"
+ },
"engines": {
"node": ">=20.10.0"
}
@@ -262,6 +270,448 @@
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@sapphire/async-queue": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
@@ -301,12 +751,113 @@
"text-hex": "1.0.x"
}
},
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz",
+ "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
- "version": "25.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
- "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
+ "version": "20.19.43",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
+ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.23.1",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz",
+ "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
}
},
"node_modules/@types/triple-beam": {
@@ -725,6 +1276,48 @@
"node": ">= 0.4"
}
},
+ "node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -892,6 +1485,21 @@
"node": ">= 0.8"
}
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -1838,6 +2446,25 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
+ "node_modules/tsx": {
+ "version": "4.23.15",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
+ "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
"node_modules/type-is": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
@@ -1867,6 +2494,20 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/undici": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
@@ -1876,9 +2517,10 @@
}
},
"node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
@@ -2195,6 +2837,188 @@
"ws": "^8.17.0"
}
},
+ "@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "dev": true,
+ "optional": true
+ },
"@sapphire/async-queue": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
@@ -2223,12 +3047,102 @@
"text-hex": "1.0.x"
}
},
+ "@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "requires": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "dev": true,
+ "requires": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "@types/express-serve-static-core": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz",
+ "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true
+ },
"@types/node": {
- "version": "25.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
- "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
+ "version": "20.19.43",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
+ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"requires": {
- "undici-types": ">=7.24.0 <7.24.7"
+ "undici-types": "~6.21.0"
+ }
+ },
+ "@types/pg": {
+ "version": "8.23.1",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz",
+ "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "dev": true
+ },
+ "@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true
+ },
+ "@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "dev": true,
+ "requires": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
}
},
"@types/triple-beam": {
@@ -2526,6 +3440,40 @@
"hasown": "^2.0.2"
}
},
+ "esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "dev": true,
+ "requires": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -2649,6 +3597,13 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="
},
+ "fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "optional": true
+ },
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3316,6 +4271,16 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
+ "tsx": {
+ "version": "4.23.15",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
+ "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
+ "dev": true,
+ "requires": {
+ "esbuild": "~0.28.0",
+ "fsevents": "~2.3.3"
+ }
+ },
"type-is": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
@@ -3333,15 +4298,21 @@
}
}
},
+ "typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true
+ },
"undici": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg=="
},
"undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"unpipe": {
"version": "1.0.0",
diff --git a/package.json b/package.json
index 2430aec17b..f8cb77364c 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,33 @@
{
- "name": "titanbot",
- "version": "2.1.0",
- "description": "Modular Ultimate Community Bot by Touchpoint Support",
- "main": "src/app.js",
+ "name": "nexobot",
+ "version": "1.0.0",
+ "description": "Nexo - Ein Bot. Alle Funktionen. Modular Discord community bot.",
+ "author": "Robertwlt03",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Robertwlt03/NexoBot.git"
+ },
+ "bugs": {
+ "url": "https://github.com/Robertwlt03/NexoBot/issues"
+ },
+ "homepage": "https://github.com/Robertwlt03/NexoBot#readme",
+ "main": "dist/app.js",
"type": "module",
"scripts": {
- "start": "node src/app.js",
- "migrate": "node scripts/migrate.js apply",
- "migrate:check": "node scripts/migrate.js check",
- "migrate:status": "node scripts/migrate.js status",
- "migrate:keys": "node scripts/migrate-keys.js",
- "migrate:keys:dry": "node scripts/migrate-keys.js --dry-run",
- "backup:db": "node scripts/backup.js",
- "restore:db": "node scripts/restore.js",
- "backup:drill": "node scripts/restore-drill.js"
+ "build": "tsc -p tsconfig.json",
+ "start": "node dist/app.js",
+ "dev": "tsx watch src/app.ts",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "migrate": "node dist/scripts/migrate.js apply",
+ "migrate:check": "node dist/scripts/migrate.js check",
+ "migrate:status": "node dist/scripts/migrate.js status",
+ "migrate:keys": "node dist/scripts/migrate-keys.js",
+ "migrate:keys:dry": "node dist/scripts/migrate-keys.js --dry-run",
+ "backup:db": "node dist/scripts/backup.js",
+ "restore:db": "node dist/scripts/restore.js",
+ "backup:drill": "node dist/scripts/restore-drill.js",
+ "test": "tsx --test tests/*.test.ts"
},
"dependencies": {
"@discordjs/rest": "^2.6.1",
@@ -23,10 +37,10 @@
"express": "^5.1.0",
"node-cron": "^4.2.1",
"pg": "^8.11.3",
+ "riffy": "1.0.12",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0",
- "zod": "^3.25.76",
- "riffy": "1.0.12"
+ "zod": "^3.25.76"
},
"engines": {
"node": ">=20.10.0"
@@ -34,5 +48,12 @@
"overrides": {
"form-data": "^4.0.6",
"undici": "^6.27.0"
+ },
+ "devDependencies": {
+ "@types/express": "^5.0.6",
+ "@types/node": "^20.19.43",
+ "@types/pg": "^8.23.1",
+ "tsx": "^4.23.15",
+ "typescript": "^5.9.3"
}
}
diff --git a/roadmap.txt b/roadmap.txt
index 890cfbe3df..d09516f002 100644
--- a/roadmap.txt
+++ b/roadmap.txt
@@ -1,54 +1,53 @@
-TitanBot Roadmap / Structure Overview
+NexoBot Roadmap / Structure Overview
Goal
- Keep this as a practical map of the codebase.
- Show main folders and important files only (not every file).
Root
-- index.js -> process entry point
-- package.json -> scripts, dependencies, project metadata
-- README.md -> setup and usage docs
-- SECURITY.md -> security policy
-- scripts/ -> utility/maintenance scripts (backup, restore, migrate)
+- package.json -> scripts (build, start, dev, typecheck, test, migrate, backup), dependencies
+- tsconfig.json -> TypeScript config (strict, NodeNext, src -> dist)
+- Dockerfile -> multi-stage build (tsc) + slim runtime (node dist/app.js)
+- README.md -> setup and usage docs (German)
+- docs/COMMAND_MIGRATION.md -> old -> new command mapping (99 -> 36)
+- tests/ -> node:test checks (npm test, run via tsx)
+- lavalink/ -> Lavalink node list and local Lavalink config
- logs/ -> runtime log output
-- src/ -> main bot application code
-- tests/ -> unit tests (run via npm test)
+- dist/ -> compiled output (git-ignored)
src/
-- app.js -> creates client, initializes systems, starts web server
-
-- commands/ -> slash command categories by feature
- - Core/ -> help, ping, stats, uptime, invite
- - Community/ -> application management + dashboards
- - Logging/ -> audit logging + event management
- - modules/logging_dashboard.js
- - modules/logging_setchannel.js
- - Ticket/ -> support ticket system + dashboard
- - ServerStats/ -> server stats channel system
- - Economy/ -> economy system
- - Moderation/ -> moderation/admin commands
- - Verification/ -> member verification flow
- - JoinToCreate/ -> dynamic voice channel creation
- - Reaction_roles/ -> automatic role assignment
- - Birthday/, Fun/, Giveaway/, Leveling/, Search/, Tools/, Utility/, Welcome/
-
-- config/ -> centralized configuration modules
-- events/ -> Discord event listeners
-- handlers/ -> interaction routing and component handlers
- - commandLoader.js -> loads slash commands at startup
- - events.js -> Discord event listener registration
- - interactions.js -> button/select/modal router
- - ticketButtons.js, reactionRoles.js, etc.
-
-- interactions/ -> button, modal, and select menu handlers
+- app.ts -> NexoBot client, startup, web server (/health, /ready), cron jobs
+- types/index.ts -> shared types (Command, BotEvent, handlers, Client augmentation, GuildConfig)
+
+- commands/ -> slash commands by category folder
+ - /.ts -> top-level command (router or standalone)
+ - /modules/ -> subcommand implementations (old top-level commands live here)
+ - Economy/ (eco, economy), Music/ (music), Giveaway/ (giveaway), Leveling/ (level, rank),
+ Tools/ (tools), Utility/ (info, todo, report, wipedata), Core/ (bot, help, commands, configwizard),
+ Fun/ (fun, count), Welcome/ (welcome), Verification/ (verification, verify), Ticket/ (tickets, ticket),
+ Moderation/ (ban, kick, mod, channel, msg, cases), Community/, Birthday/, JoinToCreate/,
+ Logging/, Reaction_roles/, Search/, ServerStats/
+
+- config/ -> centralized configuration
+ - bot.ts -> single source of truth for theming/colors and core bot config
+ - env.ts -> zod schema for environment variables
+ - commands/legacyCommandMap.ts -> old command names -> new paths (access, prefix, help, docs)
+ - commands/ -> aliases, categories, prefix restrictions
+ - database/ -> Postgres config and schema version
+
+- events/ -> Discord event listeners (interactionCreate, messageCreate, ...)
+- handlers/ -> component handlers and loaders
+ - loaders/commandLoader.ts -> loads and registers slash commands
+ - loaders/events.ts -> event registration
+ - loaders/interactions.ts -> button/select/modal registry
+- interactions/ -> button, modal and select menu handler modules
- services/ -> business logic + data operations
- utils/ -> shared helpers
- - database.js -> facade re-exporting database modules
- - database/wrapper.js -> DatabaseWrapper, get/set helpers
- - database/keys.js -> Redis-style key builders
- - database/tickets.js -> ticket persistence + guild ticket stats
- - panelStatus.js -> shared panel message detection
- - errorHandler.js, logger.js, embeds.js, etc.
+ - commandRouter.ts -> builds consolidated commands from old command modules
+ - database.ts -> facade re-exporting database modules
+ - database/wrapper.ts -> DatabaseWrapper, typed key-value API
+ - errorHandler.ts, logger.ts, embeds.ts, interactionHelper.ts, ...
+- scripts/ -> maintenance scripts (backup, restore, migrate), run from dist/scripts
Database
- PostgreSQL (primary) with in-memory fallback for development
@@ -56,4 +55,4 @@ Database
Notes
- Modular by feature area: commands -> handlers -> services -> utils
-- bot.js is the single source of truth for embed theming/colors and core bot config
+- User-facing texts are German; code, comments and logs are English
diff --git a/src/app.js b/src/app.ts
similarity index 79%
rename from src/app.js
rename to src/app.ts
index 693cd228ea..4ff9b190d7 100644
--- a/src/app.js
+++ b/src/app.ts
@@ -1,11 +1,14 @@
-import 'dotenv/config';
+import 'dotenv/config';
import { Client, Collection, GatewayIntentBits } from 'discord.js';
+import type { Request, Response, NextFunction } from 'express';
import { REST } from '@discordjs/rest';
import express from 'express';
import cron from 'node-cron';
import config from './config/application.js';
import { initializeDatabase } from './utils/database.js';
+import { db } from './utils/database/wrapper.js';
+import { validateBotEnv } from './config/env.js';
import { getGuildConfig } from './services/config/guildConfig.js';
import { getServerCounters, saveServerCounters, updateCounter } from './services/serverstatsService.js';
import { logger, startupLog, shutdownLog } from './utils/logger.js';
@@ -13,12 +16,33 @@ import { checkBirthdays } from './services/birthdayService.js';
import { checkGiveaways } from './services/giveawayService.js';
import { loadCommands, registerCommands as registerSlashCommands } from './handlers/loaders/commandLoader.js';
import { runSafeTask, handleTaskError, ErrorCodes } from './utils/errorHandler.js';
+import type { ErrorLike } from './utils/errorHandler.js';
import { initializeMusic } from './services/music/riffySetup.js';
import { shutdownMusic } from './services/music/playerHandler.js';
-import pkg from '../package.json' with { type: 'json' };
+import { readFileSync } from 'node:fs';
import { EXPECTED_SCHEMA_VERSION, EXPECTED_SCHEMA_LABEL } from './config/database/schemaVersion.js';
+import type { ServerCounter } from './services/serverstatsService.js';
+import type { NexoClient } from './types/index.js';
+
+/** Shape read by /health and /ready, including their fallbacks when no status is available. */
+interface DbStatusView {
+ isDegraded: boolean | 'unknown';
+ connectionType?: string;
+ degradedReason?: string | null;
+}
+
+type HandlerLoader = (client: NexoClient) => unknown;
+
+// package.json sits outside rootDir; read it at runtime (works from src/ and dist/).
+const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string };
+
+class NexoBot extends Client {
+ // Handlers/services are typed for a logged-in client (Client); they only touch
+ // ready-only state after login, so this narrow cast mirrors the JS behaviour.
+ private get ready(): NexoClient {
+ return this as NexoClient;
+ }
-class TitanBot extends Client {
constructor() {
super({
intents: [
@@ -39,18 +63,19 @@ class TitanBot extends Client {
this.config = config;
this.commands = new Collection();
- this.events = new Collection();
this.buttons = new Collection();
this.selectMenus = new Collection();
this.modals = new Collection();
this.cooldowns = new Collection();
- this.db = null;
- this.rest = new REST({ version: '10' }).setToken(config.bot.token);
+ this.db = db;
+ // Token presence is enforced by validateBotEnv() in start(); setToken only stores it.
+ this.rest = new REST({ version: '10' }).setToken(config.bot.token!);
}
async start() {
try {
- startupLog('Starting TitanBot...');
+ validateBotEnv();
+ startupLog('Starting NexoBot...');
await new Promise(resolve => setTimeout(resolve, 1000));
startupLog('Initializing database...');
@@ -77,7 +102,7 @@ class TitanBot extends Client {
this.startWebServer();
startupLog('Loading commands...');
- await loadCommands(this);
+ await loadCommands(this.ready);
startupLog(`Commands loaded: ${this.commands.size}`);
startupLog('Loading handlers...');
@@ -116,8 +141,8 @@ class TitanBot extends Client {
const host = process.env.WEB_HOST || '0.0.0.0';
const corsOrigin = this.config.api?.cors?.origin || '*';
- app.use((req, res, next) => {
- const allowedOrigins = Array.isArray(corsOrigin) ? corsOrigin : [corsOrigin];
+ app.use((req: Request, res: Response, next: NextFunction) => {
+ const allowedOrigins: (string | undefined)[] = Array.isArray(corsOrigin) ? corsOrigin : [corsOrigin];
const origin = req.headers.origin;
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) {
@@ -132,11 +157,11 @@ class TitanBot extends Client {
next();
});
- const requestCounts = new Map();
+ const requestCounts = new Map();
const windowMs = this.config.api?.rateLimit?.windowMs || 60000;
const maxRequests = this.config.api?.rateLimit?.max || 100;
- app.use((req, res, next) => {
+ app.use((req: Request, res: Response, next: NextFunction) => {
const ip = req.ip;
const now = Date.now();
const windowStart = now - windowMs;
@@ -145,7 +170,7 @@ class TitanBot extends Client {
requestCounts.set(ip, []);
}
- const times = requestCounts.get(ip).filter(t => t > windowStart);
+ const times = requestCounts.get(ip)!.filter(t => t > windowStart);
if (times.length >= maxRequests) {
return res.status(429).json({ error: 'Too many requests' });
@@ -156,8 +181,8 @@ class TitanBot extends Client {
next();
});
- app.get('/health', (req, res) => {
- const dbStatus = this.db?.getStatus?.() || { isDegraded: 'unknown' };
+ app.get('/health', (req: Request, res: Response) => {
+ const dbStatus: DbStatusView = this.db?.getStatus?.() || { isDegraded: 'unknown' };
const status = {
status: 'healthy',
timestamp: new Date().toISOString(),
@@ -171,8 +196,8 @@ class TitanBot extends Client {
res.status(200).json(status);
});
- app.get('/ready', (req, res) => {
- const dbStatus = this.db?.getStatus?.() || { isDegraded: true, connectionType: 'none' };
+ app.get('/ready', (req: Request, res: Response) => {
+ const dbStatus: DbStatusView = this.db?.getStatus?.() || { isDegraded: true, connectionType: 'none' };
const isReady = this.isReady() && !dbStatus.isDegraded;
const metrics = {
@@ -202,15 +227,15 @@ class TitanBot extends Client {
});
});
- app.get('/', (req, res) => {
+ app.get('/', (req: Request, res: Response) => {
res.status(200).json({
- message: 'TitanBot System Online',
+ message: 'NexoBot System Online',
version: pkg.version,
timestamp: new Date().toISOString()
});
});
- const startServer = (port, attempt = 0) => {
+ const startServer = (port: number, attempt = 0) => {
let hasStartedListening = false;
const server = app.listen(port, host, () => {
hasStartedListening = true;
@@ -220,7 +245,7 @@ class TitanBot extends Client {
startupLog(`Ready endpoint: http://${host}:${port}/ready`);
});
- server.on('error', (error) => {
+ server.on('error', (error: NodeJS.ErrnoException) => {
const errorCode = error?.code || 'UNKNOWN_ERROR';
const errorMessage = error?.message || 'Unknown server error';
@@ -248,8 +273,8 @@ class TitanBot extends Client {
}
setupCronJobs() {
- cron.schedule('0 6 * * *', runSafeTask('birthday_check', () => checkBirthdays(this)));
- cron.schedule('* * * * *', runSafeTask('giveaway_check', () => checkGiveaways(this)));
+ cron.schedule('0 6 * * *', runSafeTask('birthday_check', () => checkBirthdays(this.ready)));
+ cron.schedule('* * * * *', runSafeTask('giveaway_check', () => checkGiveaways(this.ready)));
cron.schedule('*/15 * * * *', runSafeTask('counter_update', () => this.updateAllCounters()));
}
@@ -261,16 +286,16 @@ class TitanBot extends Client {
for (const [guildId, guild] of this.guilds.cache) {
try {
- const counters = await getServerCounters(this, guildId);
- const validCounters = [];
- const orphanedCounters = [];
+ const counters = await getServerCounters(this.ready, guildId);
+ const validCounters: ServerCounter[] = [];
+ const orphanedCounters: ServerCounter[] = [];
for (const counter of counters) {
if (counter && counter.type && counter.channelId && counter.enabled !== false) {
const channel = guild.channels.cache.get(counter.channelId);
if (channel) {
validCounters.push(counter);
- await updateCounter(this, guild, counter);
+ await updateCounter(this.ready, guild, counter);
} else {
orphanedCounters.push(counter);
logger.info(`Removing orphaned counter ${counter.id} (type: ${counter.type}, deleted channel: ${counter.channelId}) from guild ${guildId}`);
@@ -281,7 +306,7 @@ class TitanBot extends Client {
// Save cleaned counters if any were orphaned
// Save cleaned counters if any were orphaned
if (orphanedCounters.length > 0) {
- await saveServerCounters(this, guildId, validCounters);
+ await saveServerCounters(this.ready, guildId, validCounters);
logger.info(`Cleaned up ${orphanedCounters.length} orphaned counter(s) from guild ${guildId} during scheduled update`);
}
} catch (error) {
@@ -300,23 +325,24 @@ class TitanBot extends Client {
for (const handler of handlers) {
try {
startupLog(`Loading handler: ${handler.path}`);
- const module = await import(`./handlers/loaders/${handler.path}.js`);
+ const module = (await import(`./handlers/loaders/${handler.path}.js`)) as Record;
const loaderFn = handler.type.startsWith('named:')
? module[handler.type.split(':')[1]]
: module.default;
if (typeof loaderFn === 'function') {
- await loaderFn(this);
+ await loaderFn(this.ready);
startupLog(`✅ Loaded ${handler.path}`);
} else {
throw new Error(`Invalid loader export from ${handler.path}`);
}
} catch (error) {
+ const err = error as ErrorLike;
if (handler.required) {
- logger.error(`❌ Failed to load required handler ${handler.path}:`, error.message);
+ logger.error(`❌ Failed to load required handler ${handler.path}:`, err.message);
throw error;
- } else if (error.code !== 'MODULE_NOT_FOUND') {
- logger.warn(`⚠️ Failed to load optional handler ${handler.path}:`, error.message);
+ } else if (err.code !== 'MODULE_NOT_FOUND') {
+ logger.warn(`⚠️ Failed to load optional handler ${handler.path}:`, err.message);
}
}
}
@@ -324,7 +350,7 @@ class TitanBot extends Client {
async registerCommands() {
try {
- await registerSlashCommands(this, { clientId: this.config.bot.clientId });
+ await registerSlashCommands(this.ready, { clientId: this.config.bot.clientId });
} catch (error) {
logger.error('Error registering commands:', error);
}
@@ -348,7 +374,7 @@ class TitanBot extends Client {
if (this.webServer) {
logger.info('Closing web server...');
- await new Promise((resolve) => this.webServer.close(resolve));
+ await new Promise((resolve) => this.webServer!.close(resolve));
logger.info('✅ Web server closed');
}
@@ -362,7 +388,7 @@ class TitanBot extends Client {
logger.info('✅ Database connection closed');
}
} catch (error) {
- logger.warn('Error closing database pool:', error.message);
+ logger.warn('Error closing database pool:', (error as ErrorLike).message);
}
}
@@ -373,7 +399,7 @@ class TitanBot extends Client {
logger.info('✅ Discord client destroyed');
} catch (error) {
- logger.warn('Discord client destroy warning (non-critical):', error.message);
+ logger.warn('Discord client destroy warning (non-critical):', (error as ErrorLike).message);
}
}
@@ -388,7 +414,7 @@ class TitanBot extends Client {
}
try {
- const bot = new TitanBot();
+ const bot = new NexoBot();
const setupShutdown = () => {
process.on('SIGTERM', () => bot.shutdown('SIGTERM'));
@@ -400,13 +426,14 @@ try {
bot.shutdown('UNCAUGHT_EXCEPTION');
});
- process.on('unhandledRejection', (reason) => {
- const code = reason?.code;
+ process.on('unhandledRejection', (reason: unknown) => {
+ const rejection = reason as ErrorLike | null | undefined;
+ const code = rejection?.code;
if (code === 10062 || code === 40060 || code === 50027) {
- logger.warn('Recoverable Discord interaction rejection:', reason?.message || reason);
+ logger.warn('Recoverable Discord interaction rejection:', rejection?.message || reason);
return;
}
- if (reason?.message?.includes('Queue is empty')) {
+ if (rejection?.message?.includes('Queue is empty')) {
return;
}
@@ -428,4 +455,4 @@ try {
process.exit(1);
}
-export default TitanBot;
\ No newline at end of file
+export default NexoBot;
\ No newline at end of file
diff --git a/src/commands/Birthday/birthday.js b/src/commands/Birthday/birthday.ts
similarity index 76%
rename from src/commands/Birthday/birthday.js
rename to src/commands/Birthday/birthday.ts
index c1f8672d00..36bbf28355 100644
--- a/src/commands/Birthday/birthday.js
+++ b/src/commands/Birthday/birthday.ts
@@ -10,18 +10,19 @@ import nextBirthdays from './modules/next_birthdays.js';
import birthdaySetchannel from './modules/birthday_setchannel.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('birthday')
- .setDescription('Birthday system commands')
+ .setDescription('Commands für das Geburtstagssystem')
.addSubcommand(subcommand =>
subcommand
.setName('set')
- .setDescription('Set your birthday')
+ .setDescription('Lege deinen Geburtstag fest')
.addIntegerOption(option =>
option
.setName('month')
- .setDescription('Birth month (1-12)')
+ .setDescription('Geburtsmonat (1-12)')
.setRequired(true)
.setMinValue(1)
.setMaxValue(12)
@@ -29,7 +30,7 @@ export default {
.addIntegerOption(option =>
option
.setName('day')
- .setDescription('Birth day (1-31)')
+ .setDescription('Tag des Geburtstags (1-31)')
.setRequired(true)
.setMinValue(1)
.setMaxValue(31)
@@ -38,37 +39,37 @@ export default {
.addSubcommand(subcommand =>
subcommand
.setName('info')
- .setDescription('View birthday information')
+ .setDescription('Geburtstagsinfos anzeigen')
.addUserOption(option =>
option
.setName('user')
- .setDescription('User to check birthday for')
+ .setDescription('Nutzer, dessen Geburtstag du sehen willst')
.setRequired(false)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('list')
- .setDescription('List all birthdays in the server')
+ .setDescription('Alle Geburtstage auf dem Server anzeigen')
)
.addSubcommand(subcommand =>
subcommand
.setName('remove')
- .setDescription('Remove your birthday')
+ .setDescription('Deinen Geburtstag entfernen')
)
.addSubcommand(subcommand =>
subcommand
.setName('next')
- .setDescription('Show upcoming birthdays')
+ .setDescription('Anstehende Geburtstage anzeigen')
)
.addSubcommand(subcommand =>
subcommand
.setName('setchannel')
- .setDescription('Set or disable the channel for birthday announcements. (Manage Server required)')
+ .setDescription('Kanal für Geburtstagsankündigungen setzen oder deaktivieren. („Server verwalten“ nötig)')
.addChannelOption(option =>
option
.setName('channel')
- .setDescription('The text channel for announcements. Leave empty to disable.')
+ .setDescription('Der Textkanal für Ankündigungen. Leer lassen zum Deaktivieren.')
.addChannelTypes(ChannelType.GuildText)
.setRequired(false)
)
@@ -91,7 +92,7 @@ export default {
case 'setchannel':
return await birthdaySetchannel.execute(interaction, config, client);
default:
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unbekannter Subcommand' });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Birthday/modules/birthday_info.js b/src/commands/Birthday/modules/birthday_info.ts
similarity index 69%
rename from src/commands/Birthday/modules/birthday_info.js
rename to src/commands/Birthday/modules/birthday_info.ts
index 07b8c20020..15d14a3655 100644
--- a/src/commands/Birthday/modules/birthday_info.js
+++ b/src/commands/Birthday/modules/birthday_info.ts
@@ -3,23 +3,24 @@ import { getUserBirthday } from '../../../services/birthdayService.js';
import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
await InteractionHelper.safeDefer(interaction);
const targetUser = interaction.options.getUser("user") || interaction.user;
const userId = targetUser.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const birthdayData = await getUserBirthday(client, guildId, userId);
if (!birthdayData) {
const embed = new EmbedBuilder()
.setColor(0xFF0000)
- .setTitle('No Birthday Found')
+ .setTitle('Kein Geburtstag gefunden')
.setDescription(targetUser.id === interaction.user.id
- ? "You haven't set your birthday yet. Use `/birthday set` to add it!"
- : `${targetUser.username} hasn't set their birthday yet.`);
+ ? "Du hast deinen Geburtstag noch nicht eingetragen. Nutze `/birthday set`, um ihn hinzuzufügen!"
+ : `${targetUser.username} hat noch keinen Geburtstag eingetragen.`);
return await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
});
@@ -27,8 +28,8 @@ export default {
const embed = new EmbedBuilder()
.setColor(0x00FF00)
- .setTitle('Birthday Information')
- .setDescription(`**Date:** ${birthdayData.monthName} ${birthdayData.day}\n**User:** ${targetUser.toString()}`);
+ .setTitle('Geburtstagsinfo')
+ .setDescription(`**Datum:** ${birthdayData.day}. ${birthdayData.monthName}\n**Nutzer:** ${targetUser.toString()}`);
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
@@ -41,4 +42,4 @@ export default {
commandName: 'birthday_info'
});
}
-};
\ No newline at end of file
+} satisfies SubcommandModule;
\ No newline at end of file
diff --git a/src/commands/Birthday/modules/birthday_list.js b/src/commands/Birthday/modules/birthday_list.ts
similarity index 70%
rename from src/commands/Birthday/modules/birthday_list.js
rename to src/commands/Birthday/modules/birthday_list.ts
index ce7ee064da..7974e15051 100644
--- a/src/commands/Birthday/modules/birthday_list.js
+++ b/src/commands/Birthday/modules/birthday_list.ts
@@ -4,26 +4,27 @@ import { deleteBirthday } from '../../../utils/database.js';
import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
await InteractionHelper.safeDefer(interaction);
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const sortedBirthdays = await getAllBirthdays(client, guildId);
if (sortedBirthdays.length === 0) {
const embed = new EmbedBuilder()
.setColor(0xFF0000)
- .setTitle('No Birthdays')
- .setDescription('No birthdays have been set in this server yet.');
+ .setTitle('Keine Geburtstage')
+ .setDescription('Auf diesem Server wurden noch keine Geburtstage eingetragen.');
return await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
});
}
const userIds = sortedBirthdays.map(b => b.userId);
- const fetchedMembers = await interaction.guild.members.fetch({ user: userIds }).catch(() => null);
+ const fetchedMembers = await interaction.guild!.members.fetch({ user: userIds }).catch(() => null);
let birthdayList = '';
let displayIndex = 0;
@@ -35,7 +36,7 @@ export default {
continue;
}
displayIndex++;
- birthdayList += `${displayIndex}. <@${birthday.userId}> - ${birthday.monthName} ${birthday.day}\n`;
+ birthdayList += `${displayIndex}. <@${birthday.userId}> - ${birthday.day}. ${birthday.monthName}\n`;
}
if (fetchedMembers && staleUserIds.length > 0) {
@@ -47,19 +48,19 @@ export default {
if (displayIndex === 0) {
const embed = new EmbedBuilder()
.setColor(0xFF0000)
- .setTitle('No Birthdays')
- .setDescription('No birthdays have been set by current server members.');
+ .setTitle('Keine Geburtstage')
+ .setDescription('Keines der aktuellen Mitglieder hat einen Geburtstag eingetragen.');
return await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
});
}
- birthdayList = `**${displayIndex} birthday${displayIndex !== 1 ? 's' : ''} in ${interaction.guild.name}**\n\n` + birthdayList;
+ birthdayList = `**${displayIndex} Geburtstag${displayIndex !== 1 ? 'e' : ''} auf ${interaction.guild!.name}**\n\n` + birthdayList;
const embed = new EmbedBuilder()
.setColor(0x00FF00)
- .setTitle('Server Birthdays')
- .setDescription(`${birthdayList}\n\nTotal: ${displayIndex} birthday${displayIndex !== 1 ? 's' : ''}`);
+ .setTitle('Geburtstage auf dem Server')
+ .setDescription(`${birthdayList}\n\nGesamt: ${displayIndex} Geburtstag${displayIndex !== 1 ? 'e' : ''}`);
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
@@ -73,4 +74,4 @@ export default {
commandName: 'birthday_list'
});
}
-};
\ No newline at end of file
+} satisfies SubcommandModule;
\ No newline at end of file
diff --git a/src/commands/Birthday/modules/birthday_remove.js b/src/commands/Birthday/modules/birthday_remove.ts
similarity index 67%
rename from src/commands/Birthday/modules/birthday_remove.js
rename to src/commands/Birthday/modules/birthday_remove.ts
index 2494c42768..8e8112b747 100644
--- a/src/commands/Birthday/modules/birthday_remove.js
+++ b/src/commands/Birthday/modules/birthday_remove.ts
@@ -2,20 +2,21 @@ import { EmbedBuilder } from 'discord.js';
import { deleteBirthday } from '../../../services/birthdayService.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
await InteractionHelper.safeDefer(interaction);
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const result = await deleteBirthday(client, guildId, userId);
if (result.status === 'not_found') {
const embed = new EmbedBuilder()
.setColor(0xFF0000)
- .setTitle('No Birthday Found')
- .setDescription('You don\'t have a birthday set to remove.');
+ .setTitle('Kein Geburtstag gefunden')
+ .setDescription('Du hast keinen Geburtstag eingetragen, den du entfernen könntest.');
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
});
@@ -24,10 +25,10 @@ export default {
const embed = new EmbedBuilder()
.setColor(0x00FF00)
- .setTitle('Birthday Removed')
- .setDescription('Your birthday has been successfully removed from the server.');
+ .setTitle('Geburtstag entfernt')
+ .setDescription('Dein Geburtstag wurde vom Server entfernt.');
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
});
}
-};
\ No newline at end of file
+} satisfies SubcommandModule;
\ No newline at end of file
diff --git a/src/commands/Birthday/modules/birthday_set.js b/src/commands/Birthday/modules/birthday_set.ts
similarity index 59%
rename from src/commands/Birthday/modules/birthday_set.js
rename to src/commands/Birthday/modules/birthday_set.ts
index bf968b065d..d5272100d6 100644
--- a/src/commands/Birthday/modules/birthday_set.js
+++ b/src/commands/Birthday/modules/birthday_set.ts
@@ -2,24 +2,25 @@ import { EmbedBuilder } from 'discord.js';
import { setBirthday } from '../../../services/birthdayService.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
await InteractionHelper.safeDefer(interaction);
- const month = interaction.options.getInteger("month");
- const day = interaction.options.getInteger("day");
+ const month = interaction.options.getInteger("month")!;
+ const day = interaction.options.getInteger("day")!;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const result = await setBirthday(client, guildId, userId, month, day);
const embed = new EmbedBuilder()
.setColor(0x00FF00)
- .setTitle('Birthday Set!')
- .setDescription(`Your birthday has been set to **${result.data.monthName} ${result.data.day}**!`);
+ .setTitle('Geburtstag gespeichert!')
+ .setDescription(`Dein Geburtstag ist jetzt der **${result.data.day}. ${result.data.monthName}**!`);
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
});
}
-};
\ No newline at end of file
+} satisfies SubcommandModule;
\ No newline at end of file
diff --git a/src/commands/Birthday/modules/birthday_setchannel.js b/src/commands/Birthday/modules/birthday_setchannel.ts
similarity index 64%
rename from src/commands/Birthday/modules/birthday_setchannel.js
rename to src/commands/Birthday/modules/birthday_setchannel.ts
index 09083bfa9f..b676e90e09 100644
--- a/src/commands/Birthday/modules/birthday_setchannel.js
+++ b/src/commands/Birthday/modules/birthday_setchannel.ts
@@ -1,15 +1,16 @@
-import { PermissionsBitField, EmbedBuilder, MessageFlags } from 'discord.js';
+import { PermissionsBitField, EmbedBuilder, MessageFlags, type GuildMember } from 'discord.js';
import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { logger } from '../../../utils/logger.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
- if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
+ if (!(interaction.member as GuildMember).permissions.has(PermissionsBitField.Flags.ManageGuild)) {
const embed = new EmbedBuilder()
.setColor(0xFF0000)
- .setTitle('Permission Denied')
- .setDescription('You need **Manage Server** permissions to configure the birthday channel.');
+ .setTitle('Keine Berechtigung')
+ .setDescription('Du brauchst die Berechtigung **Server verwalten**, um den Geburtstagskanal einzurichten.');
return InteractionHelper.safeReply(interaction, {
embeds: [embed],
flags: MessageFlags.Ephemeral,
@@ -18,7 +19,7 @@ export default {
try {
const channel = interaction.options.getChannel('channel');
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const guildConfig = await getGuildConfig(client, guildId);
if (channel) {
@@ -26,8 +27,8 @@ export default {
await setGuildConfig(client, guildId, guildConfig);
const embed = new EmbedBuilder()
.setColor(0x00FF00)
- .setTitle('Birthday Announcements Enabled')
- .setDescription(`Birthday announcements will now be posted in ${channel}.`);
+ .setTitle('Geburtstagsankündigungen aktiviert')
+ .setDescription(`Geburtstagsankündigungen werden ab jetzt in ${channel} gepostet.`);
return InteractionHelper.safeReply(interaction, {
embeds: [embed],
flags: MessageFlags.Ephemeral,
@@ -37,8 +38,8 @@ export default {
await setGuildConfig(client, guildId, guildConfig);
const embed = new EmbedBuilder()
.setColor(0xFFFF00)
- .setTitle('Birthday Announcements Disabled')
- .setDescription('No channel provided — birthday announcements have been disabled.');
+ .setTitle('Geburtstagsankündigungen deaktiviert')
+ .setDescription('Kein Kanal angegeben – Geburtstagsankündigungen wurden deaktiviert.');
return InteractionHelper.safeReply(interaction, {
embeds: [embed],
flags: MessageFlags.Ephemeral,
@@ -48,12 +49,12 @@ export default {
logger.error('birthday_setchannel error:', error);
const embed = new EmbedBuilder()
.setColor(0xFF0000)
- .setTitle('⚠️ Configuration Error')
- .setDescription('Could not save the birthday channel configuration.');
+ .setTitle('⚠️ Konfigurationsfehler')
+ .setDescription('Die Einstellung für den Geburtstagskanal konnte nicht gespeichert werden.');
return InteractionHelper.safeReply(interaction, {
embeds: [embed],
flags: MessageFlags.Ephemeral,
});
}
},
-};
\ No newline at end of file
+} satisfies SubcommandModule;
\ No newline at end of file
diff --git a/src/commands/Birthday/modules/next_birthdays.js b/src/commands/Birthday/modules/next_birthdays.ts
similarity index 58%
rename from src/commands/Birthday/modules/next_birthdays.js
rename to src/commands/Birthday/modules/next_birthdays.ts
index 0695347c16..5dc424c676 100644
--- a/src/commands/Birthday/modules/next_birthdays.js
+++ b/src/commands/Birthday/modules/next_birthdays.ts
@@ -4,17 +4,18 @@ import { deleteBirthday } from '../../../utils/database.js';
import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
await InteractionHelper.safeDefer(interaction);
- const next5 = await getUpcomingBirthdays(client, interaction.guildId, 5);
+ const next5 = await getUpcomingBirthdays(client, interaction.guildId!, 5);
if (next5.length === 0) {
const embed = new EmbedBuilder()
.setColor(0xFF0000)
- .setTitle('No Birthdays Found')
- .setDescription('No birthdays have been set up in this server yet. Use `/birthday set` to add birthdays!');
+ .setTitle('Keine Geburtstage gefunden')
+ .setDescription('Auf diesem Server sind noch keine Geburtstage eingetragen. Nutze `/birthday set`, um deinen hinzuzufügen!');
return await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
});
@@ -22,37 +23,37 @@ export default {
let displayIndex = 0;
for (const birthday of next5) {
- const member = await interaction.guild.members.fetch(birthday.userId).catch(() => null);
+ const member = await interaction.guild!.members.fetch(birthday.userId).catch(() => null);
if (!member) {
- deleteBirthday(client, interaction.guildId, birthday.userId).catch(() => null);
+ deleteBirthday(client, interaction.guildId!, birthday.userId).catch(() => null);
continue;
}
displayIndex++;
let timeUntil = '';
if (birthday.daysUntil === 0) {
- timeUntil = '🎉 **Today!**';
+ timeUntil = '🎉 **Heute!**';
} else if (birthday.daysUntil === 1) {
- timeUntil = '📅 **Tomorrow!**';
+ timeUntil = '📅 **Morgen!**';
} else {
- timeUntil = `In ${birthday.daysUntil} day${birthday.daysUntil > 1 ? 's' : ''}`;
+ timeUntil = `In ${birthday.daysUntil} Tag${birthday.daysUntil > 1 ? 'en' : ''}`;
}
}
if (displayIndex === 0) {
const embed = new EmbedBuilder()
.setColor(0xFF0000)
- .setTitle('No Upcoming Birthdays')
- .setDescription('No upcoming birthdays found for current server members.');
+ .setTitle('Keine anstehenden Geburtstage')
+ .setDescription('Für die aktuellen Mitglieder stehen keine Geburtstage an.');
return await InteractionHelper.safeEditReply(interaction, {
embeds: [embed]
});
}
- let birthdayList = `🎂 **Next 5 Upcoming Birthdays**\n\nHere are the next 5 birthdays in ${interaction.guild.name}:\n\n`;
+ let birthdayList = `🎂 **Die nächsten 5 Geburtstage**\n\nDas sind die nächsten 5 Geburtstage auf ${interaction.guild!.name}:\n\n`;
displayIndex = 0;
for (const birthday of next5) {
- const member = await interaction.guild.members.fetch(birthday.userId).catch(() => null);
+ const member = await interaction.guild!.members.fetch(birthday.userId).catch(() => null);
if (!member) {
continue;
}
@@ -60,21 +61,21 @@ export default {
let timeUntil = '';
if (birthday.daysUntil === 0) {
- timeUntil = '🎉 **Today!**';
+ timeUntil = '🎉 **Heute!**';
} else if (birthday.daysUntil === 1) {
- timeUntil = '📅 **Tomorrow!**';
+ timeUntil = '📅 **Morgen!**';
} else {
- timeUntil = `In ${birthday.daysUntil} day${birthday.daysUntil > 1 ? 's' : ''}`;
+ timeUntil = `In ${birthday.daysUntil} Tag${birthday.daysUntil > 1 ? 'en' : ''}`;
}
- birthdayList += `${displayIndex}. **${member.displayName}**\n<@${birthday.userId}>\n📅 **Date:** ${birthday.monthName} ${birthday.day}\n⏰ **Time:** ${timeUntil}\n\n`;
+ birthdayList += `${displayIndex}. **${member.displayName}**\n<@${birthday.userId}>\n📅 **Datum:** ${birthday.day}. ${birthday.monthName}\n⏰ **Wann:** ${timeUntil}\n\n`;
}
- birthdayList += `Use /birthday set to add your birthday!`;
+ birthdayList += `Nutze /birthday set, um deinen Geburtstag einzutragen!`;
const embed = new EmbedBuilder()
.setColor(0x00FF00)
- .setTitle('Next 5 Upcoming Birthdays')
+ .setTitle('Die nächsten 5 Geburtstage')
.setDescription(birthdayList);
await InteractionHelper.safeEditReply(interaction, {
@@ -88,4 +89,4 @@ export default {
commandName: 'next_birthdays'
});
}
-};
\ No newline at end of file
+} satisfies SubcommandModule;
\ No newline at end of file
diff --git a/src/commands/Community/app-admin.js b/src/commands/Community/app-admin.ts
similarity index 68%
rename from src/commands/Community/app-admin.js
rename to src/commands/Community/app-admin.ts
index 6ffc02b3f8..fefd661dc0 100644
--- a/src/commands/Community/app-admin.js
+++ b/src/commands/Community/app-admin.ts
@@ -1,4 +1,6 @@
import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ComponentType, LabelBuilder, RoleSelectMenuBuilder } from 'discord.js';
+import type { ChatInputCommandInteraction, GuildMember, TextChannel } from 'discord.js';
+import type { Command } from '../../types/index.js';
import { createEmbed, successEmbed } from '../../utils/embeds.js';
import { getColor, getApplicationStatusColor } from '../../config/bot.js';
import { logger } from '../../utils/logger.js';
@@ -16,16 +18,17 @@ import {
saveApplicationRoleSettings,
deleteApplication
} from '../../utils/database.js';
+import type { Application, ApplicationFilters } from '../../utils/database.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import appDashboard from './modules/app_dashboard.js';
-function getApplicationStatusPresentation(statusValue) {
+function getApplicationStatusPresentation(statusValue: unknown) {
const normalized = typeof statusValue === 'string' ? statusValue.trim().toLowerCase() : 'unknown';
const statusLabel =
- normalized === 'pending' ? 'In Progress' :
- normalized === 'approved' ? 'Accepted' :
- normalized === 'denied' ? 'Denied' :
- 'Unknown';
+ normalized === 'pending' ? 'In Bearbeitung' :
+ normalized === 'approved' ? 'Angenommen' :
+ normalized === 'denied' ? 'Abgelehnt' :
+ 'Unbekannt';
const statusEmoji =
normalized === 'pending' ? '🟡' :
normalized === 'approved' ? '🟢' :
@@ -38,49 +41,49 @@ function getApplicationStatusPresentation(statusValue) {
export default {
data: new SlashCommandBuilder()
.setName("app-admin")
- .setDescription("Manage staff applications")
+ .setDescription("Team-Bewerbungen verwalten")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand((subcommand) =>
subcommand
.setName("setup")
- .setDescription("Set up a new application")
+ .setDescription("Eine neue Bewerbung einrichten")
)
.addSubcommand((subcommand) =>
subcommand
.setName("review")
- .setDescription("Approve or deny an application")
+ .setDescription("Eine Bewerbung annehmen oder ablehnen")
.addStringOption((option) =>
option
.setName("id")
- .setDescription("The application ID")
+ .setDescription("Die Bewerbungs-ID")
.setRequired(true),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName("list")
- .setDescription("List all applications")
+ .setDescription("Alle Bewerbungen auflisten")
.addStringOption((option) =>
option
.setName("status")
- .setDescription("Filter by status")
+ .setDescription("Nach Status filtern")
.addChoices(
- { name: "Pending", value: "pending" },
- { name: "Approved", value: "approved" },
- { name: "Denied", value: "denied" },
+ { name: "Offen", value: "pending" },
+ { name: "Angenommen", value: "approved" },
+ { name: "Abgelehnt", value: "denied" },
),
)
.addStringOption((option) =>
- option.setName("role").setDescription("Filter by role ID"),
+ option.setName("role").setDescription("Nach Rollen-ID filtern"),
)
.addUserOption((option) =>
- option.setName("user").setDescription("Filter by user"),
+ option.setName("user").setDescription("Nach Nutzer filtern"),
)
.addNumberOption((option) =>
option
.setName("limit")
.setDescription(
- "Maximum number of applications to show (default: 10)",
+ "Maximale Anzahl angezeigter Bewerbungen (Standard: 10)",
)
.setMinValue(1)
.setMaxValue(25),
@@ -89,11 +92,11 @@ export default {
.addSubcommand((subcommand) =>
subcommand
.setName("dashboard")
- .setDescription("Open the applications configuration dashboard")
+ .setDescription("Öffnet das Konfigurations-Dashboard für Bewerbungen")
.addStringOption((option) =>
option
.setName("application")
- .setDescription("Select an application to configure")
+ .setDescription("Wähle eine Bewerbung zum Konfigurieren")
.setRequired(false)
.setAutocomplete(true),
),
@@ -101,9 +104,9 @@ export default {
category: "Community",
- execute: withErrorHandling(async (interaction) => {
+ execute: withErrorHandling(async (interaction: ChatInputCommandInteraction) => {
if (!interaction.inGuild()) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'This command can only be used in a server.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Dieser Command kann nur auf einem Server genutzt werden.' });
}
const { options, guild, member } = interaction;
@@ -115,11 +118,11 @@ export default {
logger.info(`App-admin command executed: ${subcommand}`, {
userId: interaction.user.id,
- guildId: guild.id,
+ guildId: guild!.id,
subcommand
});
- await ApplicationService.checkManagerPermission(interaction.client, guild.id, member);
+ await ApplicationService.checkManagerPermission(interaction.client, guild!.id, member as GuildMember);
if (subcommand === "setup") {
await handleSetup(interaction);
@@ -132,61 +135,61 @@ export default {
await appDashboard.execute(interaction, null, interaction.client, selectedAppName);
}
}, { type: 'command', commandName: 'app-admin' })
-};
+} satisfies Command;
-async function handleSetup(interaction) {
+async function handleSetup(interaction: ChatInputCommandInteraction) {
if (interaction.deferred || interaction.replied) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'This interaction has already been processed. Please try the command again.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Diese Interaktion wurde bereits verarbeitet. Bitte führe den Command erneut aus.' });
}
const modal = new ModalBuilder()
.setCustomId('app_setup_modal')
- .setTitle('Set Up New Application');
+ .setTitle('Neue Bewerbung einrichten');
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('role_id')
- .setPlaceholder('Select the role users will apply for')
+ .setPlaceholder('Rolle wählen, auf die sich Nutzer bewerben')
.setRequired(true);
const roleLabel = new LabelBuilder()
- .setLabel('Application Role')
- .setDescription('The role that users will be applying for')
+ .setLabel('Bewerbungsrolle')
+ .setDescription('Die Rolle, auf die sich Nutzer bewerben')
.setRoleSelectMenuComponent(roleSelect);
const appNameInput = new TextInputBuilder()
.setCustomId('app_name')
.setStyle(TextInputStyle.Short)
- .setPlaceholder('e.g., Moderator, Helper, Developer')
+ .setPlaceholder('z. B. Moderator, Helfer, Entwickler')
.setMaxLength(50)
.setMinLength(1)
.setRequired(true);
const appNameLabel = new LabelBuilder()
- .setLabel('Application Name')
+ .setLabel('Name der Bewerbung')
.setTextInputComponent(appNameInput);
const q1Input = new TextInputBuilder()
.setCustomId('app_question_1')
.setStyle(TextInputStyle.Short)
- .setPlaceholder('Why do you want this role?')
+ .setPlaceholder('Warum möchtest du diese Rolle?')
.setMaxLength(100)
.setMinLength(1)
.setRequired(true);
const q1Label = new LabelBuilder()
- .setLabel('Question 1 (required)')
+ .setLabel('Frage 1 (Pflicht)')
.setTextInputComponent(q1Input);
const q2Input = new TextInputBuilder()
.setCustomId('app_question_2')
.setStyle(TextInputStyle.Short)
- .setPlaceholder('What experience do you have?')
+ .setPlaceholder('Welche Erfahrung hast du?')
.setMaxLength(100)
.setRequired(false);
const q2Label = new LabelBuilder()
- .setLabel('Question 2 (optional)')
+ .setLabel('Frage 2 (optional)')
.setTextInputComponent(q2Input);
const q3Input = new TextInputBuilder()
@@ -196,7 +199,7 @@ async function handleSetup(interaction) {
.setRequired(false);
const q3Label = new LabelBuilder()
- .setLabel('Question 3 (optional)')
+ .setLabel('Frage 3 (optional)')
.setTextInputComponent(q3Input);
modal.addLabelComponents(roleLabel, appNameLabel, q1Label, q2Label, q3Label);
@@ -211,16 +214,16 @@ async function handleSetup(interaction) {
}).catch(() => null);
if (!submitted) {
- logger.info('App setup modal dismissed or timed out', { guildId: interaction.guild.id, userId: interaction.user.id });
+ logger.info('App setup modal dismissed or timed out', { guildId: interaction.guild!.id, userId: interaction.user.id });
return;
}
const appName = submitted.fields.getTextInputValue('app_name').trim();
const selectedRoles = submitted.fields.getSelectedRoles('role_id');
- const roleId = selectedRoles.first()?.id;
+ const roleId = selectedRoles!.first()?.id;
if (!roleId) {
- await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'You must select a role for the application.' });
+ await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'Du musst eine Rolle für die Bewerbung auswählen.' });
return;
}
@@ -230,15 +233,15 @@ async function handleSetup(interaction) {
submitted.fields.getTextInputValue('app_question_3').trim(),
].filter(q => q.length > 0);
- const role = await interaction.guild.roles.fetch(roleId).catch(() => null);
+ const role = await interaction.guild!.roles.fetch(roleId).catch(() => null);
if (!role) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'The selected role could not be found.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Die ausgewählte Rolle wurde nicht gefunden.' });
return;
}
- const existingRoles = await getApplicationRoles(interaction.client, interaction.guild.id);
+ const existingRoles = await getApplicationRoles(interaction.client, interaction.guild!.id);
if (existingRoles.some(r => r.roleId === roleId)) {
- await replyUserError(submitted, { type: ErrorTypes.CONFIGURATION, message: `The role ${role} is already configured as an application.` });
+ await replyUserError(submitted, { type: ErrorTypes.CONFIGURATION, message: `Die Rolle ${role} ist bereits als Bewerbung eingerichtet.` });
return;
}
@@ -248,19 +251,19 @@ async function handleSetup(interaction) {
enabled: true,
});
- await saveApplicationRoles(interaction.client, interaction.guild.id, existingRoles);
+ await saveApplicationRoles(interaction.client, interaction.guild!.id, existingRoles);
- const settings = await getApplicationSettings(interaction.client, interaction.guild.id);
+ const settings = await getApplicationSettings(interaction.client, interaction.guild!.id);
if (!settings.enabled) {
- await ApplicationService.updateSettings(interaction.client, interaction.guild.id, { enabled: true });
+ await ApplicationService.updateSettings(interaction.client, interaction.guild!.id, { enabled: true });
}
- await saveApplicationRoleSettings(interaction.client, interaction.guild.id, roleId, { questions });
+ await saveApplicationRoleSettings(interaction.client, interaction.guild!.id, roleId, { questions });
await submitted.reply({
embeds: [successEmbed(
- '✅ Application Created',
- `**${appName}** application has been created for ${role}.\n\nYou can customize the log channel, manager roles, questions, and retention period in the dashboard.`,
+ '✅ Bewerbung erstellt',
+ `Die Bewerbung **${appName}** wurde für ${role} erstellt.\n\nLog-Kanal, Manager-Rollen, Fragen und Aufbewahrungsdauer kannst du im Dashboard anpassen.`,
)],
flags: ['Ephemeral'],
});
@@ -270,46 +273,46 @@ async function handleSetup(interaction) {
}, 500);
}
-async function handleReview(interaction) {
- const appId = interaction.options.getString("id");
+async function handleReview(interaction: ChatInputCommandInteraction) {
+ const appId = interaction.options.getString("id")!;
const application = await getApplication(
interaction.client,
- interaction.guild.id,
+ interaction.guild!.id,
appId,
);
if (!application) {
- return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Application not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Bewerbung nicht gefunden.' });
}
if (application.status !== "pending") {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'This application has already been processed.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Diese Bewerbung wurde bereits bearbeitet.' });
}
const appEmbed = createEmbed({
- title: `Review Application`,
- description: `**User:** <@${application.userId}>\n**Application:** ${application.roleName}\n**Application ID:** \`${appId}\``,
+ title: `Bewerbung prüfen`,
+ description: `**Nutzer:** <@${application.userId}>\n**Bewerbung:** ${application.roleName}\n**Bewerbungs-ID:** \`${appId}\``,
color: 'info',
});
if (application.answers && application.answers.length > 0) {
application.answers.forEach((item, index) => {
appEmbed.addFields({
- name: `Q${index + 1}: ${item.question}`,
- value: item.answer || '*No answer provided*',
+ name: `F${index + 1}: ${item.question}`,
+ value: item.answer || '*Keine Antwort angegeben*',
inline: false
});
});
}
- const buttonRow = new ActionRowBuilder().addComponents(
+ const buttonRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`app_review_approve_${appId}`)
- .setLabel('Approve')
+ .setLabel('Annehmen')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`app_review_deny_${appId}`)
- .setLabel('Deny')
+ .setLabel('Ablehnen')
.setStyle(ButtonStyle.Danger),
);
@@ -319,7 +322,7 @@ async function handleReview(interaction) {
flags: ["Ephemeral"],
});
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: i =>
i.user.id === interaction.user.id &&
@@ -334,15 +337,15 @@ async function handleReview(interaction) {
const reasonModal = new ModalBuilder()
.setCustomId(`app_review_reason_${appId}_${isApprove ? 'approve' : 'deny'}`)
- .setTitle(`${isApprove ? 'Approve' : 'Deny'} Application - Reason`);
+ .setTitle(`Bewerbung ${isApprove ? 'annehmen' : 'ablehnen'} – Grund`);
reasonModal.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('review_reason')
- .setLabel('Reason (optional)')
+ .setLabel('Grund (optional)')
.setStyle(TextInputStyle.Paragraph)
- .setPlaceholder('Provide a reason for this decision...')
+ .setPlaceholder('Gib einen Grund für diese Entscheidung an...')
.setMaxLength(500)
.setRequired(false),
),
@@ -360,13 +363,13 @@ async function handleReview(interaction) {
if (!reasonSubmit) return;
- const reason = reasonSubmit.fields.getTextInputValue('review_reason').trim() || "No reason provided.";
+ const reason = reasonSubmit.fields.getTextInputValue('review_reason').trim() || "Kein Grund angegeben.";
const action = isApprove ? 'approve' : 'deny';
const status = isApprove ? 'approved' : 'denied';
const updatedApplication = await ApplicationService.reviewApplication(
reasonSubmit.client,
- interaction.guild.id,
+ interaction.guild!.id,
appId,
{
action,
@@ -380,16 +383,16 @@ async function handleReview(interaction) {
const statusColor = getApplicationStatusColor(status);
const reviewStatus = getApplicationStatusPresentation(status);
const dmEmbed = createEmbed({
- title: `${reviewStatus.statusEmoji} Application ${reviewStatus.statusLabel}`,
- description: `Your application for **${application.roleName}** has been **${status}**\n` +
- `**Note:** ${reason}\n\n` +
- `Use \`/apply status id:${appId}\` to view details.`
+ title: `${reviewStatus.statusEmoji} Bewerbung ${reviewStatus.statusLabel.toLowerCase()}`,
+ description: `Deine Bewerbung für **${application.roleName}** wurde **${reviewStatus.statusLabel.toLowerCase()}**.\n` +
+ `**Hinweis:** ${reason}\n\n` +
+ `Details siehst du mit \`/apply status id:${appId}\`.`
}).setColor(statusColor);
await user.send({ embeds: [dmEmbed] });
} catch (error) {
logger.warn('Failed to send DM to user for application review', {
- error: error.message,
+ error: (error as Error).message,
userId: application.userId,
applicationId: appId
});
@@ -398,12 +401,12 @@ async function handleReview(interaction) {
if (application.logMessageId && application.logChannelId) {
try {
const statusColor = getApplicationStatusColor(status);
- const logChannel = interaction.guild.channels.cache.get(
- application.logChannelId,
- );
+ const logChannel = interaction.guild!.channels.cache.get(
+ application.logChannelId as string,
+ ) as TextChannel | undefined;
if (logChannel) {
const logMessage = await logChannel.messages.fetch(
- application.logMessageId,
+ application.logMessageId as string,
);
if (logMessage) {
const embed = logMessage.embeds[0];
@@ -425,7 +428,7 @@ async function handleReview(interaction) {
}
} catch (error) {
logger.warn('Failed to update log message for application', {
- error: error.message,
+ error: (error as Error).message,
applicationId: appId,
logMessageId: application.logMessageId
});
@@ -434,13 +437,13 @@ async function handleReview(interaction) {
if (isApprove) {
try {
- const member = await interaction.guild.members.fetch(
+ const member = await interaction.guild!.members.fetch(
application.userId,
);
- await member.roles.add(application.roleId);
+ await member.roles.add(application.roleId!);
} catch (error) {
logger.error('Failed to assign role to approved applicant', {
- error: error.message,
+ error: (error as Error).message,
userId: application.userId,
roleId: application.roleId,
applicationId: appId
@@ -451,8 +454,8 @@ async function handleReview(interaction) {
await reasonSubmit.reply({
embeds: [
successEmbed(
- `Application ${status}`,
- `The application has been **${status}**.`,
+ `Bewerbung ${isApprove ? 'angenommen' : 'abgelehnt'}`,
+ `Die Bewerbung wurde **${isApprove ? 'angenommen' : 'abgelehnt'}**.`,
),
],
flags: ["Ephemeral"],
@@ -460,15 +463,15 @@ async function handleReview(interaction) {
} catch (error) {
logger.error('Error reviewing application:', error);
- await replyUserError(buttonInteraction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while reviewing the application.' });
+ await replyUserError(buttonInteraction, { type: ErrorTypes.UNKNOWN, message: 'Beim Prüfen der Bewerbung ist ein Fehler aufgetreten.' });
}
});
collector.on('end', async (collected, reason) => {
if (reason === 'time') {
const timeoutEmbed = createEmbed({
- title: 'Review Timeout',
- description: 'The review buttons have timed out.',
+ title: 'Prüfung abgelaufen',
+ description: 'Die Buttons zur Prüfung sind abgelaufen.',
color: 'warning',
});
@@ -480,12 +483,12 @@ async function handleReview(interaction) {
});
}
-async function handleList(interaction) {
+async function handleList(interaction: ChatInputCommandInteraction) {
const status = interaction.options.getString("status");
const user = interaction.options.getUser("user");
const limit = interaction.options.getNumber("limit") || 10;
- const filters = {};
+ const filters: ApplicationFilters = {};
if (status) {
filters.status = status;
@@ -495,7 +498,7 @@ async function handleList(interaction) {
let applications = await getApplications(
interaction.client,
- interaction.guild.id,
+ interaction.guild!.id,
filters,
);
@@ -503,15 +506,15 @@ async function handleList(interaction) {
applications = await Promise.all(
applications.map(async (app) => {
try {
- await interaction.guild.members.fetch(app.userId);
+ await interaction.guild!.members.fetch(app.userId);
return app;
} catch {
- await deleteApplication(interaction.client, interaction.guild.id, app.id, app.userId);
+ await deleteApplication(interaction.client, interaction.guild!.id, app.id, app.userId);
return null;
}
})
- ).then(results => results.filter(Boolean));
+ ).then(results => results.filter((app): app is Application => Boolean(app)));
}
if (user) {
@@ -519,58 +522,58 @@ async function handleList(interaction) {
}
if (applications.length === 0) {
- const applicationRoles = await getApplicationRoles(interaction.client, interaction.guild.id);
+ const applicationRoles = await getApplicationRoles(interaction.client, interaction.guild!.id);
if (applicationRoles.length > 0) {
const embed = createEmbed({
- title: "No Applications Found",
- description: "No submitted applications found matching the specified criteria.\n\nHowever, the following application roles are configured:"
+ title: "Keine Bewerbungen gefunden",
+ description: "Es wurden keine eingereichten Bewerbungen gefunden, die zu deinen Kriterien passen.\n\nDiese Bewerbungsrollen sind aber eingerichtet:"
});
applicationRoles.forEach((appRole, index) => {
- const role = interaction.guild.roles.cache.get(appRole.roleId);
+ const role = interaction.guild!.roles.cache.get(appRole.roleId);
embed.addFields({
name: `${index + 1}. ${appRole.name}`,
- value: `**Role:** ${role ?`<@&${appRole.roleId}>`: 'Role not found'}\n**Available for applications:** Yes`,
+ value: `**Rolle:** ${role ?`<@&${appRole.roleId}>`: 'Rolle nicht gefunden'}\n**Für Bewerbungen verfügbar:** Ja`,
inline: false
});
});
embed.setFooter({
- text: "Users can apply with /apply submit or see available roles with /apply list"
+ text: "Nutzer bewerben sich mit /apply submit und sehen verfügbare Rollen mit /apply list"
});
return InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: ["Ephemeral"] });
} else {
return await replyUserError(interaction, {
type: ErrorTypes.CONFIGURATION,
- message: 'No applications found and no application roles configured.\n' +
- 'Use `/app-admin roles add` to configure application roles first.'
+ message: 'Keine Bewerbungen gefunden und keine Bewerbungsrollen eingerichtet.\n' +
+ 'Richte zuerst mit `/app-admin setup` eine Bewerbungsrolle ein.'
});
}
}
applications = applications
- .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, limit);
- const embed = createEmbed({ title: "Submitted Applications", description: `Showing ${applications.length} applications.`, });
+ const embed = createEmbed({ title: "Eingereichte Bewerbungen", description: `${applications.length} Bewerbung(en) werden angezeigt.`, });
applications.forEach((app) => {
const statusView = getApplicationStatusPresentation(app?.status);
- const roleName = app?.roleName || 'Unknown Role';
- const username = app?.username || 'Unknown User';
+ const roleName = app?.roleName || 'Unbekannte Rolle';
+ const username = app?.username || 'Unbekannter Nutzer';
const createdAt = app?.createdAt ? new Date(app.createdAt) : null;
const createdAtDisplay = createdAt && !Number.isNaN(createdAt.getTime())
? createdAt.toLocaleString()
- : 'Unknown date';
+ : 'Unbekanntes Datum';
embed.addFields({
name: `${statusView.statusEmoji} ${roleName} - ${username}`,
value:
`**ID:** \`${app.id}\`\n` +
`**Status:** ${statusView.statusEmoji} ${statusView.statusLabel}\n` +
- `**Date:** ${createdAtDisplay}`,
+ `**Datum:** ${createdAtDisplay}`,
inline: true,
});
});
diff --git a/src/commands/Community/apply.js b/src/commands/Community/apply.ts
similarity index 66%
rename from src/commands/Community/apply.js
rename to src/commands/Community/apply.ts
index 0e2c2c9249..f352c4c7e5 100644
--- a/src/commands/Community/apply.js
+++ b/src/commands/Community/apply.ts
@@ -1,5 +1,7 @@
import { getColor, getDefaultApplicationQuestions } from '../../config/bot.js';
import { SlashCommandBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
+import type { ChatInputCommandInteraction, ModalSubmitInteraction } from 'discord.js';
+import type { Command } from '../../types/index.js';
import { createEmbed, successEmbed } from '../../utils/embeds.js';
import { logger } from '../../utils/logger.js';
import { handleInteractionError, withErrorHandling, createError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
@@ -17,14 +19,15 @@ import {
updateApplication,
getApplicationRoleSettings
} from '../../utils/database.js';
+import type { ApplicationAnswer, ApplicationSettings } from '../../utils/database.js';
-function getApplicationStatusPresentation(statusValue) {
+function getApplicationStatusPresentation(statusValue: unknown) {
const normalized = typeof statusValue === 'string' ? statusValue.trim().toLowerCase() : 'unknown';
const statusLabel =
- normalized === 'pending' ? 'In Progress' :
- normalized === 'approved' ? 'Accepted' :
- normalized === 'denied' ? 'Denied' :
- 'Unknown';
+ normalized === 'pending' ? 'In Bearbeitung' :
+ normalized === 'approved' ? 'Angenommen' :
+ normalized === 'denied' ? 'Abgelehnt' :
+ 'Unbekannt';
const statusEmoji =
normalized === 'pending' ? '🟡' :
normalized === 'approved' ? '🟢' :
@@ -38,15 +41,15 @@ export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName("apply")
- .setDescription("Manage role applications")
+ .setDescription("Bewerbungen für Rollen verwalten")
.addSubcommand((subcommand) =>
subcommand
.setName("submit")
- .setDescription("Submit an application for a role")
+ .setDescription("Eine Bewerbung für eine Rolle einreichen")
.addStringOption((option) =>
option
.setName("application")
- .setDescription("The application you want to submit")
+ .setDescription("Die Bewerbung, die du einreichen möchtest")
.setRequired(true)
.setAutocomplete(true),
),
@@ -54,25 +57,25 @@ export default {
.addSubcommand((subcommand) =>
subcommand
.setName("status")
- .setDescription("Check the status of your application")
+ .setDescription("Den Status deiner Bewerbung prüfen")
.addStringOption((option) =>
option
.setName("id")
- .setDescription("Application ID (leave empty to see all)")
+ .setDescription("Bewerbungs-ID (leer lassen, um alle zu sehen)")
.setRequired(false),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName("list")
- .setDescription("List available applications to apply for"),
+ .setDescription("Verfügbare Bewerbungen anzeigen"),
),
category: "Community",
- execute: withErrorHandling(async (interaction) => {
+ execute: withErrorHandling(async (interaction: ChatInputCommandInteraction) => {
if (!interaction.inGuild()) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'This command can only be used in a server.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Dieser Command kann nur auf einem Server genutzt werden.' });
}
const { options, guild, member } = interaction;
@@ -85,21 +88,21 @@ export default {
logger.info(`Apply command executed: ${subcommand}`, {
userId: interaction.user.id,
- guildId: guild.id,
+ guildId: guild!.id,
subcommand
});
const settings = await getApplicationSettings(
interaction.client,
- guild.id,
+ guild!.id,
);
if (!settings.enabled) {
throw createError(
'Applications are disabled',
ErrorTypes.CONFIGURATION,
- 'Applications are currently disabled in this server.',
- { guildId: guild.id }
+ 'Bewerbungen sind auf diesem Server derzeit deaktiviert.',
+ { guildId: guild!.id }
);
}
@@ -111,9 +114,9 @@ export default {
await handleList(interaction);
}
}, { type: 'command', commandName: 'apply' })
-};
+} satisfies Command;
-export async function handleApplicationModal(interaction) {
+export async function handleApplicationModal(interaction: ModalSubmitInteraction) {
if (!interaction.isModalSubmit()) return;
const customId = interaction.customId;
@@ -121,24 +124,24 @@ export async function handleApplicationModal(interaction) {
const roleId = customId.split('_')[2];
- const applicationRoles = await getApplicationRoles(interaction.client, interaction.guild.id);
+ const applicationRoles = await getApplicationRoles(interaction.client, interaction.guild!.id);
const applicationRole = applicationRoles.find(appRole => appRole.roleId === roleId);
if (!applicationRole) {
- return await replyUserError(interaction, { type: ErrorTypes.CONFIGURATION, message: 'Application configuration not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.CONFIGURATION, message: 'Bewerbungskonfiguration nicht gefunden.' });
}
- const role = interaction.guild.roles.cache.get(roleId);
+ const role = interaction.guild!.roles.cache.get(roleId);
if (!role) {
- return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Role not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Rolle nicht gefunden.' });
}
- const answers = [];
- const settings = await getApplicationSettings(interaction.client, interaction.guild.id);
+ const answers: ApplicationAnswer[] = [];
+ const settings = await getApplicationSettings(interaction.client, interaction.guild!.id);
let questions = settings.questions?.length ? settings.questions : getDefaultApplicationQuestions();
- const roleSettings = await getApplicationRoleSettings(interaction.client, interaction.guild.id, roleId);
+ const roleSettings = await getApplicationRoleSettings(interaction.client, interaction.guild!.id, roleId);
if (roleSettings.questions && roleSettings.questions.length > 0) {
questions = roleSettings.questions;
}
@@ -153,7 +156,7 @@ export async function handleApplicationModal(interaction) {
try {
const application = await ApplicationService.submitApplication(interaction.client, {
- guildId: interaction.guild.id,
+ guildId: interaction.guild!.id,
userId: interaction.user.id,
roleId: roleId,
roleName: applicationRole.name,
@@ -163,43 +166,43 @@ export async function handleApplicationModal(interaction) {
});
const embed = successEmbed(
- 'Application Submitted',
- `Your application for **${applicationRole.name}** has been submitted successfully!\n\n` +
- `Application ID: \`${application.id}\`\n` +
- `You can check the status with \`/apply status id:${application.id}\``
+ 'Bewerbung eingereicht',
+ `Deine Bewerbung für **${applicationRole.name}** wurde erfolgreich eingereicht!\n\n` +
+ `Bewerbungs-ID: \`${application.id}\`\n` +
+ `Den Status prüfst du mit \`/apply status id:${application.id}\``
);
await InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: ["Ephemeral"] });
- const settings = await getApplicationSettings(interaction.client, interaction.guild.id);
- const roleSettings = await getApplicationRoleSettings(interaction.client, interaction.guild.id, roleId);
- const guildConfig = await getGuildConfig(interaction.client, interaction.guild.id);
+ const settings = await getApplicationSettings(interaction.client, interaction.guild!.id);
+ const roleSettings = await getApplicationRoleSettings(interaction.client, interaction.guild!.id, roleId);
+ const guildConfig = await getGuildConfig(interaction.client, interaction.guild!.id);
const logChannelId = resolveApplicationLogChannel(guildConfig, roleSettings, settings);
if (logChannelId) {
const logMessage = await logEvent({
client: interaction.client,
- guildId: interaction.guild.id,
+ guildId: interaction.guild!.id,
eventType: EVENT_TYPES.APPLICATION_SUBMIT,
channelId: logChannelId,
data: {
- title: 'Application Submitted',
+ title: 'Bewerbung eingereicht',
lines: [
- formatLogLine('Applicant', `<@${interaction.user.id}> (${interaction.user.tag})`),
- formatLogLine('Application', applicationRole.name),
- formatLogLine('Role', role.name),
- formatLogLine('Application ID', `\`${application.id}\``),
+ formatLogLine('Bewerber', `<@${interaction.user.id}> (${interaction.user.tag})`),
+ formatLogLine('Bewerbung', applicationRole.name),
+ formatLogLine('Rolle', role.name),
+ formatLogLine('Bewerbungs-ID', `\`${application.id}\``),
],
inlineFields: [
- { name: 'Status', value: '🟡 In Progress', inline: true },
+ { name: 'Status', value: '🟡 In Bearbeitung', inline: true },
],
author: await resolveUserAuthor(interaction.client, interaction.user.id),
},
});
if (logMessage) {
- await updateApplication(interaction.client, interaction.guild.id, application.id, {
+ await updateApplication(interaction.client, interaction.guild!.id, application.id, {
logMessageId: logMessage.id,
logChannelId,
});
@@ -208,11 +211,11 @@ export async function handleApplicationModal(interaction) {
} catch (error) {
logger.error('Error creating application:', {
- error: error.message,
+ error: (error as Error).message,
userId: interaction.user.id,
- guildId: interaction.guild.id,
+ guildId: interaction.guild!.id,
roleId,
- stack: error.stack
+ stack: (error as Error).stack
});
await handleInteractionError(interaction, error, {
@@ -222,86 +225,86 @@ export async function handleApplicationModal(interaction) {
}
}
-async function handleList(interaction) {
+async function handleList(interaction: ChatInputCommandInteraction) {
try {
- const applicationRoles = await getApplicationRoles(interaction.client, interaction.guild.id);
+ const applicationRoles = await getApplicationRoles(interaction.client, interaction.guild!.id);
if (applicationRoles.length === 0) {
- return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'No applications are currently available.' });
+ return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Derzeit sind keine Bewerbungen verfügbar.' });
}
const embed = createEmbed({
- title: "Available Applications",
- description: "Here are the roles you can apply for:"
+ title: "Verfügbare Bewerbungen",
+ description: "Auf diese Rollen kannst du dich bewerben:"
});
applicationRoles.forEach((appRole, index) => {
- const role = interaction.guild.roles.cache.get(appRole.roleId);
+ const role = interaction.guild!.roles.cache.get(appRole.roleId);
embed.addFields({
name: `${index + 1}. ${appRole.name}`,
- value: `**Role:** ${role ?`<@&${appRole.roleId}>`: 'Role not found'}\n` +
- `**Apply with:** \`/apply submit application:"${appRole.name}"\``,
+ value: `**Rolle:** ${role ?`<@&${appRole.roleId}>`: 'Rolle nicht gefunden'}\n` +
+ `**Bewerben mit:** \`/apply submit application:"${appRole.name}"\``,
inline: false
});
});
embed.setFooter({
- text: "Use /apply submit application: to apply for any of these roles."
+ text: "Nutze /apply submit application:, um dich auf eine dieser Rollen zu bewerben."
});
return InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
} catch (error) {
logger.error('Error listing applications:', {
- error: error.message,
- guildId: interaction.guild.id,
- stack: error.stack
+ error: (error as Error).message,
+ guildId: interaction.guild!.id,
+ stack: (error as Error).stack
});
throw createError(
'Failed to load applications',
ErrorTypes.DATABASE,
- 'Failed to load applications. Please try again later.',
- { guildId: interaction.guild.id }
+ 'Bewerbungen konnten nicht geladen werden. Bitte versuch es später erneut.',
+ { guildId: interaction.guild!.id }
);
}
}
-async function handleSubmit(interaction, settings) {
+async function handleSubmit(interaction: ChatInputCommandInteraction, settings: ApplicationSettings) {
const applicationName = interaction.options.getString("application");
const member = interaction.member;
- const applicationRoles = await getApplicationRoles(interaction.client, interaction.guild.id);
+ const applicationRoles = await getApplicationRoles(interaction.client, interaction.guild!.id);
const applicationRole = applicationRoles.find(appRole =>
- appRole.name.toLowerCase() === applicationName.toLowerCase()
+ appRole.name.toLowerCase() === applicationName!.toLowerCase()
);
if (!applicationRole) {
- return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Use `/apply list` to see available applications.' });
+ return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Bewerbung nicht gefunden. Nutze `/apply list`, um die verfügbaren Bewerbungen zu sehen.' });
}
const userApps = await getUserApplications(
interaction.client,
- interaction.guild.id,
+ interaction.guild!.id,
interaction.user.id,
);
const pendingApp = userApps.find((app) => app.status === "pending");
if (pendingApp) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You already have a pending application. Please wait for it to be reviewed.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du hast bereits eine offene Bewerbung. Bitte warte, bis sie geprüft wurde.' });
}
- const role = interaction.guild.roles.cache.get(applicationRole.roleId);
+ const role = interaction.guild!.roles.cache.get(applicationRole.roleId);
if (!role) {
- return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'The role for this application no longer exists.' });
+ return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Die Rolle für diese Bewerbung existiert nicht mehr.' });
}
const modal = new ModalBuilder()
.setCustomId(`app_modal_${applicationRole.roleId}`)
- .setTitle(`Application for ${applicationRole.name}`);
+ .setTitle(`Bewerbung: ${applicationRole.name}`.slice(0, 45));
let questions = settings.questions?.length ? settings.questions : getDefaultApplicationQuestions();
- const roleSettings = await getApplicationRoleSettings(interaction.client, interaction.guild.id, applicationRole.roleId);
+ const roleSettings = await getApplicationRoleSettings(interaction.client, interaction.guild!.id, applicationRole.roleId);
if (roleSettings.questions && roleSettings.questions.length > 0) {
questions = roleSettings.questions;
}
@@ -318,80 +321,80 @@ async function handleSubmit(interaction, settings) {
.setRequired(true)
.setMaxLength(1000);
- const row = new ActionRowBuilder().addComponents(input);
+ const row = new ActionRowBuilder().addComponents(input);
modal.addComponents(row);
});
await interaction.showModal(modal);
}
-async function handleStatus(interaction) {
+async function handleStatus(interaction: ChatInputCommandInteraction) {
const appId = interaction.options.getString("id");
if (appId) {
const application = await getApplication(
interaction.client,
- interaction.guild.id,
+ interaction.guild!.id,
appId,
);
if (!application || application.userId !== interaction.user.id) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Application not found or you do not have permission to view it.' });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Bewerbung nicht gefunden oder du darfst sie nicht ansehen.' });
}
const submittedAt = application?.createdAt ? new Date(application.createdAt) : null;
const submittedAtDisplay = submittedAt && !Number.isNaN(submittedAt.getTime())
? submittedAt.toLocaleString()
- : 'Unknown date';
+ : 'Unbekanntes Datum';
const statusView = getApplicationStatusPresentation(application.status);
const embed = createEmbed({
- title: `Application #${application.id} - ${application.roleName || 'Unknown Role'}`,
+ title: `Bewerbung #${application.id} - ${application.roleName || 'Unbekannte Rolle'}`,
description:
- `**Application ID:** \`${application.id}\`\n` +
+ `**Bewerbungs-ID:** \`${application.id}\`\n` +
`**Status:** ${statusView.statusEmoji} ${statusView.statusLabel}\n` +
- `**Submitted:** ${submittedAtDisplay}`
+ `**Eingereicht:** ${submittedAtDisplay}`
});
return InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: ["Ephemeral"] });
} else {
const applications = await getUserApplications(
interaction.client,
- interaction.guild.id,
+ interaction.guild!.id,
interaction.user.id,
);
if (applications.length === 0) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You have not submitted any applications yet.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du hast noch keine Bewerbungen eingereicht.' });
}
const recentApplications = applications
- .sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0))
+ .sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime())
.slice(0, 10);
const embed = createEmbed({
- title: "Your Applications",
- description: `Showing ${recentApplications.length} recent application(s).`
+ title: "Deine Bewerbungen",
+ description: `${recentApplications.length} aktuelle Bewerbung(en).`
});
recentApplications.forEach((application) => {
const submittedAt = application?.createdAt ? new Date(application.createdAt) : null;
const submittedAtDisplay = submittedAt && !Number.isNaN(submittedAt.getTime())
? submittedAt.toLocaleDateString()
- : 'Unknown date';
+ : 'Unbekanntes Datum';
const statusView = getApplicationStatusPresentation(application.status);
embed.addFields({
- name: `${statusView.statusEmoji} ${application.roleName || 'Unknown Role'} (${statusView.statusLabel})`,
+ name: `${statusView.statusEmoji} ${application.roleName || 'Unbekannte Rolle'} (${statusView.statusLabel})`,
value:
`**ID:** \`${application.id}\`\n` +
`**Status:** ${statusView.statusEmoji} ${statusView.statusLabel}\n` +
- `**Submitted:** ${submittedAtDisplay}`,
+ `**Eingereicht:** ${submittedAtDisplay}`,
inline: true,
});
});
if (applications.length > recentApplications.length) {
- embed.setFooter({ text: `Showing latest ${recentApplications.length} of ${applications.length} applications.` });
+ embed.setFooter({ text: `Die neuesten ${recentApplications.length} von ${applications.length} Bewerbungen.` });
}
return InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: ["Ephemeral"] });
diff --git a/src/commands/Community/modules/app_dashboard.js b/src/commands/Community/modules/app_dashboard.ts
similarity index 63%
rename from src/commands/Community/modules/app_dashboard.js
rename to src/commands/Community/modules/app_dashboard.ts
index 856a28b918..8463c98698 100644
--- a/src/commands/Community/modules/app_dashboard.js
+++ b/src/commands/Community/modules/app_dashboard.ts
@@ -18,10 +18,20 @@ import {
CheckboxBuilder,
TextDisplayBuilder,
} from 'discord.js';
+import type {
+ ChatInputCommandInteraction,
+ Guild,
+ ModalSubmitInteraction,
+ SelectMenuModalData,
+ StringSelectMenuInteraction,
+} from 'discord.js';
+import type { GuildConfig, NexoClient } from '../../../types/index.js';
+import type { ApplicationRole, ApplicationSettings } from '../../../utils/database.js';
+import type { ErrorLike } from '../../../utils/errorHandler.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { successEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
import { safeDeferInteraction } from '../../../utils/interactionValidator.js';
import {
getApplicationSettings,
@@ -37,110 +47,117 @@ import {
import { getGuildConfig } from '../../../services/config/guildConfig.js';
import { setLogChannel, resolveApplicationLogChannel, resolveLogChannel } from '../../../services/loggingService.js';
-async function buildDashboardEmbed(settings, roles, guild, client) {
+/** The dashboard is opened from the /app-admin slash command or from the setup modal submit. */
+export type AppDashboardInteraction = ChatInputCommandInteraction | ModalSubmitInteraction;
+
+/** `managerRoles` is stored in the settings record but not declared on ApplicationSettings. */
+type ManagerRoles = string[] | undefined;
+
+async function buildDashboardEmbed(settings: ApplicationSettings, roles: ApplicationRole[], guild: Guild, client: NexoClient) {
const guildConfig = await getGuildConfig(client, guild.id);
const applicationsChannel = resolveLogChannel(guildConfig, 'applications') || settings.logChannelId;
- const logChannel = applicationsChannel ? `<#${applicationsChannel}>` : '`Not set`';
+ const logChannel = applicationsChannel ? `<#${applicationsChannel}>` : '`Nicht festgelegt`';
+ const managerRoles = settings.managerRoles as ManagerRoles;
const managerRoleList =
- settings.managerRoles?.length > 0
- ? settings.managerRoles.map(id => `<@&${id}>`).join(',')
- : '`None configured`';
+ (managerRoles?.length ?? 0) > 0
+ ? managerRoles!.map(id => `<@&${id}>`).join(',')
+ : '`Keine eingerichtet`';
const roleList =
roles.length > 0
? roles.map(r => `<@&${r.roleId}> — ${r.name}`).join('\n')
- : '`No application roles configured`';
+ : '`Keine Bewerbungsrollen eingerichtet`';
const questionCount = settings.questions?.length ?? 0;
const firstQ =
settings.questions?.[0]
? `\`${settings.questions[0].length > 55 ? settings.questions[0].substring(0, 55) + '…' : settings.questions[0]}\``
- : '`Not set`';
+ : '`Nicht festgelegt`';
return new EmbedBuilder()
- .setTitle('Applications Dashboard')
- .setDescription(`Manage application settings for **${guild.name}**.\nSelect an option below to modify a setting.`)
+ .setTitle('Bewerbungs-Dashboard')
+ .setDescription(`Verwalte die Bewerbungseinstellungen für **${guild.name}**.\nWähle unten eine Option, um eine Einstellung zu ändern.`)
.setColor(getColor('info'))
.addFields(
- { name: 'Application Status', value: settings.enabled ? 'Enabled' : 'Disabled', inline: true },
- { name: 'Log Channel', value: logChannel, inline: true },
+ { name: 'Bewerbungsstatus', value: settings.enabled ? 'Aktiviert' : 'Deaktiviert', inline: true },
+ { name: 'Log-Kanal', value: logChannel, inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
- { name: 'Manager Roles', value: managerRoleList, inline: false },
- { name: 'Questions', value: `${questionCount} configured — first: ${firstQ}`, inline: false },
- { name: 'Application Roles', value: roleList, inline: false },
+ { name: 'Manager-Rollen', value: managerRoleList, inline: false },
+ { name: 'Fragen', value: `${questionCount} eingerichtet — erste: ${firstQ}`, inline: false },
+ { name: 'Bewerbungsrollen', value: roleList, inline: false },
{
- name: 'Retention',
- value: `Pending: **${settings.pendingApplicationRetentionDays ?? 30}d** · Reviewed: **${settings.reviewedApplicationRetentionDays ?? 14}d**`,
+ name: 'Aufbewahrung',
+ value: `Offen: **${settings.pendingApplicationRetentionDays ?? 30} T.** · Geprüft: **${settings.reviewedApplicationRetentionDays ?? 14} T.**`,
inline: false,
},
)
- .setFooter({ text: 'Dashboard closes after 15 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 15 Minuten Inaktivität' })
.setTimestamp();
}
-function buildSelectMenu(guildId) {
+function buildSelectMenu(guildId: string) {
return new StringSelectMenuBuilder()
.setCustomId(`app_cfg_${guildId}`)
- .setPlaceholder('Select a setting to configure...')
+ .setPlaceholder('Einstellung zum Konfigurieren auswählen...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Log Channel')
- .setDescription('Set the channel where new applications are logged')
+ .setLabel('Log-Kanal')
+ .setDescription('Kanal festlegen, in dem neue Bewerbungen geloggt werden')
.setValue('log_channel')
.setEmoji('📢'),
new StringSelectMenuOptionBuilder()
- .setLabel('Manager Roles')
- .setDescription('Add or remove a role that can manage applications')
+ .setLabel('Manager-Rollen')
+ .setDescription('Rolle hinzufügen oder entfernen, die Bewerbungen verwalten darf')
.setValue('manager_role')
.setEmoji('🛡️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Edit Questions')
- .setDescription('Customise the questions shown on the application form')
+ .setLabel('Fragen bearbeiten')
+ .setDescription('Fragen im Bewerbungsformular anpassen')
.setValue('questions')
.setEmoji('📝'),
new StringSelectMenuOptionBuilder()
- .setLabel('Add Application Role')
- .setDescription('Add a role that members can apply for')
+ .setLabel('Bewerbungsrolle hinzufügen')
+ .setDescription('Rolle hinzufügen, auf die sich Mitglieder bewerben können')
.setValue('role_add')
.setEmoji('➕'),
new StringSelectMenuOptionBuilder()
- .setLabel('Remove Application Role')
- .setDescription('Remove a role from the applications list')
+ .setLabel('Bewerbungsrolle entfernen')
+ .setDescription('Rolle aus der Bewerbungsliste entfernen')
.setValue('role_remove')
.setEmoji('➖'),
new StringSelectMenuOptionBuilder()
- .setLabel('Retention Period')
- .setDescription('Set how long pending and reviewed applications are kept')
+ .setLabel('Aufbewahrungsdauer')
+ .setDescription('Festlegen, wie lange offene und geprüfte Bewerbungen gespeichert werden')
.setValue('retention')
.setEmoji('🗑️'),
);
}
-function buildButtonRow(settings, guildId, disabled = false) {
+function buildButtonRow(settings: ApplicationSettings, guildId: string, disabled = false) {
const systemOn = settings.enabled === true;
- return new ActionRowBuilder().addComponents(
+ return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`app_cfg_toggle_${guildId}`)
- .setLabel('Applications')
+ .setLabel('Bewerbungen')
.setStyle(systemOn ? ButtonStyle.Success : ButtonStyle.Danger)
.setDisabled(disabled),
);
}
-async function refreshDashboard(rootInteraction, settings, roles, guildId, client) {
+async function refreshDashboard(rootInteraction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient) {
const selectMenu = buildSelectMenu(guildId);
await InteractionHelper.safeEditReply(rootInteraction, {
- embeds: [await buildDashboardEmbed(settings, roles, rootInteraction.guild, client)],
+ embeds: [await buildDashboardEmbed(settings, roles, rootInteraction.guild!, client)],
components: [
buildButtonRow(settings, guildId),
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
}).catch(() => {});
}
export default {
prefixOnly: false,
- async execute(interaction, config, client, selectedAppName = null) {
+ async execute(interaction: AppDashboardInteraction, config: GuildConfig | null, client: NexoClient, selectedAppName: string | null = null) {
try {
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
await InteractionHelper.safeDefer(interaction, { flags: ['Ephemeral'] });
@@ -155,14 +172,14 @@ export default {
const isCompletelyUnconfigured =
!applicationsChannel &&
!settings.enabled &&
- (settings.managerRoles?.length ?? 0) === 0 &&
+ ((settings.managerRoles as ManagerRoles)?.length ?? 0) === 0 &&
roles.length === 0;
if (isCompletelyUnconfigured) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Applications system not set up',
ErrorTypes.CONFIGURATION,
- 'The applications system has not been configured yet. Please run `/app-admin setup` to create your first application.',
+ 'Das Bewerbungssystem ist noch nicht eingerichtet. Nutze `/app-admin setup`, um deine erste Bewerbung zu erstellen.',
);
}
@@ -184,42 +201,42 @@ export default {
await showApplicationDashboard(interaction, defaultRole, settings, roles, guildId, client);
} catch (error) {
- if (error instanceof TitanBotError) throw error;
+ if (error instanceof NexoBotError) throw error;
logger.error('Unexpected error in app_dashboard:', error);
- throw new TitanBotError(
- `Applications dashboard failed: ${error.message}`,
+ throw new NexoBotError(
+ `Applications dashboard failed: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'Failed to open the applications dashboard.',
+ 'Das Bewerbungs-Dashboard konnte nicht geöffnet werden.',
);
}
},
};
-async function showApplicationSelector(interaction, roles, settings, guildId, client) {
+async function showApplicationSelector(interaction: AppDashboardInteraction, roles: ApplicationRole[], settings: ApplicationSettings, guildId: string, client: NexoClient) {
const selectMenu = new StringSelectMenuBuilder()
.setCustomId(`app_select_${guildId}`)
- .setPlaceholder('Select an application to configure...')
+ .setPlaceholder('Bewerbung zum Konfigurieren auswählen...')
.addOptions(
roles.map(role =>
new StringSelectMenuOptionBuilder()
.setLabel(role.name)
- .setDescription(`Configure the ${role.name} application`)
+ .setDescription(`Bewerbung ${role.name} konfigurieren`)
.setValue(role.roleId)
.setEmoji('📋'),
),
);
const embed = new EmbedBuilder()
- .setTitle('Select Application')
- .setDescription('Choose which application role you want to configure.')
+ .setTitle('Bewerbung auswählen')
+ .setDescription('Wähle, welche Bewerbungsrolle du konfigurieren möchtest.')
.setColor(getColor('info'));
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed],
- components: [new ActionRowBuilder().addComponents(selectMenu)],
+ components: [new ActionRowBuilder().addComponents(selectMenu)],
});
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === interaction.user.id && i.customId === `app_select_${guildId}`,
@@ -243,28 +260,28 @@ async function showApplicationSelector(interaction, roles, settings, guildId, cl
if (reason === 'time' && collected.size === 0) {
replyUserError(interaction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No selection was made. The dashboard has closed.',
+ message: 'Es wurde nichts ausgewählt. Das Dashboard wurde geschlossen.',
}).catch(() => {});
}
});
}
-async function showGlobalDashboard(interaction, settings, roles, guildId, client) {
+async function showGlobalDashboard(interaction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient) {
const selectMenu = buildSelectMenu(guildId);
await InteractionHelper.safeEditReply(interaction, {
- embeds: [await buildDashboardEmbed(settings, roles, interaction.guild, client)],
+ embeds: [await buildDashboardEmbed(settings, roles, interaction.guild!, client)],
components: [
buildButtonRow(settings, guildId),
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
});
setupCollectors(interaction, settings, roles, guildId, client, null);
}
-async function showApplicationDashboard(rootInteraction, selectedRole, settings, roles, guildId, client) {
- const roleObj = rootInteraction.guild.roles.cache.get(selectedRole.roleId);
+async function showApplicationDashboard(rootInteraction: AppDashboardInteraction, selectedRole: ApplicationRole, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient) {
+ const roleObj = rootInteraction.guild!.roles.cache.get(selectedRole.roleId);
const guildConfig = await getGuildConfig(client, guildId);
const appSettings = await getApplicationRoleSettings(client, guildId, selectedRole.roleId);
@@ -274,71 +291,72 @@ async function showApplicationDashboard(rootInteraction, selectedRole, settings,
const logChannelDisplay = appLogChannelId
? `<#${appLogChannelId}>`
- : '`Inherits global log channel`';
+ : '`Übernimmt den globalen Log-Kanal`';
const questionsDisplay = questions.length > 0
? questions.map((q, i) => `${i + 1}. \`${q.length > 60 ? q.substring(0, 60) + '…' : q}\``).join('\n')
- : '`Inherits global questions`';
+ : '`Übernimmt die globalen Fragen`';
- const managerRolesDisplay = settings.managerRoles && settings.managerRoles.length > 0
- ? settings.managerRoles.map(id => `<@&${id}>`).join(',')
- : '`None configured`';
+ const managerRoles = settings.managerRoles as ManagerRoles;
+ const managerRolesDisplay = managerRoles && managerRoles.length > 0
+ ? managerRoles.map(id => `<@&${id}>`).join(',')
+ : '`Keine eingerichtet`';
const embed = new EmbedBuilder()
- .setTitle('📋 Application Dashboard')
- .setDescription(`Configuration for **${selectedRole.name}**`)
+ .setTitle('📋 Bewerbungs-Dashboard')
+ .setDescription(`Konfiguration für **${selectedRole.name}**`)
.setColor(isEnabled ? getColor('success') : getColor('error'))
.addFields(
{
- name: 'Role',
+ name: 'Rolle',
value: roleObj ? roleObj.toString() : `<@&${selectedRole.roleId}>`,
inline: true
},
{
- name: 'Application Status',
- value: isEnabled ? '✅ **Enabled**' : '❌ **Disabled**',
+ name: 'Bewerbungsstatus',
+ value: isEnabled ? '✅ **Aktiviert**' : '❌ **Deaktiviert**',
inline: true
},
{ name: '\u200B', value: '\u200B', inline: true },
{
- name: 'Questions',
+ name: 'Fragen',
value: questionsDisplay,
inline: false
},
{
- name: 'Log Channel',
+ name: 'Log-Kanal',
value: logChannelDisplay,
inline: true
},
{
- name: 'Manager Roles',
+ name: 'Manager-Rollen',
value: managerRolesDisplay,
inline: true
},
{
- name: 'Retention Period',
- value: `Pending: **${settings.pendingApplicationRetentionDays ?? 30}d** · Reviewed: **${settings.reviewedApplicationRetentionDays ?? 14}d**`,
+ name: 'Aufbewahrungsdauer',
+ value: `Offen: **${settings.pendingApplicationRetentionDays ?? 30} T.** · Geprüft: **${settings.reviewedApplicationRetentionDays ?? 14} T.**`,
inline: false
},
)
- .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität' })
.setTimestamp();
const configMenu = buildApplicationSelectMenu(guildId, selectedRole.roleId);
- const controlButtons = new ActionRowBuilder().addComponents(
+ const controlButtons = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`app_toggle_${selectedRole.roleId}`)
- .setLabel(isEnabled ? 'Disable Application' : 'Enable Application')
+ .setLabel(isEnabled ? 'Bewerbung deaktivieren' : 'Bewerbung aktivieren')
.setStyle(isEnabled ? ButtonStyle.Danger : ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`app_delete_${selectedRole.roleId}`)
- .setLabel('Delete Application')
+ .setLabel('Bewerbung löschen')
.setStyle(ButtonStyle.Danger)
.setEmoji('🗑️'),
);
- const menuRow = new ActionRowBuilder().addComponents(configMenu);
+ const menuRow = new ActionRowBuilder().addComponents(configMenu);
await InteractionHelper.safeEditReply(rootInteraction, {
embeds: [embed],
@@ -348,10 +366,10 @@ async function showApplicationDashboard(rootInteraction, selectedRole, settings,
setupCollectors(rootInteraction, settings, roles, guildId, client, selectedRole.roleId);
}
-function setupCollectors(interaction, settings, roles, guildId, client, selectedRoleId) {
+function setupCollectors(interaction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient, selectedRoleId: string | null) {
const customIdPrefix = selectedRoleId ? `app_cfg_${selectedRoleId}` : `app_cfg_${guildId}`;
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === interaction.user.id &&
@@ -373,7 +391,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
await handleLogChannel(selectInteraction, interaction, settings, roles, guildId, client, selectedRoleId);
break;
case 'manager_role':
- await handleManagerRole(selectInteraction, interaction, settings, roles, guildId, client, selectedRoleId);
+ await handleManagerRole(selectInteraction, interaction, settings, roles, guildId, client);
break;
case 'questions':
await handleQuestions(selectInteraction, interaction, settings, roles, guildId, client, selectedRoleId);
@@ -385,20 +403,20 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
await handleRoleRemove(selectInteraction, interaction, settings, roles, guildId, client);
break;
case 'retention':
- await handleRetention(selectInteraction, interaction, settings, roles, guildId, client, selectedRoleId);
+ await handleRetention(selectInteraction, interaction, settings, roles, guildId, client);
break;
}
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
logger.debug(`Applications config validation error: ${error.message}`);
} else {
logger.error('Unexpected applications dashboard error:', error);
}
const errorMessage =
- error instanceof TitanBotError
- ? error.userMessage || 'An error occurred while processing your selection.'
- : 'An unexpected error occurred while updating the configuration.';
+ error instanceof NexoBotError
+ ? error.userMessage || 'Beim Verarbeiten deiner Auswahl ist ein Fehler aufgetreten.'
+ : 'Beim Aktualisieren der Konfiguration ist ein unerwarteter Fehler aufgetreten.';
if (!selectInteraction.replied && !selectInteraction.deferred) {
await safeDeferInteraction(selectInteraction);
@@ -414,8 +432,8 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
collector.on('end', async (collected, reason) => {
if (reason === 'time') {
const timeoutEmbed = new EmbedBuilder()
- .setTitle('\u23f0 Dashboard Timed Out')
- .setDescription('This dashboard has been closed due to inactivity. Please run the command again to continue.')
+ .setTitle('\u23f0 Dashboard abgelaufen')
+ .setDescription('Dieses Dashboard wurde wegen Inaktivität geschlossen. Führe den Command erneut aus, um weiterzumachen.')
.setColor(getColor('error'));
await InteractionHelper.safeEditReply(interaction, {
@@ -426,7 +444,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
});
if (!selectedRoleId) {
- const globalToggleCollector = interaction.channel.createMessageComponentCollector({
+ const globalToggleCollector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: i =>
i.user.id === interaction.user.id &&
@@ -450,11 +468,11 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
await toggleInteraction.followUp({
embeds: [successEmbed(
- wasEnabled ? '🔴 Applications Disabled' : '🟢 Applications Enabled',
- `The applications system is now **${wasEnabled ? 'disabled' : 'enabled'}**.\n\n${
+ wasEnabled ? '🔴 Bewerbungen deaktiviert' : '🟢 Bewerbungen aktiviert',
+ `Das Bewerbungssystem ist jetzt **${wasEnabled ? 'deaktiviert' : 'aktiviert'}**.\n\n${
wasEnabled
- ? 'Members will no longer be able to apply for roles.'
- : 'Members can now start applying for roles.'
+ ? 'Mitglieder können sich nicht mehr auf Rollen bewerben.'
+ : 'Mitglieder können sich jetzt auf Rollen bewerben.'
}`,
)],
flags: MessageFlags.Ephemeral,
@@ -464,7 +482,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
logger.error('Error toggling global application status:', error);
await replyUserError(toggleInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'An error occurred while toggling the application status.',
+ message: 'Beim Umschalten des Bewerbungsstatus ist ein Fehler aufgetreten.',
});
}
});
@@ -472,8 +490,8 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
globalToggleCollector.on('end', async (collected, reason) => {
if (reason === 'time') {
const timeoutEmbed = new EmbedBuilder()
- .setTitle('Configuration Timeout')
- .setDescription('This dashboard session has timed out due to inactivity (10 minutes).\n\nTo continue configuring your applications, please run the command again.')
+ .setTitle('Konfiguration abgelaufen')
+ .setDescription('Diese Dashboard-Sitzung ist wegen Inaktivität abgelaufen (10 Minuten).\n\nFühre den Command erneut aus, um deine Bewerbungen weiter zu konfigurieren.')
.setColor(getColor('warning'));
await InteractionHelper.safeEditReply(interaction, {
@@ -485,7 +503,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
}
if (selectedRoleId) {
- const btnCollector = interaction.channel.createMessageComponentCollector({
+ const btnCollector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: i =>
i.user.id === interaction.user.id &&
@@ -496,21 +514,21 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
btnCollector.on('collect', async btnInteraction => {
const appRoleForDelete = roles.find(r => r.roleId === selectedRoleId);
- const appNameForDelete = appRoleForDelete?.name ?? 'this application';
+ const appNameForDelete = appRoleForDelete?.name ?? 'diese Bewerbung';
const confirmModal = new ModalBuilder()
.setCustomId('app_delete_confirm')
- .setTitle('Confirm Application Deletion');
+ .setTitle('Löschen der Bewerbung bestätigen');
const deleteWarningText = new TextDisplayBuilder()
- .setContent(`⚠️ You are about to permanently delete **${appNameForDelete}**. All stored applications and settings for this role will be removed and cannot be recovered.`);
+ .setContent(`⚠️ Du bist dabei, **${appNameForDelete}** endgültig zu löschen. Alle gespeicherten Bewerbungen und Einstellungen für diese Rolle werden entfernt und können nicht wiederhergestellt werden.`);
const deleteCheckbox = new CheckboxBuilder()
.setCustomId('confirm_delete')
.setDefault(false);
const deleteCheckboxLabel = new LabelBuilder()
- .setLabel('I confirm — this cannot be undone')
+ .setLabel('Ich bestätige – nicht rückgängig zu machen')
.setCheckboxComponent(deleteCheckbox);
confirmModal
@@ -523,7 +541,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
logger.error('Error showing delete confirmation modal:', error);
await replyUserError(btnInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'Failed to show confirmation modal. Please try again.',
+ message: 'Das Bestätigungsfenster konnte nicht angezeigt werden. Bitte versuch es erneut.',
}).catch(() => {});
return;
}
@@ -538,14 +556,14 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
if (!confirmSubmit) {
await replyUserError(btnInteraction, {
type: ErrorTypes.VALIDATION,
- message: 'Application deletion was cancelled.',
+ message: 'Das Löschen der Bewerbung wurde abgebrochen.',
});
return;
}
const confirmed = confirmSubmit.fields.getCheckbox('confirm_delete');
if (!confirmed) {
- await replyUserError(confirmSubmit, { type: ErrorTypes.VALIDATION, message: 'You must tick the confirmation checkbox to delete the application.' });
+ await replyUserError(confirmSubmit, { type: ErrorTypes.VALIDATION, message: 'Du musst das Bestätigungskästchen anhaken, um die Bewerbung zu löschen.' });
return;
}
@@ -557,7 +575,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
logger.error('Error confirming application deletion:', error);
await replyUserError(btnInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'An error occurred while deleting the application.',
+ message: 'Beim Löschen der Bewerbung ist ein Fehler aufgetreten.',
});
}
});
@@ -565,8 +583,8 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
btnCollector.on('end', async (collected, reason) => {
if (reason === 'time') {
const timeoutEmbed = new EmbedBuilder()
- .setTitle('Configuration Timeout')
- .setDescription('This dashboard session has timed out due to inactivity (10 minutes).\n\nTo continue configuring your applications, please run the command again.')
+ .setTitle('Konfiguration abgelaufen')
+ .setDescription('Diese Dashboard-Sitzung ist wegen Inaktivität abgelaufen (10 Minuten).\n\nFühre den Command erneut aus, um deine Bewerbungen weiter zu konfigurieren.')
.setColor(getColor('warning'));
await InteractionHelper.safeEditReply(interaction, {
@@ -576,7 +594,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
}
});
- const toggleCollector = interaction.channel.createMessageComponentCollector({
+ const toggleCollector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: i =>
i.user.id === interaction.user.id &&
@@ -594,7 +612,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
if (roleIndex === -1) {
await replyUserError(toggleInteraction, {
type: ErrorTypes.USER_INPUT,
- message: 'Application role not found.',
+ message: 'Bewerbungsrolle nicht gefunden.',
});
return;
}
@@ -610,11 +628,11 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
await toggleInteraction.followUp({
embeds: [successEmbed(
- wasEnabled ? '🔴 Application Disabled' : '🟢 Application Enabled',
- `The **${updatedRole.name}** application is now **${wasEnabled ? 'disabled' : 'enabled'}**.\n\n${
+ wasEnabled ? '🔴 Bewerbung deaktiviert' : '🟢 Bewerbung aktiviert',
+ `Die Bewerbung **${updatedRole.name}** ist jetzt **${wasEnabled ? 'deaktiviert' : 'aktiviert'}**.\n\n${
wasEnabled
- ? 'This application will no longer appear in `/apply submit` options.'
- : 'This application will now appear in `/apply submit` options.'
+ ? 'Diese Bewerbung erscheint nicht mehr in den Optionen von `/apply submit`.'
+ : 'Diese Bewerbung erscheint jetzt in den Optionen von `/apply submit`.'
}`,
)],
flags: MessageFlags.Ephemeral,
@@ -624,7 +642,7 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
logger.error('Error toggling application status:', error);
await replyUserError(toggleInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'An error occurred while toggling the application status.',
+ message: 'Beim Umschalten des Bewerbungsstatus ist ein Fehler aufgetreten.',
});
}
});
@@ -632,8 +650,8 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
toggleCollector.on('end', async (collected, reason) => {
if (reason === 'time') {
const timeoutEmbed = new EmbedBuilder()
- .setTitle('Configuration Timeout')
- .setDescription('This dashboard session has timed out due to inactivity (10 minutes).\n\nTo continue configuring your applications, please run the command again.')
+ .setTitle('Konfiguration abgelaufen')
+ .setDescription('Diese Dashboard-Sitzung ist wegen Inaktivität abgelaufen (10 Minuten).\n\nFühre den Command erneut aus, um deine Bewerbungen weiter zu konfigurieren.')
.setColor(getColor('warning'));
await InteractionHelper.safeEditReply(interaction, {
@@ -645,35 +663,35 @@ function setupCollectors(interaction, settings, roles, guildId, client, selected
}
}
-function buildApplicationSelectMenu(guildId, roleId) {
+function buildApplicationSelectMenu(guildId: string, roleId: string) {
return new StringSelectMenuBuilder()
.setCustomId(`app_cfg_${roleId}`)
- .setPlaceholder('Select a setting to configure...')
+ .setPlaceholder('Einstellung zum Konfigurieren auswählen...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Log Channel')
- .setDescription('Set the channel where applications are logged')
+ .setLabel('Log-Kanal')
+ .setDescription('Kanal festlegen, in dem Bewerbungen geloggt werden')
.setValue('log_channel')
.setEmoji('📢'),
new StringSelectMenuOptionBuilder()
- .setLabel('Manager Roles')
- .setDescription('Add or remove a role that can manage applications')
+ .setLabel('Manager-Rollen')
+ .setDescription('Rolle hinzufügen oder entfernen, die Bewerbungen verwalten darf')
.setValue('manager_role')
.setEmoji('🛡️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Edit Questions')
- .setDescription('Customise the questions shown on the application form')
+ .setLabel('Fragen bearbeiten')
+ .setDescription('Fragen im Bewerbungsformular anpassen')
.setValue('questions')
.setEmoji('📝'),
new StringSelectMenuOptionBuilder()
- .setLabel('Retention Period')
- .setDescription('Set how long pending and reviewed applications are kept')
+ .setLabel('Aufbewahrungsdauer')
+ .setDescription('Festlegen, wie lange offene und geprüfte Bewerbungen gespeichert werden')
.setValue('retention')
.setEmoji('🗑️'),
);
}
-async function handleLogChannel(selectInteraction, rootInteraction, settings, roles, guildId, client, selectedRoleId) {
+async function handleLogChannel(selectInteraction: StringSelectMenuInteraction, rootInteraction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient, selectedRoleId: string | null) {
let currentChannel = settings.logChannelId;
if (selectedRoleId) {
const roleSettings = await getApplicationRoleSettings(client, guildId, selectedRoleId);
@@ -682,19 +700,19 @@ async function handleLogChannel(selectInteraction, rootInteraction, settings, ro
const modal = new ModalBuilder()
.setCustomId(`app_cfg_log_channel_modal_${guildId}_${selectedRoleId || 'global'}`)
- .setTitle('Configure Log Channel');
+ .setTitle('Log-Kanal konfigurieren');
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('log_channel')
- .setPlaceholder('Select a text channel...')
+ .setPlaceholder('Textkanal auswählen...')
.setMinValues(1)
.setMaxValues(1)
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
.setRequired(true);
const channelLabel = new LabelBuilder()
- .setLabel('Log Channel')
- .setDescription('Channel where new applications will be logged')
+ .setLabel('Log-Kanal')
+ .setDescription('Kanal, in dem neue Bewerbungen geloggt werden')
.setChannelSelectMenuComponent(channelSelect);
modal.addLabelComponents(channelLabel);
@@ -707,8 +725,8 @@ async function handleLogChannel(selectInteraction, rootInteraction, settings, ro
filter: i => i.user.id === selectInteraction.user.id && i.customId === `app_cfg_log_channel_modal_${guildId}_${selectedRoleId || 'global'}`,
});
- const channelId = modalSubmission.fields.getField('log_channel').values[0];
- const channel = selectInteraction.guild.channels.cache.get(channelId);
+ const channelId = (modalSubmission.fields.getField('log_channel') as SelectMenuModalData).values[0];
+ const channel = selectInteraction.guild!.channels.cache.get(channelId);
if (selectedRoleId) {
const roleSettings = await getApplicationRoleSettings(client, guildId, selectedRoleId);
@@ -721,36 +739,36 @@ async function handleLogChannel(selectInteraction, rootInteraction, settings, ro
}
await modalSubmission.reply({
- embeds: [successEmbed('Log Channel Updated', `Application logs will now be sent to ${channel ?? `<#${channelId}>`}.\nYou can also manage this from \`/logging dashboard\`.`)],
+ embeds: [successEmbed('Log-Kanal aktualisiert', `Bewerbungs-Logs werden jetzt an ${channel ?? `<#${channelId}>`} gesendet.\nDu kannst das auch über \`/logging dashboard\` verwalten.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, settings, roles, guildId, client);
} catch (error) {
- if (error.code === 'INTERACTION_TIMEOUT') return;
+ if ((error as ErrorLike).code === 'INTERACTION_TIMEOUT') return;
logger.error('Error in log channel modal:', error);
await replyUserError(selectInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'An error occurred while updating the log channel.',
+ message: 'Beim Aktualisieren des Log-Kanals ist ein Fehler aufgetreten.',
});
}
}
-async function handleManagerRole(selectInteraction, rootInteraction, settings, roles, guildId, client) {
+async function handleManagerRole(selectInteraction: StringSelectMenuInteraction, rootInteraction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId(`app_cfg_manager_role_modal_${guildId}`)
- .setTitle('Configure Manager Roles');
+ .setTitle('Manager-Rollen konfigurieren');
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('manager_roles')
- .setPlaceholder('Select roles to grant manager access...')
+ .setPlaceholder('Rollen für Manager-Zugriff auswählen...')
.setMinValues(1)
.setMaxValues(5)
.setRequired(true);
const roleLabel = new LabelBuilder()
- .setLabel('Manager Roles')
- .setDescription('Selected roles will be toggled on/off as manager roles')
+ .setLabel('Manager-Rollen')
+ .setDescription('Ausgewählte Rollen werden als Manager-Rollen an- bzw. abgeschaltet')
.setRoleSelectMenuComponent(roleSelect);
modal.addLabelComponents(roleLabel);
@@ -763,8 +781,8 @@ async function handleManagerRole(selectInteraction, rootInteraction, settings, r
filter: i => i.user.id === selectInteraction.user.id && i.customId === `app_cfg_manager_role_modal_${guildId}`,
});
- const selectedRoleIds = modalSubmission.fields.getField('manager_roles').values;
- const roleSet = new Set(settings.managerRoles ?? []);
+ const selectedRoleIds = (modalSubmission.fields.getField('manager_roles') as SelectMenuModalData).values;
+ const roleSet = new Set((settings.managerRoles as ManagerRoles) ?? []);
for (const roleId of selectedRoleIds) {
if (roleSet.has(roleId)) {
@@ -774,30 +792,31 @@ async function handleManagerRole(selectInteraction, rootInteraction, settings, r
}
}
- settings.managerRoles = Array.from(roleSet);
+ const managerRoles = Array.from(roleSet);
+ settings.managerRoles = managerRoles;
await saveApplicationSettings(client, guildId, settings);
- const finalList = settings.managerRoles.length > 0
- ? settings.managerRoles.map(id => `<@&${id}>`).join(',')
- : '`None`';
+ const finalList = managerRoles.length > 0
+ ? managerRoles.map(id => `<@&${id}>`).join(',')
+ : '`Keine`';
await modalSubmission.reply({
- embeds: [successEmbed('Manager Roles Updated', `Current manager roles: ${finalList}`)],
+ embeds: [successEmbed('Manager-Rollen aktualisiert', `Aktuelle Manager-Rollen: ${finalList}`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, settings, roles, guildId, client);
} catch (error) {
- if (error.code === 'INTERACTION_TIMEOUT') return;
+ if ((error as ErrorLike).code === 'INTERACTION_TIMEOUT') return;
logger.error('Error in manager role modal:', error);
await replyUserError(selectInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'An error occurred while updating manager roles.',
+ message: 'Beim Aktualisieren der Manager-Rollen ist ein Fehler aufgetreten.',
});
}
}
-async function handleQuestions(selectInteraction, rootInteraction, settings, roles, guildId, client, selectedRoleId) {
+async function handleQuestions(selectInteraction: StringSelectMenuInteraction, rootInteraction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient, selectedRoleId: string | null) {
let currentQuestions = settings.questions ?? [];
if (selectedRoleId) {
@@ -807,49 +826,49 @@ async function handleQuestions(selectInteraction, rootInteraction, settings, rol
const modal = new ModalBuilder()
.setCustomId('app_cfg_questions')
- .setTitle('Edit Application Questions')
+ .setTitle('Bewerbungsfragen bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('q1')
- .setLabel('Question 1 (required)')
+ .setLabel('Frage 1 (Pflicht)')
.setStyle(TextInputStyle.Short)
.setValue(currentQuestions[0] ?? '')
.setMaxLength(100)
.setMinLength(1)
.setRequired(true),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('q2')
- .setLabel('Question 2 (optional)')
+ .setLabel('Frage 2 (optional)')
.setStyle(TextInputStyle.Short)
.setValue(currentQuestions[1] ?? '')
.setMaxLength(100)
.setRequired(false),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('q3')
- .setLabel('Question 3 (optional)')
+ .setLabel('Frage 3 (optional)')
.setStyle(TextInputStyle.Short)
.setValue(currentQuestions[2] ?? '')
.setMaxLength(100)
.setRequired(false),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('q4')
- .setLabel('Question 4 (optional)')
+ .setLabel('Frage 4 (optional)')
.setStyle(TextInputStyle.Short)
.setValue(currentQuestions[3] ?? '')
.setMaxLength(100)
.setRequired(false),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('q5')
- .setLabel('Question 5 (optional)')
+ .setLabel('Frage 5 (optional)')
.setStyle(TextInputStyle.Short)
.setValue(currentQuestions[4] ?? '')
.setMaxLength(100)
@@ -874,7 +893,7 @@ async function handleQuestions(selectInteraction, rootInteraction, settings, rol
.filter(Boolean);
if (newQuestions.length === 0) {
- await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'At least one question is required.' });
+ await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'Mindestens eine Frage ist erforderlich.' });
return;
}
@@ -892,8 +911,8 @@ async function handleQuestions(selectInteraction, rootInteraction, settings, rol
await submitted.reply({
embeds: [
successEmbed(
- '✅ Questions Updated',
- `${newQuestions.length} question${newQuestions.length !== 1 ? 's' : ''} saved.`,
+ '✅ Fragen aktualisiert',
+ `${newQuestions.length} Frage${newQuestions.length !== 1 ? 'n' : ''} gespeichert.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -902,32 +921,32 @@ async function handleQuestions(selectInteraction, rootInteraction, settings, rol
await refreshDashboard(rootInteraction, settings, roles, guildId, client);
}
-async function handleRoleAdd(selectInteraction, rootInteraction, settings, roles, guildId, client) {
+async function handleRoleAdd(selectInteraction: StringSelectMenuInteraction, rootInteraction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId(`app_cfg_role_add_modal_${guildId}`)
- .setTitle('Add Application Role');
+ .setTitle('Bewerbungsrolle hinzufügen');
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('application_role')
- .setPlaceholder('Select the role members can apply for...')
+ .setPlaceholder('Rolle wählen, auf die sich Mitglieder bewerben können...')
.setMinValues(1)
.setMaxValues(1)
.setRequired(true);
const roleLabel = new LabelBuilder()
- .setLabel('Application Role')
- .setDescription('Select the Discord role members will be applying for')
+ .setLabel('Bewerbungsrolle')
+ .setDescription('Die Discord-Rolle, auf die sich Mitglieder bewerben')
.setRoleSelectMenuComponent(roleSelect);
const nameInput = new TextInputBuilder()
.setCustomId('role_name')
- .setLabel('Display name (leave blank to use role name)')
+ .setLabel('Anzeigename (leer = Rollenname)')
.setStyle(TextInputStyle.Short)
.setMaxLength(50)
.setRequired(false);
modal.addLabelComponents(roleLabel);
- modal.addComponents(new ActionRowBuilder().addComponents(nameInput));
+ modal.addComponents(new ActionRowBuilder().addComponents(nameInput));
await selectInteraction.showModal(modal);
@@ -937,12 +956,12 @@ async function handleRoleAdd(selectInteraction, rootInteraction, settings, roles
filter: i => i.user.id === selectInteraction.user.id && i.customId === `app_cfg_role_add_modal_${guildId}`,
});
- const roleId = modalSubmission.fields.getField('application_role').values[0];
- const role = selectInteraction.guild.roles.cache.get(roleId);
+ const roleId = (modalSubmission.fields.getField('application_role') as SelectMenuModalData).values[0];
+ const role = selectInteraction.guild!.roles.cache.get(roleId);
const customName = modalSubmission.fields.getTextInputValue('role_name').trim() || role?.name || roleId;
if (roles.some(r => r.roleId === roleId)) {
- await replyUserError(modalSubmission, { type: ErrorTypes.UNKNOWN, message: `${role ?? roleId} is already an application role.` });
+ await replyUserError(modalSubmission, { type: ErrorTypes.UNKNOWN, message: `${role ?? roleId} ist bereits eine Bewerbungsrolle.` });
return;
}
@@ -953,44 +972,44 @@ async function handleRoleAdd(selectInteraction, rootInteraction, settings, roles
});
await modalSubmission.reply({
- embeds: [successEmbed('Role Added', `${role ?? roleId} added as **${customName}**.`)],
+ embeds: [successEmbed('Rolle hinzugefügt', `${role ?? roleId} wurde als **${customName}** hinzugefügt.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, settings, roles, guildId, client);
} catch (error) {
- if (error.code === 'INTERACTION_TIMEOUT') return;
+ if ((error as ErrorLike).code === 'INTERACTION_TIMEOUT') return;
logger.error('Error in role add modal:', error);
await replyUserError(selectInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'An error occurred while adding the application role.',
+ message: 'Beim Hinzufügen der Bewerbungsrolle ist ein Fehler aufgetreten.',
});
}
}
-async function handleRoleRemove(selectInteraction, rootInteraction, settings, roles, guildId, client) {
+async function handleRoleRemove(selectInteraction: StringSelectMenuInteraction, rootInteraction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient) {
if (roles.length === 0) {
await replyUserError(selectInteraction, {
type: ErrorTypes.USER_INPUT,
- message: 'There are no application roles configured to remove.',
+ message: 'Es sind keine Bewerbungsrollen eingerichtet, die entfernt werden könnten.',
});
return;
}
const modal = new ModalBuilder()
.setCustomId(`app_cfg_role_remove_modal_${guildId}`)
- .setTitle('Remove Application Role');
+ .setTitle('Bewerbungsrolle entfernen');
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('remove_role')
- .setPlaceholder('Select the role to remove...')
+ .setPlaceholder('Zu entfernende Rolle auswählen...')
.setMinValues(1)
.setMaxValues(1)
.setRequired(true);
const roleLabel = new LabelBuilder()
- .setLabel('Remove Application Role')
- .setDescription('Select the role to remove from the applications list')
+ .setLabel('Bewerbungsrolle entfernen')
+ .setDescription('Rolle, die aus der Bewerbungsliste entfernt werden soll')
.setRoleSelectMenuComponent(roleSelect);
modal.addLabelComponents(roleLabel);
@@ -1003,11 +1022,11 @@ async function handleRoleRemove(selectInteraction, rootInteraction, settings, ro
filter: i => i.user.id === selectInteraction.user.id && i.customId === `app_cfg_role_remove_modal_${guildId}`,
});
- const roleId = modalSubmission.fields.getField('remove_role').values[0];
+ const roleId = (modalSubmission.fields.getField('remove_role') as SelectMenuModalData).values[0];
const index = roles.findIndex(r => r.roleId === roleId);
if (index === -1) {
- await replyUserError(modalSubmission, { type: ErrorTypes.USER_INPUT, message: `<@&${roleId}> is not in the application roles list.` });
+ await replyUserError(modalSubmission, { type: ErrorTypes.USER_INPUT, message: `<@&${roleId}> ist nicht in der Liste der Bewerbungsrollen.` });
return;
}
@@ -1015,35 +1034,35 @@ async function handleRoleRemove(selectInteraction, rootInteraction, settings, ro
await saveApplicationRoles(client, guildId, roles);
await modalSubmission.reply({
- embeds: [successEmbed('Role Removed', `<@&${roleId}> has been removed from the application roles.`)],
+ embeds: [successEmbed('Rolle entfernt', `<@&${roleId}> wurde aus den Bewerbungsrollen entfernt.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, settings, roles, guildId, client);
} catch (error) {
- if (error.code === 'INTERACTION_TIMEOUT') return;
+ if ((error as ErrorLike).code === 'INTERACTION_TIMEOUT') return;
logger.error('Error in role remove modal:', error);
await replyUserError(selectInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'An error occurred while removing the application role.',
+ message: 'Beim Entfernen der Bewerbungsrolle ist ein Fehler aufgetreten.',
});
}
}
-async function handleRetention(selectInteraction, rootInteraction, settings, roles, guildId, client) {
+async function handleRetention(selectInteraction: StringSelectMenuInteraction, rootInteraction: AppDashboardInteraction, settings: ApplicationSettings, roles: ApplicationRole[], guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('app_cfg_retention')
- .setTitle('Application Retention Periods');
+ .setTitle('Aufbewahrungsdauer für Bewerbungen');
const retentionInfo = new TextDisplayBuilder()
.setContent(
- '**Pending** — how long unanswered/in-progress applications are kept before being automatically removed.\n' +
- '**Reviewed** — how long approved or denied applications are kept.\n' +
- '-# Enter a whole number between 1 and 3650 (max 10 years).',
+ '**Offen** — wie lange unbeantwortete bzw. laufende Bewerbungen gespeichert werden, bevor sie automatisch entfernt werden.\n' +
+ '**Geprüft** — wie lange angenommene oder abgelehnte Bewerbungen gespeichert werden.\n' +
+ '-# Gib eine ganze Zahl zwischen 1 und 3650 ein (max. 10 Jahre).',
);
const pendingLabel = new LabelBuilder()
- .setLabel('Pending retention (days)')
+ .setLabel('Aufbewahrung offen (Tage)')
.setTextInputComponent(
new TextInputBuilder()
.setCustomId('pending_days')
@@ -1055,7 +1074,7 @@ async function handleRetention(selectInteraction, rootInteraction, settings, rol
);
const reviewedLabel = new LabelBuilder()
- .setLabel('Reviewed retention (days)')
+ .setLabel('Aufbewahrung geprüft (Tage)')
.setTextInputComponent(
new TextInputBuilder()
.setCustomId('reviewed_days')
@@ -1086,12 +1105,12 @@ async function handleRetention(selectInteraction, rootInteraction, settings, rol
const reviewedDays = parseInt(submitted.fields.getTextInputValue('reviewed_days').trim(), 10);
if (isNaN(pendingDays) || pendingDays < 1 || pendingDays > 3650) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Pending retention must be a whole number between **1** and **3650** days.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Die Aufbewahrung für offene Bewerbungen muss eine ganze Zahl zwischen **1** und **3650** Tagen sein.' });
return;
}
if (isNaN(reviewedDays) || reviewedDays < 1 || reviewedDays > 3650) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Reviewed retention must be a whole number between **1** and **3650** days.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Die Aufbewahrung für geprüfte Bewerbungen muss eine ganze Zahl zwischen **1** und **3650** Tagen sein.' });
return;
}
@@ -1102,8 +1121,8 @@ async function handleRetention(selectInteraction, rootInteraction, settings, rol
await submitted.reply({
embeds: [
successEmbed(
- '✅ Retention Updated',
- `Pending applications will be kept for **${pendingDays} days**.\nReviewed applications will be kept for **${reviewedDays} days**.`,
+ '✅ Aufbewahrung aktualisiert',
+ `Offene Bewerbungen werden **${pendingDays} Tage** gespeichert.\nGeprüfte Bewerbungen werden **${reviewedDays} Tage** gespeichert.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -1112,12 +1131,12 @@ async function handleRetention(selectInteraction, rootInteraction, settings, rol
await refreshDashboard(rootInteraction, settings, roles, guildId, client);
}
-async function handleDeleteApplication(confirmSubmit, selectedRoleId, guildId, roles, client) {
+async function handleDeleteApplication(confirmSubmit: ModalSubmitInteraction, selectedRoleId: string, guildId: string, roles: ApplicationRole[], client: NexoClient) {
try {
const roleIndex = roles.findIndex(r => r.roleId === selectedRoleId);
if (roleIndex === -1) {
- await replyUserError(confirmSubmit, { type: ErrorTypes.USER_INPUT, message: 'Application role not found.' });
+ await replyUserError(confirmSubmit, { type: ErrorTypes.USER_INPUT, message: 'Bewerbungsrolle nicht gefunden.' });
return;
}
@@ -1139,9 +1158,9 @@ async function handleDeleteApplication(confirmSubmit, selectedRoleId, guildId, r
await confirmSubmit.reply({
embeds: [
successEmbed(
- '🗑️ Application Deleted',
- `The application for <@&${selectedRoleId}> (**${deletedRole.name}**) has been permanently deleted.\n\n` +
- `Deleted: **${applicationsToDelete.length}** application${applicationsToDelete.length !== 1 ? 's' : ''}`,
+ '🗑️ Bewerbung gelöscht',
+ `Die Bewerbung für <@&${selectedRoleId}> (**${deletedRole.name}**) wurde endgültig gelöscht.\n\n` +
+ `Gelöscht: **${applicationsToDelete.length}** Bewerbung${applicationsToDelete.length !== 1 ? 'en' : ''}`,
),
],
flags: MessageFlags.Ephemeral,
@@ -1149,6 +1168,6 @@ async function handleDeleteApplication(confirmSubmit, selectedRoleId, guildId, r
} catch (error) {
logger.error('Error in handleDeleteApplication:', error);
- await replyUserError(confirmSubmit, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while deleting the application. Please try again.' });
+ await replyUserError(confirmSubmit, { type: ErrorTypes.UNKNOWN, message: 'Beim Löschen der Bewerbung ist ein Fehler aufgetreten. Bitte versuch es erneut.' });
}
}
\ No newline at end of file
diff --git a/src/commands/Core/bot.ts b/src/commands/Core/bot.ts
new file mode 100644
index 0000000000..ce2c8fcf3f
--- /dev/null
+++ b/src/commands/Core/bot.ts
@@ -0,0 +1,17 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import pingCommand from './modules/ping.js';
+import statsCommand from './modules/stats.js';
+import supportCommand from './modules/support.js';
+import uptimeCommand from './modules/uptime.js';
+
+export default createRouterCommand({
+ name: 'bot',
+ description: 'Status und Infos zum Bot',
+ category: 'Core',
+ routes: [
+ { legacyName: 'ping', module: pingCommand },
+ { legacyName: 'stats', module: statsCommand },
+ { legacyName: 'support', module: supportCommand },
+ { legacyName: 'uptime', module: uptimeCommand },
+ ],
+});
diff --git a/src/commands/Core/commands.js b/src/commands/Core/commands.ts
similarity index 72%
rename from src/commands/Core/commands.js
rename to src/commands/Core/commands.ts
index 579cbf2bd3..eae6fde6da 100644
--- a/src/commands/Core/commands.js
+++ b/src/commands/Core/commands.ts
@@ -3,6 +3,8 @@ import {
PermissionFlagsBits,
MessageFlags,
} from 'discord.js';
+import type { APIActionRowComponent, APIComponentInMessageActionRow, ChatInputCommandInteraction } from 'discord.js';
+import type { Command, NexoClient } from '../../types/index.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import { successEmbed } from '../../utils/embeds.js';
import { logger } from '../../utils/logger.js';
@@ -25,7 +27,7 @@ import {
const DASHBOARD_TIMEOUT_MS = 10 * 60 * 1000;
-function buildCategoryChoices(client) {
+function buildCategoryChoices(client: NexoClient) {
const registry = buildCommandRegistry(client);
return [...registry.values()]
.sort((a, b) => a.displayName.localeCompare(b.displayName))
@@ -36,9 +38,9 @@ function buildCategoryChoices(client) {
}));
}
-async function ensureManageGuild(interaction) {
+async function ensureManageGuild(interaction: ChatInputCommandInteraction) {
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
- await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the **Manage Server** permission to manage commands.' });
+ await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um Commands zu verwalten.' });
return false;
}
@@ -48,32 +50,32 @@ async function ensureManageGuild(interaction) {
export default {
data: new SlashCommandBuilder()
.setName('commands')
- .setDescription('Enable or disable bot commands and categories for this server')
+ .setDescription('Commands und Kategorien des Bots für diesen Server aktivieren oder deaktivieren')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.setDMPermission(false)
.addSubcommand((subcommand) =>
subcommand
.setName('dashboard')
- .setDescription('Open the interactive command access dashboard'),
+ .setDescription('Öffnet das interaktive Dashboard für Command-Zugriff'),
)
.addSubcommand((subcommand) =>
subcommand
.setName('disable')
- .setDescription('Disable a command or entire category')
+ .setDescription('Einen Command oder eine ganze Kategorie deaktivieren')
.addStringOption((option) =>
option
.setName('scope')
- .setDescription('Disable a single command or a whole category')
+ .setDescription('Einen einzelnen Command oder eine ganze Kategorie deaktivieren')
.setRequired(true)
.addChoices(
- { name: 'Category', value: 'category' },
+ { name: 'Kategorie', value: 'category' },
{ name: 'Command', value: 'command' },
),
)
.addStringOption((option) =>
option
.setName('target')
- .setDescription('Category or command name')
+ .setDescription('Name der Kategorie oder des Commands')
.setRequired(true)
.setAutocomplete(true),
),
@@ -81,21 +83,21 @@ export default {
.addSubcommand((subcommand) =>
subcommand
.setName('enable')
- .setDescription('Enable a command or entire category')
+ .setDescription('Einen Command oder eine ganze Kategorie aktivieren')
.addStringOption((option) =>
option
.setName('scope')
- .setDescription('Enable a single command or a whole category')
+ .setDescription('Einen einzelnen Command oder eine ganze Kategorie aktivieren')
.setRequired(true)
.addChoices(
- { name: 'Category', value: 'category' },
+ { name: 'Kategorie', value: 'category' },
{ name: 'Command', value: 'command' },
),
)
.addStringOption((option) =>
option
.setName('target')
- .setDescription('Category or command name')
+ .setDescription('Name der Kategorie oder des Commands')
.setRequired(true)
.setAutocomplete(true),
),
@@ -121,7 +123,7 @@ export default {
// For command scope, get all commands including subcommands
const registry = buildCommandRegistry(interaction.client);
- const allCommands = [];
+ const allCommands: string[] = [];
// Check if the query matches a category name - if so, show commands from that category
const matchedCategory = resolveCategoryChoice(interaction.client, query);
@@ -166,7 +168,7 @@ export default {
return;
}
- const view = await buildDashboardView(client, interaction.guildId, interaction.guild, 'overview');
+ const view = await buildDashboardView(client, interaction.guildId!, interaction.guild!, 'overview');
await InteractionHelper.safeEditReply(interaction, {
embeds: [view.embed],
components: view.components,
@@ -190,21 +192,21 @@ export default {
await handleDashboardComponent(componentInteraction, client);
} catch (error) {
logger.error('Command access dashboard interaction failed', {
- error: error.message,
+ error: (error as Error).message,
customId: componentInteraction.customId,
guildId: interaction.guildId,
});
await replyUserError(componentInteraction, {
type: ErrorTypes.UNKNOWN,
- message: error.message || 'Failed to update command access.',
+ message: (error as Error).message || 'Command-Zugriff konnte nicht aktualisiert werden.',
}).catch(() => {});
}
});
collector.on('end', async () => {
- const finalView = await buildDashboardView(client, interaction.guildId, interaction.guild, 'overview');
+ const finalView = await buildDashboardView(client, interaction.guildId!, interaction.guild!, 'overview');
const disabledComponents = finalView.components.map((row) => {
- const newRow = row.toJSON();
+ const newRow: APIActionRowComponent = row.toJSON();
newRow.components = newRow.components.map((component) => ({ ...component, disabled: true }));
return newRow;
});
@@ -227,38 +229,38 @@ export default {
if (scope === 'category') {
const category = resolveCategoryChoice(client, target);
if (!category) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `No category matched \`${target}\`. Use \`/commands dashboard\` to browse categories.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Keine Kategorie passt zu \`${target}\`. Nutze \`/commands dashboard\`, um die Kategorien zu durchsuchen.` });
}
if (isDisable) {
- await disableCategory(client, interaction.guildId, category.key);
+ await disableCategory(client, interaction.guildId!, category.key);
return InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- 'Category Disabled',
- `All **${category.displayName}** commands are now disabled.\nProtected commands remain available.`,
+ 'Kategorie deaktiviert',
+ `Alle Commands aus **${category.displayName}** sind jetzt deaktiviert.\nGeschützte Commands bleiben verfügbar.`,
),
],
});
}
- await enableCategory(client, interaction.guildId, category.key);
+ await enableCategory(client, interaction.guildId!, category.key);
return InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed('Category Enabled', `**${category.displayName}** commands are now enabled (except individually disabled commands).`)],
+ embeds: [successEmbed('Kategorie aktiviert', `Die Commands aus **${category.displayName}** sind jetzt aktiviert (außer einzeln deaktivierte Commands).`)],
});
}
- const commandName = target.toLowerCase();
+ const commandName = target!.toLowerCase();
if (isDisable) {
- await disableCommand(client, interaction.guildId, commandName);
+ const result = await disableCommand(client, interaction.guildId!, commandName);
return InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed('Command Disabled', `\`/${commandName}\` is now disabled in this server.`)],
+ embeds: [successEmbed('Command deaktiviert', `\`/${result.commandName}\` ist auf diesem Server jetzt deaktiviert.`)],
});
}
- await enableCommand(client, interaction.guildId, commandName);
+ const result = await enableCommand(client, interaction.guildId!, commandName);
return InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed('Command Enabled', `\`/${commandName}\` is now enabled in this server.`)],
+ embeds: [successEmbed('Command aktiviert', `\`/${result.commandName}\` ist auf diesem Server jetzt aktiviert.`)],
});
},
-};
+} satisfies Command;
diff --git a/src/commands/Core/configWizard.js b/src/commands/Core/configWizard.ts
similarity index 59%
rename from src/commands/Core/configWizard.js
rename to src/commands/Core/configWizard.ts
index 22ac013ec0..c466600500 100644
--- a/src/commands/Core/configWizard.js
+++ b/src/commands/Core/configWizard.ts
@@ -15,6 +15,17 @@ import {
LabelBuilder,
ChannelType,
} from 'discord.js';
+import type {
+ ButtonInteraction,
+ ChatInputCommandInteraction,
+ DMChannel,
+ Guild,
+ MessageComponentInteraction,
+ ModalSubmitInteraction,
+ SelectMenuModalData,
+ StringSelectMenuInteraction,
+} from 'discord.js';
+import type { Command, GuildConfig, NexoClient } from '../../types/index.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import { createEmbed, successEmbed, infoEmbed, warningEmbed, buildUserErrorEmbed } from '../../utils/embeds.js';
import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
@@ -23,45 +34,55 @@ import ConfigService from '../../services/config/configService.js';
import { logger } from '../../utils/logger.js';
import { botConfig, getCommandPrefix } from '../../config/bot.js';
+type WizardAnswer = { cancelled: true; answer?: undefined } | { answer: string; cancelled?: undefined };
+
+interface WizardPrompt {
+ key: string;
+ skipMessage: string;
+ question: string;
+ /** undefined = skip, null = clear */
+ parse(answer: string): Promise;
+}
+
const DASHBOARD_CUSTOM_ID = 'config_select';
const WIZARD_BUTTON_ID = 'config_wizard';
-const activeWizardSessions = new Set();
+const activeWizardSessions = new Set();
const DM_DISABLED_HELP = [
- '1. Right-click this server\'s name (mobile: tap the server name at the top).',
- '2. Open **Privacy Settings**.',
- '3. Turn on **Allow direct messages from server members**.',
- '4. Click **Start Setup Wizard** again.',
+ '1. Rechtsklick auf den Servernamen (mobil: oben auf den Servernamen tippen).',
+ '2. Öffne die **Privatsphäre-Einstellungen**.',
+ '3. Aktiviere **Direktnachrichten von Servermitgliedern erlauben**.',
+ '4. Klicke erneut auf **Einrichtung starten**.',
].join('\n');
-async function notifyWizardStarted(buttonInteraction) {
+async function notifyWizardStarted(buttonInteraction: ButtonInteraction) {
await buttonInteraction.followUp({
embeds: [infoEmbed(
- 'Setup Wizard Started',
- 'Check your DMs — I sent you the first setup question there.\n\nAnswer each question in that DM. Type `skip` to keep the current value.',
+ 'Einrichtung gestartet',
+ 'Schau in deine DMs — dort findest du die erste Frage zur Einrichtung.\n\nBeantworte jede Frage in dieser DM. Schreib `skip`, um den aktuellen Wert zu behalten.',
)],
flags: MessageFlags.Ephemeral,
}).catch(() => {});
}
-async function notifyWizardDmBlocked(buttonInteraction) {
+async function notifyWizardDmBlocked(buttonInteraction: ButtonInteraction) {
await replyUserError(buttonInteraction, {
type: ErrorTypes.USER_INPUT,
- message: `I couldn't send you a DM. Enable DMs from this server, then try again.\n\n${DM_DISABLED_HELP}`,
+ message: `Ich konnte dir keine DM schicken. Erlaube DMs von diesem Server und versuch es dann erneut.\n\n${DM_DISABLED_HELP}`,
}).catch(() => {});
}
-function formatChannelMention(guild, channelId) {
+function formatChannelMention(guild: Guild, channelId: string | null | undefined) {
if (!channelId) {
- return '`Not set`';
+ return '`Nicht festgelegt`';
}
const channel = guild.channels.cache.get(channelId);
return channel ? `<#${channelId}>` : `#${channelId}`;
}
-function formatRoleMention(guild, roleId) {
+function formatRoleMention(guild: Guild, roleId: string | null | undefined) {
if (!roleId) {
- return '`Not set`';
+ return '`Nicht festgelegt`';
}
const role = guild.roles.cache.get(roleId);
return role ? `<@&${roleId}>` : `@${roleId}`;
@@ -70,10 +91,10 @@ function formatRoleMention(guild, roleId) {
function getBotPresenceText() {
const activity = botConfig.presence?.activities?.[0];
if (!activity?.name) {
- return '`Not configured`';
+ return '`Nicht konfiguriert`';
}
- const typeLabels = ['Playing', 'Streaming', 'Listening to', 'Watching', '', 'Competing in'];
+ const typeLabels = ['Spielt', 'Streamt', 'Hört', 'Schaut', '', 'Tritt an in'];
const typeLabel = typeLabels[activity.type];
if (!typeLabel) {
return activity.name;
@@ -85,97 +106,97 @@ function getBotPresenceText() {
function getThemeColorLines() {
const colors = botConfig.embeds.colors;
return [
- `🎨 Primary \`${colors.primary}\` · Success \`${colors.success}\``,
- `⚠️ Warning \`${colors.warning}\` · Error \`${colors.error}\``,
+ `🎨 Primär \`${colors.primary}\` · Erfolg \`${colors.success}\``,
+ `⚠️ Warnung \`${colors.warning}\` · Fehler \`${colors.error}\``,
].join('\n');
}
-function buildDashboardEmbed(config, guild) {
+function buildDashboardEmbed(config: GuildConfig, guild: Guild) {
const setupDone = config.setupWizardCompleted;
return createEmbed({
- title: '⚙️ Server Configuration',
- description: `Core settings for **${guild.name}**. Pick an option below or run the setup wizard.`,
+ title: '⚙️ Server-Konfiguration',
+ description: `Grundeinstellungen für **${guild.name}**. Wähle unten eine Option oder starte die Einrichtung.`,
color: 'info',
fields: [
{
- name: '⌨️ Server Prefix',
+ name: '⌨️ Server-Prefix',
value: `\`${config.prefix || getCommandPrefix()}\``,
inline: true,
},
{
- name: '🛡️ Moderator Role',
+ name: '🛡️ Moderator-Rolle',
value: formatRoleMention(guild, config.modRole),
inline: true,
},
{
- name: '📋 Log Channel',
+ name: '📋 Log-Kanal',
value: formatChannelMention(guild, config.logging?.channels?.audit),
inline: true,
},
{
- name: '💚 Bot Status',
+ name: '💚 Bot-Status',
value: getBotPresenceText(),
inline: false,
},
{
- name: '🎨 Embed Theme',
- value: `${getThemeColorLines()}\n-# Colors are set in bot config and apply globally.`,
+ name: '🎨 Embed-Design',
+ value: `${getThemeColorLines()}\n-# Die Farben werden in der Bot-Konfiguration festgelegt und gelten global.`,
inline: false,
},
{
- name: '⚡ Command Access',
- value: 'Use `/commands dashboard` to enable or disable commands and subcommands.',
+ name: '⚡ Command-Zugriff',
+ value: 'Mit `/commands dashboard` aktivierst oder deaktivierst du Commands und Subcommands.',
inline: false,
},
{
- name: `${setupDone ? '✅' : '📝'} Setup`,
+ name: `${setupDone ? '✅' : '📝'} Einrichtung`,
value: setupDone
- ? 'Setup wizard completed — re-run anytime to update settings.'
- : 'Run the setup wizard to configure your server quickly.',
+ ? 'Einrichtung abgeschlossen — du kannst sie jederzeit erneut starten, um Einstellungen zu ändern.'
+ : 'Starte die Einrichtung, um deinen Server schnell zu konfigurieren.',
inline: false,
},
],
- footer: 'Dashboard closes after 10 minutes of inactivity',
+ footer: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität',
});
}
-function buildSettingsSelect(guildId) {
- return new ActionRowBuilder().addComponents(
+function buildSettingsSelect(guildId: string) {
+ return new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId(`${DASHBOARD_CUSTOM_ID}:${guildId}`)
- .setPlaceholder('⚙️ Select a setting to edit...')
+ .setPlaceholder('⚙️ Einstellung zum Bearbeiten auswählen...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Server Prefix')
- .setDescription('Change the text command prefix')
+ .setLabel('Server-Prefix')
+ .setDescription('Prefix für Text-Commands ändern')
.setValue('prefix')
.setEmoji('⌨️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Moderator Role')
- .setDescription('Role used for moderation commands')
+ .setLabel('Moderator-Rolle')
+ .setDescription('Rolle für Moderations-Commands')
.setValue('modRole')
.setEmoji('🛡️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Log Channel')
- .setDescription('Channel for system log messages')
+ .setLabel('Log-Kanal')
+ .setDescription('Kanal für System-Log-Nachrichten')
.setValue('logChannelId')
.setEmoji('📋'),
),
);
}
-function buildButtonRow(config, guildId) {
- return new ActionRowBuilder().addComponents(
+function buildButtonRow(config: GuildConfig, guildId: string) {
+ return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`${WIZARD_BUTTON_ID}:${guildId}`)
- .setLabel(config.setupWizardCompleted ? 'Re-run Setup Wizard' : 'Start Setup Wizard')
+ .setLabel(config.setupWizardCompleted ? 'Einrichtung erneut starten' : 'Einrichtung starten')
.setEmoji('📝')
.setStyle(config.setupWizardCompleted ? ButtonStyle.Secondary : ButtonStyle.Success),
);
}
-function extractId(value) {
+function extractId(value: unknown) {
if (!value || typeof value !== 'string') return null;
const channelMention = value.match(/<#!?(\d{17,19})>/);
@@ -190,10 +211,10 @@ function extractId(value) {
return null;
}
-async function askQuestion(dmChannel, userId, prompt, stepNumber, totalSteps) {
+async function askQuestion(dmChannel: DMChannel, userId: string, prompt: string, stepNumber: number, totalSteps: number): Promise {
await dmChannel.send({
embeds: [createEmbed({
- title: `Setup Question ${stepNumber}/${totalSteps}`,
+ title: `Einrichtung – Frage ${stepNumber}/${totalSteps}`,
description: prompt,
color: 'primary',
})],
@@ -207,15 +228,15 @@ async function askQuestion(dmChannel, userId, prompt, stepNumber, totalSteps) {
if (!collected || !collected.size) {
await dmChannel.send({
- embeds: [buildUserErrorEmbed(ErrorTypes.RATE_LIMIT, 'You did not answer in time. Run the setup wizard again when ready.')],
+ embeds: [buildUserErrorEmbed(ErrorTypes.RATE_LIMIT, 'Du hast nicht rechtzeitig geantwortet. Starte die Einrichtung erneut, wenn du so weit bist.')],
});
return null;
}
- const answer = collected.first().content.trim();
+ const answer = collected.first()!.content.trim();
if (answer.toLowerCase() === 'cancel') {
await dmChannel.send({
- embeds: [infoEmbed('Setup Cancelled', 'Setup wizard stopped. Your saved answers are still applied.')],
+ embeds: [infoEmbed('Einrichtung abgebrochen', 'Die Einrichtung wurde beendet. Deine bereits gespeicherten Antworten bleiben übernommen.')],
});
return { cancelled: true };
}
@@ -223,58 +244,58 @@ async function askQuestion(dmChannel, userId, prompt, stepNumber, totalSteps) {
return { answer };
}
-function formatSavedAck(key, value, guild) {
+function formatSavedAck(key: string, value: string | null, guild: Guild) {
if (key === 'prefix') {
- return `Server prefix saved as \`${value}\`.`;
+ return `Server-Prefix gespeichert: \`${value}\`.`;
}
if (key === 'logChannelId') {
if (value === null) {
- return 'Log channel cleared.';
+ return 'Log-Kanal entfernt.';
}
const channel = guild.channels.cache.get(value);
- return `Log channel saved as ${channel ?? `<#${value}>`}.`;
+ return `Log-Kanal gespeichert: ${channel ?? `<#${value}>`}.`;
}
if (key === 'modRole') {
if (value === null) {
- return 'Moderator role cleared.';
+ return 'Moderator-Rolle entfernt.';
}
const role = guild.roles.cache.get(value);
- return `Moderator role saved as ${role ?? `<@&${value}>`}.`;
+ return `Moderator-Rolle gespeichert: ${role ?? `<@&${value}>`}.`;
}
- return 'Setting saved.';
+ return 'Einstellung gespeichert.';
}
-async function validateGuildChannelId(guild, channelId) {
+async function validateGuildChannelId(guild: Guild, channelId: string) {
const channel = guild.channels.cache.get(channelId) ?? await guild.channels.fetch(channelId).catch(() => null);
if (!channel || !channel.isTextBased()) {
- throw new Error('That channel was not found in this server or is not a text channel.');
+ throw new Error('Dieser Kanal wurde auf diesem Server nicht gefunden oder ist kein Textkanal.');
}
return channel.id;
}
-async function validateGuildRoleId(guild, roleId) {
+async function validateGuildRoleId(guild: Guild, roleId: string) {
const role = guild.roles.cache.get(roleId) ?? await guild.roles.fetch(roleId).catch(() => null);
if (!role) {
- throw new Error('That role was not found in this server.');
+ throw new Error('Diese Rolle wurde auf diesem Server nicht gefunden.');
}
return role.id;
}
-async function refreshDashboard(rootInteraction, config, guild) {
+async function refreshDashboard(rootInteraction: ChatInputCommandInteraction, config: GuildConfig, guild: Guild) {
const embed = buildDashboardEmbed(config, guild);
const components = [buildButtonRow(config, guild.id), buildSettingsSelect(guild.id)];
await InteractionHelper.safeEditReply(rootInteraction, { embeds: [embed], components }).catch(() => {});
}
-async function runSetupWizard(buttonInteraction, config, guild, client, rootInteraction) {
+async function runSetupWizard(buttonInteraction: ButtonInteraction, config: GuildConfig, guild: Guild, client: NexoClient, rootInteraction: ChatInputCommandInteraction) {
const user = buttonInteraction.user;
if (activeWizardSessions.has(user.id)) {
await buttonInteraction.followUp({
- embeds: [warningEmbed('Setup Already Running', 'You already have a setup wizard open in your DMs. Reply there to continue, or type `cancel` to stop it.')],
+ embeds: [warningEmbed('Einrichtung läuft bereits', 'Du hast bereits eine Einrichtung in deinen DMs offen. Antworte dort, um weiterzumachen, oder schreib `cancel`, um sie zu beenden.')],
flags: MessageFlags.Ephemeral,
}).catch(() => {});
return;
@@ -282,12 +303,12 @@ async function runSetupWizard(buttonInteraction, config, guild, client, rootInte
activeWizardSessions.add(user.id);
- let dmChannel;
+ let dmChannel: DMChannel | undefined;
try {
dmChannel = await user.createDM();
} catch (error) {
- logger.warn('Failed to create DM channel for setup wizard', { userId: user.id, error: error.message });
+ logger.warn('Failed to create DM channel for setup wizard', { userId: user.id, error: (error as Error).message });
await notifyWizardDmBlocked(buttonInteraction);
return;
} finally {
@@ -296,63 +317,63 @@ async function runSetupWizard(buttonInteraction, config, guild, client, rootInte
}
}
- const prompts = [
+ const prompts: WizardPrompt[] = [
{
key: 'prefix',
- skipMessage: 'Keeping the current server prefix.',
- question: 'What command prefix should this server use?\nCurrent: `' + (config.prefix || getCommandPrefix()) + '`\nReply `skip` to keep it, or `cancel` to stop.',
+ skipMessage: 'Der aktuelle Server-Prefix bleibt.',
+ question: 'Welchen Command-Prefix soll dieser Server nutzen?\nAktuell: `' + (config.prefix || getCommandPrefix()) + '`\nAntworte mit `skip`, um ihn zu behalten, oder `cancel`, um abzubrechen.',
parse: async (answer) => {
const normalized = answer.trim();
if (normalized.toLowerCase() === 'skip') return undefined;
if (/\s/.test(normalized) || normalized.length < 1 || normalized.length > 10) {
- throw new Error('Prefix must be 1-10 characters with no spaces.');
+ throw new Error('Der Prefix muss 1–10 Zeichen lang sein und darf keine Leerzeichen enthalten.');
}
return normalized;
},
},
{
key: 'logChannelId',
- skipMessage: 'Keeping the current log channel.',
- question: 'Which channel should receive bot logs?\nSend a channel mention, channel ID, `none` to clear, `skip` to keep the current value, or `cancel` to stop.',
+ skipMessage: 'Der aktuelle Log-Kanal bleibt.',
+ question: 'In welchen Kanal sollen die Bot-Logs gehen?\nSchick eine Kanal-Erwähnung oder Kanal-ID, `none` zum Entfernen, `skip` zum Behalten oder `cancel` zum Abbrechen.',
parse: async (answer) => {
const normalized = answer.trim();
if (normalized.toLowerCase() === 'skip') return undefined;
if (normalized.toLowerCase() === 'none') return null;
const id = extractId(normalized);
- if (!id) throw new Error('Provide a valid channel mention or ID from this server.');
+ if (!id) throw new Error('Gib eine gültige Kanal-Erwähnung oder Kanal-ID von diesem Server an.');
return validateGuildChannelId(guild, id);
},
},
{
key: 'modRole',
- skipMessage: 'Keeping the current moderator role.',
- question: 'What role should moderators have?\nSend a role mention, role ID, `none` to clear, `skip` to keep the current value, or `cancel` to stop.',
+ skipMessage: 'Die aktuelle Moderator-Rolle bleibt.',
+ question: 'Welche Rolle sollen Moderatoren haben?\nSchick eine Rollen-Erwähnung oder Rollen-ID, `none` zum Entfernen, `skip` zum Behalten oder `cancel` zum Abbrechen.',
parse: async (answer) => {
const normalized = answer.trim();
if (normalized.toLowerCase() === 'skip') return undefined;
if (normalized.toLowerCase() === 'none') return null;
const id = extractId(normalized);
- if (!id) throw new Error('Provide a valid role mention or ID from this server.');
+ if (!id) throw new Error('Gib eine gültige Rollen-Erwähnung oder Rollen-ID von diesem Server an.');
return validateGuildRoleId(guild, id);
},
},
];
- const changes = {};
- const errors = [];
+ const changes: Record = {};
+ const errors: string[] = [];
let wizardCancelled = false;
try {
try {
await dmChannel.send({
embeds: [createEmbed({
- title: '📝 Setup Wizard',
- description: 'Answer each question in this DM.\n\n• Type `skip` to keep the current value\n• Type `cancel` to stop the wizard',
+ title: '📝 Einrichtung',
+ description: 'Beantworte jede Frage in dieser DM.\n\n• Schreib `skip`, um den aktuellen Wert zu behalten\n• Schreib `cancel`, um die Einrichtung abzubrechen',
color: 'info',
})],
});
} catch (error) {
- logger.warn('Failed to send setup wizard DM', { userId: user.id, error: error.message });
+ logger.warn('Failed to send setup wizard DM', { userId: user.id, error: (error as Error).message });
await notifyWizardDmBlocked(buttonInteraction);
return;
}
@@ -389,28 +410,28 @@ async function runSetupWizard(buttonInteraction, config, guild, client, rootInte
if (value === undefined) {
await dmChannel.send({
- embeds: [infoEmbed('Skipped', prompt.skipMessage)],
+ embeds: [infoEmbed('Übersprungen', prompt.skipMessage)],
});
} else {
await ConfigService.updateSetting(client, guild.id, prompt.key, value, user.id);
changes[prompt.key] = value;
await dmChannel.send({
- embeds: [successEmbed('Saved', formatSavedAck(prompt.key, value, guild))],
+ embeds: [successEmbed('Gespeichert', formatSavedAck(prompt.key, value, guild))],
});
try {
const updatedConfig = await getGuildConfig(client, guild.id);
await refreshDashboard(rootInteraction, updatedConfig, guild);
} catch (refreshError) {
- logger.debug('Failed to refresh dashboard during setup wizard', { error: refreshError.message });
+ logger.debug('Failed to refresh dashboard during setup wizard', { error: (refreshError as Error).message });
}
}
answered = true;
} catch (error) {
- errors.push(`• ${prompt.key}: ${error.message}`);
+ errors.push(`• ${prompt.key}: ${(error as Error).message}`);
await dmChannel.send({
- embeds: [buildUserErrorEmbed(ErrorTypes.VALIDATION, `${error.message}\n\nPlease reply again with a valid answer, \`skip\`, or \`cancel\`.`)],
+ embeds: [buildUserErrorEmbed(ErrorTypes.VALIDATION, `${(error as Error).message}\n\nBitte antworte erneut mit einer gültigen Antwort, \`skip\` oder \`cancel\`.`)],
});
}
}
@@ -424,21 +445,21 @@ async function runSetupWizard(buttonInteraction, config, guild, client, rootInte
try {
await setConfigValue(client, guild.id, 'setupWizardCompleted', true);
} catch (error) {
- logger.warn('Failed to persist setupWizardCompleted flag', { guildId: guild.id, error: error.message });
+ logger.warn('Failed to persist setupWizardCompleted flag', { guildId: guild.id, error: (error as Error).message });
}
}
const summaryTitle = wizardCancelled
- ? (Object.keys(changes).length > 0 ? 'Setup Stopped' : 'Setup Cancelled')
- : (errors.length > 0 ? 'Setup Complete' : 'Setup Complete');
+ ? (Object.keys(changes).length > 0 ? 'Einrichtung beendet' : 'Einrichtung abgebrochen')
+ : (errors.length > 0 ? 'Einrichtung abgeschlossen' : 'Einrichtung abgeschlossen');
const summaryBody = wizardCancelled
? (Object.keys(changes).length > 0
- ? `Setup stopped early. Saved **${Object.keys(changes).length}** setting(s) before stopping.`
- : 'Setup wizard stopped before any changes were saved.')
+ ? `Einrichtung vorzeitig beendet. Bis dahin wurden **${Object.keys(changes).length}** Einstellung(en) gespeichert.`
+ : 'Die Einrichtung wurde beendet, bevor Änderungen gespeichert wurden.')
: (Object.keys(changes).length > 0
- ? `Updated **${Object.keys(changes).length}** setting(s).${errors.length > 0 ? ' Some answers needed retries.' : ''}`
- : 'No changes were applied.');
+ ? `**${Object.keys(changes).length}** Einstellung(en) aktualisiert.${errors.length > 0 ? ' Einige Antworten mussten wiederholt werden.' : ''}`
+ : 'Es wurden keine Änderungen übernommen.');
const summaryEmbed = createEmbed({
title: wizardCancelled ? `⚠️ ${summaryTitle}` : `✅ ${summaryTitle}`,
@@ -448,7 +469,7 @@ async function runSetupWizard(buttonInteraction, config, guild, client, rootInte
if (errors.length > 0) {
const uniqueErrors = [...new Set(errors)];
- summaryEmbed.addFields({ name: 'Issues', value: uniqueErrors.join('\n').slice(0, 1024) });
+ summaryEmbed.addFields({ name: 'Probleme', value: uniqueErrors.join('\n').slice(0, 1024) });
}
await dmChannel.send({ embeds: [summaryEmbed] });
@@ -457,32 +478,32 @@ async function runSetupWizard(buttonInteraction, config, guild, client, rootInte
const updatedConfig = await getGuildConfig(client, guild.id);
await refreshDashboard(rootInteraction, updatedConfig, guild);
} catch (error) {
- logger.debug('Failed to refresh dashboard after wizard completion', { error: error.message });
+ logger.debug('Failed to refresh dashboard after wizard completion', { error: (error as Error).message });
}
} finally {
activeWizardSessions.delete(user.id);
}
}
-async function showSettingModal(selectInteraction, guildId, setting) {
+async function showSettingModal(selectInteraction: StringSelectMenuInteraction, guildId: string, setting: string) {
const modalCustomId = `config_wizard_modal:${setting}:${guildId}`;
if (setting === 'logChannelId') {
const modal = new ModalBuilder()
.setCustomId(modalCustomId)
- .setTitle('📋 Update Log Channel');
+ .setTitle('📋 Log-Kanal ändern');
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('log_channel')
- .setPlaceholder('Select a text channel...')
+ .setPlaceholder('Textkanal auswählen...')
.setMinValues(1)
.setMaxValues(1)
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
.setRequired(true);
const channelLabel = new LabelBuilder()
- .setLabel('Log Channel')
- .setDescription('Channel where system log messages will be sent')
+ .setLabel('Log-Kanal')
+ .setDescription('Kanal, in den System-Log-Nachrichten gesendet werden')
.setChannelSelectMenuComponent(channelSelect);
modal.addLabelComponents(channelLabel);
@@ -493,18 +514,18 @@ async function showSettingModal(selectInteraction, guildId, setting) {
if (setting === 'modRole') {
const modal = new ModalBuilder()
.setCustomId(modalCustomId)
- .setTitle('🛡️ Update Moderator Role');
+ .setTitle('🛡️ Moderator-Rolle ändern');
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('mod_role')
- .setPlaceholder('Select a moderator role...')
+ .setPlaceholder('Moderator-Rolle auswählen...')
.setMinValues(1)
.setMaxValues(1)
.setRequired(true);
const roleLabel = new LabelBuilder()
- .setLabel('Moderator Role')
- .setDescription('Role used for moderation commands')
+ .setLabel('Moderator-Rolle')
+ .setDescription('Rolle für Moderations-Commands')
.setRoleSelectMenuComponent(roleSelect);
modal.addLabelComponents(roleLabel);
@@ -514,59 +535,59 @@ async function showSettingModal(selectInteraction, guildId, setting) {
const modal = new ModalBuilder()
.setCustomId(modalCustomId)
- .setTitle('Update Server Prefix');
+ .setTitle('Server-Prefix ändern');
const textInput = new TextInputBuilder()
.setCustomId('value')
- .setLabel('New prefix (1-10 characters, no spaces)')
+ .setLabel('Neuer Prefix (1–10 Zeichen, ohne Leerzeichen)')
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setMinLength(1)
.setMaxLength(10);
- modal.addComponents(new ActionRowBuilder().addComponents(textInput));
+ modal.addComponents(new ActionRowBuilder().addComponents(textInput));
await selectInteraction.showModal(modal);
}
-function resolveSettingModalValue(setting, submitted) {
+function resolveSettingModalValue(setting: string, submitted: ModalSubmitInteraction) {
if (setting === 'logChannelId') {
- const channelId = submitted.fields.getField('log_channel')?.values?.[0];
+ const channelId = (submitted.fields.getField('log_channel') as SelectMenuModalData)?.values?.[0];
if (!channelId) {
- throw new Error('Please select a log channel.');
+ throw new Error('Bitte wähle einen Log-Kanal aus.');
}
return channelId;
}
if (setting === 'modRole') {
- const roleId = submitted.fields.getField('mod_role')?.values?.[0];
+ const roleId = (submitted.fields.getField('mod_role') as SelectMenuModalData)?.values?.[0];
if (!roleId) {
- throw new Error('Please select a moderator role.');
+ throw new Error('Bitte wähle eine Moderator-Rolle aus.');
}
return roleId;
}
const prefix = submitted.fields.getTextInputValue('value')?.trim();
if (!prefix || prefix.length < 1 || prefix.length > 10 || /\s/.test(prefix)) {
- throw new Error('Prefix must be 1-10 characters with no spaces.');
+ throw new Error('Der Prefix muss 1–10 Zeichen lang sein und darf keine Leerzeichen enthalten.');
}
return prefix;
}
-function buildSettingSuccessMessage(setting, value, guild) {
+function buildSettingSuccessMessage(setting: string, value: string, guild: Guild) {
if (setting === 'logChannelId') {
const channel = guild.channels.cache.get(value);
- return `Log channel set to ${channel ?? `<#${value}>`}.`;
+ return `Log-Kanal festgelegt: ${channel ?? `<#${value}>`}.`;
}
if (setting === 'modRole') {
const role = guild.roles.cache.get(value);
- return `Moderator role set to ${role ?? `<@&${value}>`}.`;
+ return `Moderator-Rolle festgelegt: ${role ?? `<@&${value}>`}.`;
}
- return `Server prefix set to \`${value}\`.`;
+ return `Server-Prefix festgelegt: \`${value}\`.`;
}
-async function handleSettingModalSubmit(selectInteraction, rootInteraction, setting, guildId, client) {
+async function handleSettingModalSubmit(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, setting: string, guildId: string, client: NexoClient) {
const modalCustomId = `config_wizard_modal:${setting}:${guildId}`;
const submitted = await selectInteraction
@@ -587,17 +608,17 @@ async function handleSettingModalSubmit(selectInteraction, rootInteraction, sett
await ConfigService.updateSetting(client, guildId, setting, value, submitted.user.id);
await submitted.reply({
- embeds: [successEmbed('Configuration Updated', buildSettingSuccessMessage(setting, value, submitted.guild))],
+ embeds: [successEmbed('Konfiguration aktualisiert', buildSettingSuccessMessage(setting, value, submitted.guild!))],
flags: MessageFlags.Ephemeral,
});
const updatedConfig = await getGuildConfig(client, guildId);
- await refreshDashboard(rootInteraction, updatedConfig, submitted.guild);
+ await refreshDashboard(rootInteraction, updatedConfig, submitted.guild!);
} catch (error) {
logger.error('Config wizard modal submit error:', error);
await replyUserError(submitted, {
type: ErrorTypes.CONFIGURATION,
- message: error.message || 'Please try again.',
+ message: (error as Error).message || 'Bitte versuch es erneut.',
}).catch(() => {});
}
}
@@ -606,7 +627,7 @@ export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName('configwizard')
- .setDescription('Open the server configuration dashboard and setup wizard')
+ .setDescription('Öffnet das Konfigurations-Dashboard und die Einrichtung des Servers')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.setDMPermission(false),
category: 'Core',
@@ -621,13 +642,13 @@ export default {
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
return replyUserError(interaction, {
type: ErrorTypes.PERMISSION,
- message: 'You need the **Manage Server** permission to use this command.',
+ message: 'Du brauchst die Berechtigung **Server verwalten**, um diesen Command zu nutzen.',
});
}
- const guildConfig = await getGuildConfig(interaction.client, interaction.guildId);
- const embed = buildDashboardEmbed(guildConfig, interaction.guild);
- const components = [buildButtonRow(guildConfig, interaction.guildId), buildSettingsSelect(interaction.guildId)];
+ const guildConfig = await getGuildConfig(interaction.client, interaction.guildId!);
+ const embed = buildDashboardEmbed(guildConfig, interaction.guild!);
+ const components = [buildButtonRow(guildConfig, interaction.guildId!), buildSettingsSelect(interaction.guildId!)];
await InteractionHelper.safeEditReply(interaction, { embeds: [embed], components });
@@ -636,7 +657,7 @@ export default {
return;
}
- const collectorFilter = (componentInteraction) =>
+ const collectorFilter = (componentInteraction: MessageComponentInteraction) =>
componentInteraction.user.id === interaction.user.id &&
componentInteraction.customId.includes(`:${interaction.guildId}`);
@@ -651,20 +672,20 @@ export default {
await componentInteraction.deferUpdate();
if (componentInteraction.customId.startsWith(`${WIZARD_BUTTON_ID}:`)) {
- const latestConfig = await getGuildConfig(interaction.client, interaction.guildId);
- await runSetupWizard(componentInteraction, latestConfig, interaction.guild, interaction.client, interaction);
+ const latestConfig = await getGuildConfig(interaction.client, interaction.guildId!);
+ await runSetupWizard(componentInteraction, latestConfig, interaction.guild!, interaction.client, interaction);
}
return;
}
if (componentInteraction.isStringSelectMenu()) {
const selected = componentInteraction.values[0];
- await showSettingModal(componentInteraction, interaction.guildId, selected);
+ await showSettingModal(componentInteraction, interaction.guildId!, selected);
await handleSettingModalSubmit(
componentInteraction,
interaction,
selected,
- interaction.guildId,
+ interaction.guildId!,
interaction.client,
);
}
@@ -672,7 +693,7 @@ export default {
logger.error('Config dashboard interaction error:', error);
await replyUserError(componentInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'Failed to process your selection. Please try again.',
+ message: 'Deine Auswahl konnte nicht verarbeitet werden. Bitte versuch es erneut.',
}).catch(() => {});
}
});
@@ -680,8 +701,8 @@ export default {
logger.error('Config command error:', error);
await replyUserError(interaction, {
type: ErrorTypes.CONFIGURATION,
- message: 'Failed to open configuration dashboard. Please try again.',
+ message: 'Das Konfigurations-Dashboard konnte nicht geöffnet werden. Bitte versuch es erneut.',
});
}
},
-};
+} satisfies Command;
diff --git a/src/commands/Core/help.js b/src/commands/Core/help.ts
similarity index 56%
rename from src/commands/Core/help.js
rename to src/commands/Core/help.ts
index 3d9d7eb783..c658d7d7c8 100644
--- a/src/commands/Core/help.js
+++ b/src/commands/Core/help.ts
@@ -4,8 +4,11 @@ import {
ButtonBuilder,
ButtonStyle,
} from "discord.js";
+import type { Command, NexoClient } from '../../types/index.js';
+import { logger } from '../../utils/logger.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import { createEmbed } from "../../utils/embeds.js";
+import { formatCategoryName, getCategoryIcon } from "../../config/commands/commandCategories.js";
import {
createSelectMenu,
} from "../../utils/components.js";
@@ -21,35 +24,7 @@ const ALL_COMMANDS_ID = "help-all-commands";
const BUG_REPORT_BUTTON_ID = "help-bug-report";
const HELP_MENU_TIMEOUT_MS = 5 * 60 * 1000;
-const CATEGORY_ICONS = {
- Core: "ℹ️",
- Moderation: "🛡️",
- Economy: "💰",
- Music: "🎵",
- Fun: "🎮",
- Leveling: "📊",
- Utility: "🔧",
- Ticket: "🎫",
- Welcome: "👋",
- Giveaway: "🎉",
- Counter: "🔢",
- Tools: "🛠️",
- Search: "🔍",
- "Reaction Roles": "🎭",
- Community: "👥",
- Birthday: "🎂",
- "Join To Create": "🔌",
- Verification: "✅",
-};
-
-function formatCategoryName(rawCategory) {
- return rawCategory
- .replace(/_/g, '')
- .replace(/([a-z])([A-Z])/g, '$1 $2')
- .replace(/\b\w/g, (char) => char.toUpperCase());
-}
-
-export async function createInitialHelpMenu(client) {
+export async function createInitialHelpMenu(client: NexoClient) {
const commandsPath = path.join(__dirname, "../../commands");
const categoryDirs = (
await fs.readdir(commandsPath, { withFileTypes: true })
@@ -60,77 +35,73 @@ export async function createInitialHelpMenu(client) {
const options = [
{
- label: "📋 All Commands",
- description: "Browse every available command in a single list",
+ label: "📋 Alle Commands",
+ description: "Alle verfügbaren Commands in einer Liste durchsuchen",
value: ALL_COMMANDS_ID,
},
...categoryDirs.map((category) => {
const categoryName = formatCategoryName(category);
- const icon = CATEGORY_ICONS[categoryName] || "🔍";
+ const icon = getCategoryIcon(category);
return {
label: `${icon} ${categoryName}`,
- description: `View commands in the ${categoryName} category`,
+ description: `Commands der Kategorie ${categoryName} anzeigen`,
value: category,
};
}),
];
- const botName = client?.user?.username || "Bot";
+ const botName = client?.user?.username || "Nexo";
const embed = createEmbed({
- title: `📖 ${botName} Help`,
- description: 'Set up your server, pick what to enable, then browse commands below.',
+ title: `📖 ${botName} – Hilfe`,
+ description: 'Richte deinen Server ein, wähle aus, was aktiv sein soll, und stöbere dann unten durch die Commands.',
color: 'primary',
thumbnail: client.user?.displayAvatarURL?.({ size: 1024 }),
fields: [
{
- name: '🚀 Getting Started',
+ name: '🚀 Erste Schritte',
value: [
- '**1. Launch setup** — Run `/configwizard` to configure prefix, mod role, and logs.',
- '**2. Enable systems** — Use `/commands dashboard` to turn categories on or off.', '**3. Browse commands** — Use the menu below to view categories and commands.',
+ '**1. Einrichtung starten** — Mit `/configwizard` legst du Prefix, Moderator-Rolle und Logs fest.',
+ '**2. Systeme aktivieren** — Mit `/commands dashboard` schaltest du Kategorien an oder aus.',
+ '**3. Commands entdecken** — Im Menü unten findest du alle Kategorien und Commands.',
].join('\n'),
inline: false,
},
{
- name: 'ℹ️ How It Works',
+ name: 'ℹ️ So funktioniert’s',
value: [
- '• Dashboard commands manage each feature visually',
- '• Settings are saved per server',
- '• Slash commands and prefixes both work once enabled',
+ '• Dashboard-Commands verwalten jede Funktion übersichtlich',
+ '• Einstellungen werden pro Server gespeichert',
+ '• Slash-Commands und Prefix-Commands funktionieren, sobald sie aktiviert sind',
].join('\n'),
inline: false,
},
{
name: '\u200B',
- value: `-# ${botName} is [open source](https://youtu.be/1jCZX8s3bJE?si=NPOYx-vxVE1I5vJK)`,
+ value: `-# ${botName} ist [Open Source](https://github.com/Robertwlt03/NexoBot)`,
inline: false,
},
],
});
embed.setFooter({
- text: "Made with ❤️"
+ text: "Ein Bot. Alle Funktionen. · Mit ❤️ gemacht"
});
embed.setTimestamp();
const bugReportButton = new ButtonBuilder()
.setCustomId(BUG_REPORT_BUTTON_ID)
- .setLabel("Report Bug")
+ .setLabel("Bug melden")
.setStyle(ButtonStyle.Danger);
- const supportButton = new ButtonBuilder()
- .setLabel("Support Server")
- .setURL("https://discord.gg/QnWNz2dKCE")
- .setStyle(ButtonStyle.Link);
const selectRow = createSelectMenu(
CATEGORY_SELECT_ID,
- "Select to view the commands",
+ "Wähle eine Kategorie, um die Commands zu sehen",
options,
);
- const buttonRow = new ActionRowBuilder().addComponents([
+ const buttonRow = new ActionRowBuilder().addComponents([
bugReportButton,
- supportButton,
]);
return {
@@ -143,7 +114,7 @@ export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName("help")
- .setDescription("Displays the help menu with all available commands"),
+ .setDescription("Zeigt das Hilfemenü mit allen verfügbaren Commands"),
async execute(interaction, guildConfig, client) {
@@ -164,8 +135,8 @@ export default {
}
const closedEmbed = createEmbed({
- title: "Help menu closed",
- description: "Help menu has been closed, use /help again.",
+ title: "Hilfemenü geschlossen",
+ description: "Das Hilfemenü wurde geschlossen. Nutze /help erneut.",
color: "secondary",
});
@@ -174,8 +145,8 @@ export default {
components: [],
});
} catch (error) {
- logger.debug('Help menu close edit failed (interaction may have expired):', error?.message);
+ logger.debug('Help menu close edit failed (interaction may have expired):', (error as Error | undefined)?.message);
}
}, HELP_MENU_TIMEOUT_MS);
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Core/modules/commands_dashboard.js b/src/commands/Core/modules/commands_dashboard.ts
similarity index 63%
rename from src/commands/Core/modules/commands_dashboard.js
rename to src/commands/Core/modules/commands_dashboard.ts
index 88a8865a50..5f755e9e3f 100644
--- a/src/commands/Core/modules/commands_dashboard.js
+++ b/src/commands/Core/modules/commands_dashboard.ts
@@ -5,6 +5,8 @@ import {
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
} from 'discord.js';
+import type { APIEmbedField, Guild, MessageComponentInteraction, StringSelectMenuInteraction } from 'discord.js';
+import type { NexoClient } from '../../../types/index.js';
import { createEmbed } from '../../../utils/embeds.js';
import {
getCommandAccessSnapshot,
@@ -15,6 +17,17 @@ import {
resetCategoryCommands,
} from '../../../services/commandAccessService.js';
import { getGuildConfig } from '../../../services/config/guildConfig.js';
+import type { CommandRegistryEntry } from '../../../services/commandAccessService.js';
+
+export type CommandAccessSnapshot = ReturnType;
+export type CommandAccessCategory = CommandAccessSnapshot['categories'][number];
+export type DashboardRow = ActionRowBuilder | ActionRowBuilder;
+
+export interface DashboardView {
+ embed: ReturnType;
+ components: DashboardRow[];
+ categoryKey?: string;
+}
export const DASHBOARD_CATEGORY_SELECT = 'cmdaccess_category';
export const DASHBOARD_COMMAND_SELECT = 'cmdaccess_command';
@@ -31,11 +44,11 @@ const STATUS = {
disabled: '🔴',
};
-function customId(base, guildId, suffix = '') {
+function customId(base: string, guildId: string, suffix = '') {
return suffix ? `${base}:${guildId}:${suffix}` : `${base}:${guildId}`;
}
-function getCategoryStatus(category) {
+function getCategoryStatus(category: CommandAccessCategory) {
if (category.categoryDisabled) {
return STATUS.disabled;
}
@@ -45,15 +58,15 @@ function getCategoryStatus(category) {
return STATUS.partial;
}
-function formatCommandLabel(command) {
+function formatCommandLabel(command: CommandRegistryEntry) {
if (command.isSubcommand) {
return `\`${command.name.replace(/ /g, ' ')}\``;
}
return `\`${command.name}\``;
}
-function chunkLines(lines, maxLength = 980) {
- const chunks = [];
+function chunkLines(lines: string[], maxLength = 980) {
+ const chunks: string[] = [];
let current = '';
for (const line of lines) {
@@ -73,29 +86,29 @@ function chunkLines(lines, maxLength = 980) {
return chunks;
}
-export function buildOverviewEmbed(snapshot, guild) {
+export function buildOverviewEmbed(snapshot: CommandAccessSnapshot, guild: Guild) {
const fullyEnabled = snapshot.categories.filter((c) => !c.categoryDisabled && c.disabledCount === 0).length;
const partial = snapshot.categories.filter((c) => !c.categoryDisabled && c.disabledCount > 0).length;
const disabled = snapshot.categories.filter((c) => c.categoryDisabled).length;
const categoryLines = snapshot.categories.map((category) => {
const icon = getCategoryStatus(category);
- const subcommandNote = category.commands.some((c) => c.isSubcommand) ? ' · incl. subcommands' : '';
+ const subcommandNote = category.commands.some((c) => c.isSubcommand) ? ' · inkl. Subcommands' : '';
return `${icon} ${category.icon} **${category.displayName}** — ${category.enabledCount}/${category.totalCount}${subcommandNote}`;
});
- const fields = [
+ const fields: APIEmbedField[] = [
{
- name: '📊 Summary',
+ name: '📊 Übersicht',
value: [
- `**${snapshot.enabledTotal}/${snapshot.totalCommands}** entries enabled`,
- `${STATUS.enabled} ${fullyEnabled} fully on · ${STATUS.partial} ${partial} partial · ${STATUS.disabled} ${disabled} off`,
+ `**${snapshot.enabledTotal}/${snapshot.totalCommands}** Einträge aktiviert`,
+ `${STATUS.enabled} ${fullyEnabled} komplett an · ${STATUS.partial} ${partial} teilweise · ${STATUS.disabled} ${disabled} aus`,
].join('\n'),
inline: false,
},
{
- name: '🔑 Legend',
- value: `${STATUS.enabled} All enabled · ${STATUS.partial} Some disabled · ${STATUS.disabled} Category off`,
+ name: '🔑 Legende',
+ value: `${STATUS.enabled} Alle aktiviert · ${STATUS.partial} Teilweise deaktiviert · ${STATUS.disabled} Kategorie aus`,
inline: false,
},
];
@@ -103,37 +116,37 @@ export function buildOverviewEmbed(snapshot, guild) {
const chunks = chunkLines(categoryLines);
chunks.forEach((chunk, index) => {
fields.push({
- name: index === 0 ? '📁 Categories' : '📁 Categories (cont.)',
+ name: index === 0 ? '📁 Kategorien' : '📁 Kategorien (Forts.)',
value: chunk,
inline: false,
});
});
fields.push({
- name: 'How to Use',
+ name: 'So funktioniert’s',
value: [
- '• Select a category below to manage commands and subcommands',
- '• `/commands disable` — turn off a category or specific command',
- '• `/commands enable` — turn something back on',
+ '• Wähle unten eine Kategorie, um Commands und Subcommands zu verwalten',
+ '• `/commands disable` — eine Kategorie oder einen Command deaktivieren',
+ '• `/commands enable` — etwas wieder aktivieren',
].join('\n'),
});
return createEmbed({
- title: '⚙️ Command Access',
- description: `Manage slash and prefix commands for **${guild.name}**. Subcommands (e.g. \`birthday list\`) are listed separately.`,
+ title: '⚙️ Command-Zugriff',
+ description: `Verwalte Slash- und Prefix-Commands für **${guild.name}**. Subcommands (z. B. \`birthday list\`) werden einzeln aufgeführt.`,
color: 'info',
fields,
- footer: '🔒 commands & configwizard always stay available',
+ footer: '🔒 commands & configwizard bleiben immer verfügbar',
});
}
-export function buildCategoryEmbed(category, guild) {
+export function buildCategoryEmbed(category: CommandAccessCategory, guild: Guild) {
const statusIcon = getCategoryStatus(category);
const statusText = category.categoryDisabled
- ? 'Category disabled'
+ ? 'Kategorie deaktiviert'
: category.disabledCount === 0
- ? 'All entries enabled'
- : `${category.disabledCount} of ${category.totalCount} disabled`;
+ ? 'Alle Einträge aktiviert'
+ : `${category.disabledCount} von ${category.totalCount} deaktiviert`;
const commandLines = category.commands.map((command) => {
const enabled = category.enabledCommands.includes(command.name);
@@ -142,15 +155,15 @@ export function buildCategoryEmbed(category, guild) {
return `${icon} ${formatCommandLabel(command)}${lock}`;
});
- const fields = [
+ const fields: APIEmbedField[] = [
{
name: `${statusIcon} Status`,
value: statusText,
inline: true,
},
{
- name: '📈 Count',
- value: `${category.enabledCount}/${category.totalCount} enabled`,
+ name: '📈 Anzahl',
+ value: `${category.enabledCount}/${category.totalCount} aktiviert`,
inline: true,
},
];
@@ -158,58 +171,58 @@ export function buildCategoryEmbed(category, guild) {
const chunks = chunkLines(commandLines);
chunks.forEach((chunk, index) => {
fields.push({
- name: index === 0 ? '📋 Commands & Subcommands' : '📋 (cont.)',
+ name: index === 0 ? '📋 Commands & Subcommands' : '📋 (Forts.)',
value: chunk,
inline: false,
});
});
fields.push({
- name: 'How to Use',
+ name: 'So funktioniert’s',
value: [
- '• Use the dropdown to toggle individual commands or subcommands',
- '• **Disable All** turns off the whole category',
- '• **Clear Overrides** re-enables individually disabled entries',
+ '• Über das Dropdown schaltest du einzelne Commands oder Subcommands um',
+ '• **Alle deaktivieren** schaltet die ganze Kategorie aus',
+ '• **Ausnahmen löschen** aktiviert einzeln deaktivierte Einträge wieder',
].join('\n'),
});
return createEmbed({
title: `${category.icon} ${category.displayName}`,
- description: `Command access for **${guild.name}**.`,
+ description: `Command-Zugriff für **${guild.name}**.`,
color: category.categoryDisabled ? 'error' : category.disabledCount > 0 ? 'warning' : 'success',
fields,
- footer: '🔒 Protected entries cannot be disabled',
+ footer: '🔒 Geschützte Einträge können nicht deaktiviert werden',
});
}
-export function buildOverviewComponents(guildId, snapshot) {
+export function buildOverviewComponents(guildId: string, snapshot: CommandAccessSnapshot): DashboardRow[] {
const categoryOptions = snapshot.categories.slice(0, 25).map((category) => {
const status = getCategoryStatus(category);
return new StringSelectMenuOptionBuilder()
.setLabel(`${category.displayName}`.slice(0, 100))
- .setDescription(`${status} ${category.enabledCount}/${category.totalCount} enabled`.slice(0, 100))
+ .setDescription(`${status} ${category.enabledCount}/${category.totalCount} aktiviert`.slice(0, 100))
.setValue(category.key)
.setEmoji(category.icon);
});
return [
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId(customId(DASHBOARD_CATEGORY_SELECT, guildId))
- .setPlaceholder('📁 Select a category...')
+ .setPlaceholder('📁 Kategorie auswählen...')
.addOptions(categoryOptions),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(customId(DASHBOARD_REFRESH, guildId))
- .setLabel('Refresh')
+ .setLabel('Aktualisieren')
.setEmoji('🔄')
.setStyle(ButtonStyle.Secondary),
),
];
}
-export function buildCategoryComponents(guildId, category) {
+export function buildCategoryComponents(guildId: string, category: CommandAccessCategory): DashboardRow[] {
const toggleableCommands = category.commands.filter((command) => !command.protected);
const commandOptions = toggleableCommands.slice(0, 25).map((command) => {
const enabled = category.enabledCommands.includes(command.name);
@@ -219,35 +232,35 @@ export function buildCategoryComponents(guildId, category) {
return new StringSelectMenuOptionBuilder()
.setLabel(label)
- .setDescription((enabled ? '🟢 Enabled — click to disable' : '🔴 Disabled — click to enable').slice(0, 100))
+ .setDescription((enabled ? '🟢 Aktiviert — klicken zum Deaktivieren' : '🔴 Deaktiviert — klicken zum Aktivieren').slice(0, 100))
.setValue(command.name);
});
- const rows = [
- new ActionRowBuilder().addComponents(
+ const rows: DashboardRow[] = [
+ new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(customId(DASHBOARD_HOME, guildId))
- .setLabel('Back')
+ .setLabel('Zurück')
.setEmoji('◀️')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId(customId(DASHBOARD_TOGGLE_CATEGORY, guildId, category.key))
- .setLabel(category.categoryDisabled ? 'Enable Category' : 'Disable Category')
+ .setLabel(category.categoryDisabled ? 'Kategorie aktivieren' : 'Kategorie deaktivieren')
.setEmoji(category.categoryDisabled ? '🟢' : '🔴')
.setStyle(category.categoryDisabled ? ButtonStyle.Success : ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(customId(DASHBOARD_ENABLE_ALL, guildId, category.key))
- .setLabel('Enable All')
+ .setLabel('Alle aktivieren')
.setEmoji('✅')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(customId(DASHBOARD_DISABLE_ALL, guildId, category.key))
- .setLabel('Disable All')
+ .setLabel('Alle deaktivieren')
.setEmoji('⛔')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(customId(DASHBOARD_RESET_COMMANDS, guildId, category.key))
- .setLabel('Clear Overrides')
+ .setLabel('Ausnahmen löschen')
.setEmoji('🧹')
.setStyle(ButtonStyle.Secondary),
),
@@ -255,10 +268,10 @@ export function buildCategoryComponents(guildId, category) {
if (commandOptions.length > 0) {
rows.unshift(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId(customId(DASHBOARD_COMMAND_SELECT, guildId, category.key))
- .setPlaceholder('Toggle a command or subcommand...')
+ .setPlaceholder('Command oder Subcommand umschalten...')
.addOptions(commandOptions),
),
);
@@ -267,7 +280,7 @@ export function buildCategoryComponents(guildId, category) {
return rows;
}
-export async function buildDashboardView(client, guildId, guild, view = 'overview', categoryKey = null) {
+export async function buildDashboardView(client: NexoClient, guildId: string, guild: Guild, view = 'overview', categoryKey: string | null = null): Promise {
const config = await getGuildConfig(client, guildId);
const snapshot = getCommandAccessSnapshot(client, config);
@@ -293,7 +306,7 @@ export async function buildDashboardView(client, guildId, guild, view = 'overvie
};
}
-export async function handleDashboardComponent(interaction, client) {
+export async function handleDashboardComponent(interaction: MessageComponentInteraction, client: NexoClient) {
const parts = interaction.customId.split(':');
const action = parts[0];
const guildId = parts[1];
@@ -301,14 +314,15 @@ export async function handleDashboardComponent(interaction, client) {
if (guildId !== interaction.guildId) {
return interaction.reply({
- content: 'This dashboard belongs to another server.',
+ content: 'Dieses Dashboard gehört zu einem anderen Server.',
ephemeral: true,
});
}
if (action === DASHBOARD_COMMAND_SELECT) {
const categoryKey = suffix;
- const commandName = interaction.values[0];
+ // Only the command select menu uses this customId.
+ const commandName = (interaction as StringSelectMenuInteraction).values[0];
const config = await getGuildConfig(client, guildId);
const snapshot = getCommandAccessSnapshot(client, config);
const category = snapshot.categories.find((entry) => entry.key === categoryKey);
@@ -320,20 +334,21 @@ export async function handleDashboardComponent(interaction, client) {
await enableCommand(client, guildId, commandName);
}
- const view = await buildDashboardView(client, guildId, interaction.guild, 'category', categoryKey);
+ const view = await buildDashboardView(client, guildId, interaction.guild!, 'category', categoryKey);
return interaction.update({ embeds: [view.embed], components: view.components });
}
if (action === DASHBOARD_CATEGORY_SELECT) {
- const categoryKey = interaction.values[0];
- const view = await buildDashboardView(client, guildId, interaction.guild, 'category', categoryKey);
+ // Only the category select menu uses this customId.
+ const categoryKey = (interaction as StringSelectMenuInteraction).values[0];
+ const view = await buildDashboardView(client, guildId, interaction.guild!, 'category', categoryKey);
return interaction.update({ embeds: [view.embed], components: view.components });
}
await interaction.deferUpdate();
if (action === DASHBOARD_REFRESH || action === DASHBOARD_HOME) {
- const view = await buildDashboardView(client, guildId, interaction.guild, 'overview');
+ const view = await buildDashboardView(client, guildId, interaction.guild!, 'overview');
return interaction.editReply({ embeds: [view.embed], components: view.components });
}
@@ -344,44 +359,44 @@ export async function handleDashboardComponent(interaction, client) {
const category = snapshot.categories.find((entry) => entry.key === categoryKey);
if (category?.categoryDisabled) {
- await enableCategory(client, guildId, categoryKey);
+ await enableCategory(client, guildId, categoryKey!);
} else {
- await disableCategory(client, guildId, categoryKey);
+ await disableCategory(client, guildId, categoryKey!);
}
- const view = await buildDashboardView(client, guildId, interaction.guild, 'category', categoryKey);
+ const view = await buildDashboardView(client, guildId, interaction.guild!, 'category', categoryKey);
return interaction.editReply({ embeds: [view.embed], components: view.components });
}
if (action === DASHBOARD_ENABLE_ALL) {
- await enableCategory(client, guildId, suffix);
- await resetCategoryCommands(client, guildId, suffix);
- const view = await buildDashboardView(client, guildId, interaction.guild, 'category', suffix);
+ await enableCategory(client, guildId, suffix!);
+ await resetCategoryCommands(client, guildId, suffix!);
+ const view = await buildDashboardView(client, guildId, interaction.guild!, 'category', suffix);
return interaction.editReply({ embeds: [view.embed], components: view.components });
}
if (action === DASHBOARD_DISABLE_ALL) {
- await disableCategory(client, guildId, suffix);
- const view = await buildDashboardView(client, guildId, interaction.guild, 'category', suffix);
+ await disableCategory(client, guildId, suffix!);
+ const view = await buildDashboardView(client, guildId, interaction.guild!, 'category', suffix);
return interaction.editReply({ embeds: [view.embed], components: view.components });
}
if (action === DASHBOARD_RESET_COMMANDS) {
- await enableCategory(client, guildId, suffix);
- await resetCategoryCommands(client, guildId, suffix);
- const view = await buildDashboardView(client, guildId, interaction.guild, 'category', suffix);
+ await enableCategory(client, guildId, suffix!);
+ await resetCategoryCommands(client, guildId, suffix!);
+ const view = await buildDashboardView(client, guildId, interaction.guild!, 'category', suffix);
return interaction.editReply({ embeds: [view.embed], components: view.components });
}
- return interaction.editReply({ content: 'Unknown dashboard action.', embeds: [], components: [] });
+ return interaction.editReply({ content: 'Unbekannte Dashboard-Aktion.', embeds: [], components: [] });
}
-export function isCommandAccessCustomId(customIdValue) {
+export function isCommandAccessCustomId(customIdValue: string) {
return customIdValue.startsWith('cmdaccess_');
}
-export function createDashboardCollectorFilter(userId, guildId) {
- return (componentInteraction) =>
+export function createDashboardCollectorFilter(userId: string, guildId: string | null) {
+ return (componentInteraction: MessageComponentInteraction) =>
componentInteraction.user.id === userId &&
componentInteraction.customId.includes(`:${guildId}`);
}
diff --git a/src/commands/Core/ping.js b/src/commands/Core/modules/ping.ts
similarity index 62%
rename from src/commands/Core/ping.js
rename to src/commands/Core/modules/ping.ts
index 8341f780d0..b352adc3b3 100644
--- a/src/commands/Core/ping.js
+++ b/src/commands/Core/modules/ping.ts
@@ -1,38 +1,44 @@
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { ChatInputCommandInteraction, GuildTextBasedChannel } from 'discord.js';
+import type { Command } from '../../../types/index.js';
+import type { InteractionExtras } from '../../../utils/interactionHelper.js';
+
+/** Prefix invocations arrive as a mock interaction carrying the message timestamp (see utils/messageAdapter). */
+type PingInteraction = ChatInputCommandInteraction & InteractionExtras & { _commandStartTime?: number };
+import { createEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("ping")
- .setDescription("Checks the bot's latency and API speed"),
+ .setDescription("Prüft die Latenz des Bots und die API-Geschwindigkeit"),
- async prefixExecute(interaction) {
+ async prefixExecute(interaction: PingInteraction) {
try {
const startTime = Date.now();
- const pingingMessage = await interaction.reply({ content: 'Pinging...' });
+ const pingingMessage = await interaction.reply({ content: 'Pinge...' });
const latency = Date.now() - startTime;
const apiLatency = Math.max(0, Math.round(interaction.client.ws.ping));
const embed = createEmbed({ title: 'Pong!', description: null }).addFields(
- { name: 'Bot Latency', value: `${latency}ms`, inline: true },
- { name: 'API Latency', value: `${apiLatency}ms`, inline: true },
+ { name: 'Bot-Latenz', value: `${latency}ms`, inline: true },
+ { name: 'API-Latenz', value: `${apiLatency}ms`, inline: true },
);
await pingingMessage.edit({ content: null, embeds: [embed] });
} catch (error) {
logger.error('Ping prefix command error:', error);
if (!interaction.replied && !interaction._replyMessage) {
- await interaction.channel.send({
- embeds: [createEmbed({ title: 'System Error', description: 'Could not determine latency at this time.', color: 'error' })],
+ await (interaction.channel as GuildTextBasedChannel).send({
+ embeds: [createEmbed({ title: 'Systemfehler', description: 'Die Latenz konnte gerade nicht ermittelt werden.', color: 'error' })],
}).catch(() => {});
}
}
},
- async execute(interaction) {
+ async execute(interaction: PingInteraction) {
logger.info('execute called - checking if slash command or prefix command');
logger.info(`execute - has _commandStartTime: ${!!interaction._commandStartTime}, createdTimestamp: ${interaction.createdTimestamp}`);
@@ -48,7 +54,7 @@ export default {
try {
await InteractionHelper.safeEditReply(interaction, {
- content: "Pinging...",
+ content: "Pinge...",
});
const startTime = interaction._commandStartTime || interaction.createdTimestamp;
@@ -58,8 +64,8 @@ export default {
logger.info(`execute - calculated latency: ${latency}ms, apiLatency: ${apiLatency}ms`);
const embed = createEmbed({ title: "Pong!", description: null }).addFields(
- { name: "Bot Latency", value: `${latency}ms`, inline: true },
- { name: "API Latency", value: `${apiLatency}ms`, inline: true },
+ { name: "Bot-Latenz", value: `${latency}ms`, inline: true },
+ { name: "API-Latenz", value: `${apiLatency}ms`, inline: true },
);
await InteractionHelper.safeEditReply(interaction, {
@@ -70,7 +76,7 @@ export default {
logger.error('Ping command error:', error);
try {
return await InteractionHelper.safeReply(interaction, {
- embeds: [createEmbed({ title: 'System Error', description: 'Could not determine latency at this time.', color: 'error' })],
+ embeds: [createEmbed({ title: 'Systemfehler', description: 'Die Latenz konnte gerade nicht ermittelt werden.', color: 'error' })],
flags: MessageFlags.Ephemeral,
});
} catch (replyError) {
@@ -78,4 +84,4 @@ export default {
}
}
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Core/stats.js b/src/commands/Core/modules/stats.ts
similarity index 62%
rename from src/commands/Core/stats.js
rename to src/commands/Core/modules/stats.ts
index fc6dc3d392..deadc102b8 100644
--- a/src/commands/Core/stats.js
+++ b/src/commands/Core/modules/stats.ts
@@ -1,12 +1,13 @@
import { SlashCommandBuilder, version, MessageFlags } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
+import type { Command } from '../../../types/index.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("stats")
- .setDescription("View bot statistics"),
+ .setDescription("Statistiken des Bots anzeigen"),
async execute(interaction) {
try {
@@ -19,13 +20,13 @@ export default {
);
const nodeVersion = process.version;
- const embed = createEmbed({ title: "System Statistics", description: "Real-time performance metrics." }).addFields(
+ const embed = createEmbed({ title: "Systemstatistiken", description: "Leistungswerte in Echtzeit." }).addFields(
{ name: "Servers", value: `${totalGuilds}`, inline: true },
- { name: "Users", value: `${totalMembers}`, inline: true },
+ { name: "Nutzer", value: `${totalMembers}`, inline: true },
{ name: "Node.js", value: `${nodeVersion}`, inline: true },
{ name: "Discord.js", value: `v${version}`, inline: true },
{
- name: "Memory Usage",
+ name: "Speicherverbrauch",
value: `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`,
inline: true,
},
@@ -35,9 +36,9 @@ export default {
} catch (error) {
logger.error('Stats command error:', error);
return InteractionHelper.safeEditReply(interaction, {
- embeds: [createEmbed({ title: 'System Error', description: 'Could not fetch system statistics.', color: 'error' })],
+ embeds: [createEmbed({ title: 'Systemfehler', description: 'Systemstatistiken konnten nicht abgerufen werden.', color: 'error' })],
flags: MessageFlags.Ephemeral,
});
}
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Core/modules/support.ts b/src/commands/Core/modules/support.ts
new file mode 100644
index 0000000000..ca8de89791
--- /dev/null
+++ b/src/commands/Core/modules/support.ts
@@ -0,0 +1,42 @@
+import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, MessageFlags } from 'discord.js';
+import type { Command } from '../../../types/index.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+const SUPPORT_URL = "https://github.com/Robertwlt03/NexoBot/issues";
+export default {
+ data: new SlashCommandBuilder()
+ .setName("support")
+ .setDescription("Hilfe bekommen und Probleme auf GitHub melden"),
+
+ async execute(interaction) {
+ try {
+ const supportButton = new ButtonBuilder()
+ .setLabel("GitHub-Issues öffnen")
+ .setStyle(ButtonStyle.Link)
+ .setURL(SUPPORT_URL);
+
+ const actionRow = new ActionRowBuilder().addComponents(supportButton);
+
+ await InteractionHelper.safeReply(interaction, {
+ embeds: [
+ createEmbed({ title: "Brauchst du Hilfe?", description: "Du brauchst Hilfe, hast einen Bug gefunden oder eine Idee für ein Feature? Eröffne ein Issue auf GitHub." }),
+ ],
+ components: [actionRow],
+ flags: MessageFlags.Ephemeral,
+ });
+ } catch (error) {
+ logger.error('Support command error:', error);
+
+ try {
+ return await InteractionHelper.safeReply(interaction, {
+ embeds: [createEmbed({ title: 'Systemfehler', description: 'Support-Infos konnten nicht angezeigt werden.', color: 'error' })],
+ flags: MessageFlags.Ephemeral,
+ });
+ } catch (replyError) {
+ logger.error('Failed to send error reply:', replyError);
+ }
+ }
+ },
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Core/uptime.js b/src/commands/Core/modules/uptime.ts
similarity index 69%
rename from src/commands/Core/uptime.js
rename to src/commands/Core/modules/uptime.ts
index a3fb52487c..0af5e3e007 100644
--- a/src/commands/Core/uptime.js
+++ b/src/commands/Core/modules/uptime.ts
@@ -1,12 +1,13 @@
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
+import type { Command } from '../../../types/index.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("uptime")
- .setDescription("Check how long the bot has been online"),
+ .setDescription("Zeigt, wie lange der Bot schon online ist"),
async execute(interaction) {
try {
@@ -24,7 +25,7 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [createEmbed({
- title: "System Uptime",
+ title: "System-Uptime",
description: `\`\`\`${uptimeStr}\`\`\``
})],
});
@@ -33,7 +34,7 @@ export default {
try {
return await InteractionHelper.safeEditReply(interaction, {
- embeds: [createEmbed({ title: 'System Error', description: 'Could not compute uptime.', color: 'error' })],
+ embeds: [createEmbed({ title: 'Systemfehler', description: 'Uptime konnte nicht berechnet werden.', color: 'error' })],
flags: MessageFlags.Ephemeral,
});
} catch (replyError) {
@@ -41,4 +42,4 @@ export default {
}
}
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Core/support.js b/src/commands/Core/support.js
deleted file mode 100644
index 032f3c894d..0000000000
--- a/src/commands/Core/support.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, MessageFlags } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-const SUPPORT_SERVER_URL = "https://discord.gg/QnWNz2dKCE";
-export default {
- data: new SlashCommandBuilder()
- .setName("support")
- .setDescription("Get link to the support server"),
-
- async execute(interaction) {
- try {
- const supportButton = new ButtonBuilder()
- .setLabel("Join Support Server")
- .setStyle(ButtonStyle.Link)
- .setURL(SUPPORT_SERVER_URL);
-
- const actionRow = new ActionRowBuilder().addComponents(supportButton);
-
- await InteractionHelper.safeReply(interaction, {
- embeds: [
- createEmbed({ title: "Need Help?", description: "Join our official support server for assistance, report bugs, or suggest features. If you are customizing this bot, remember to change the link in the code!" }),
- ],
- components: [actionRow],
- flags: MessageFlags.Ephemeral,
- });
- } catch (error) {
- logger.error('Support command error:', error);
-
- try {
- return await InteractionHelper.safeReply(interaction, {
- embeds: [createEmbed({ title: 'System Error', description: 'Could not display support information.', color: 'error' })],
- flags: MessageFlags.Ephemeral,
- });
- } catch (replyError) {
- logger.error('Failed to send error reply:', replyError);
- }
- }
- },
-};
\ No newline at end of file
diff --git a/src/commands/Economy/eco.ts b/src/commands/Economy/eco.ts
new file mode 100644
index 0000000000..61ada2beb5
--- /dev/null
+++ b/src/commands/Economy/eco.ts
@@ -0,0 +1,43 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import balanceCommand from './modules/balance.js';
+import begCommand from './modules/beg.js';
+import buyCommand from './modules/buy.js';
+import crimeCommand from './modules/crime.js';
+import dailyCommand from './modules/daily.js';
+import depositCommand from './modules/deposit.js';
+import eleaderboardCommand from './modules/eleaderboard.js';
+import fishCommand from './modules/fish.js';
+import gambleCommand from './modules/gamble.js';
+import inventoryCommand from './modules/inventory.js';
+import mineCommand from './modules/mine.js';
+import payCommand from './modules/pay.js';
+import robCommand from './modules/rob.js';
+import shopCommand from './modules/shop.js';
+import slutCommand from './modules/slut.js';
+import withdrawCommand from './modules/withdraw.js';
+import workCommand from './modules/work.js';
+
+export default createRouterCommand({
+ name: 'eco',
+ description: 'Wirtschaft: Kontostand, Arbeiten, Shop und mehr',
+ category: 'Economy',
+ routes: [
+ { legacyName: 'balance', module: balanceCommand },
+ { legacyName: 'beg', module: begCommand },
+ { legacyName: 'buy', module: buyCommand },
+ { legacyName: 'crime', module: crimeCommand },
+ { legacyName: 'daily', module: dailyCommand },
+ { legacyName: 'deposit', module: depositCommand },
+ { legacyName: 'eleaderboard', module: eleaderboardCommand },
+ { legacyName: 'fish', module: fishCommand },
+ { legacyName: 'gamble', module: gambleCommand },
+ { legacyName: 'inventory', module: inventoryCommand },
+ { legacyName: 'mine', module: mineCommand },
+ { legacyName: 'pay', module: payCommand },
+ { legacyName: 'rob', module: robCommand },
+ { legacyName: 'shop', module: shopCommand },
+ { legacyName: 'slut', module: slutCommand },
+ { legacyName: 'withdraw', module: withdrawCommand },
+ { legacyName: 'work', module: workCommand },
+ ],
+});
diff --git a/src/commands/Economy/economy.ts b/src/commands/Economy/economy.ts
new file mode 100644
index 0000000000..f750112280
--- /dev/null
+++ b/src/commands/Economy/economy.ts
@@ -0,0 +1,15 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import economyCommand from './modules/economy.js';
+import shopConfigCommand from './modules/shop-config.js';
+
+export default createRouterCommand({
+ name: 'economy',
+ description: 'Wirtschaftssystem konfigurieren',
+ category: 'Economy',
+ defaultMemberPermissions: PermissionFlagsBits.ManageGuild,
+ routes: [
+ { legacyName: 'economy', module: economyCommand, spread: true },
+ { legacyName: 'shop-config', module: shopConfigCommand },
+ ],
+});
diff --git a/src/commands/Economy/balance.js b/src/commands/Economy/modules/balance.ts
similarity index 69%
rename from src/commands/Economy/balance.js
rename to src/commands/Economy/modules/balance.ts
index 95528ecd9c..546fd85944 100644
--- a/src/commands/Economy/balance.js
+++ b/src/commands/Economy/modules/balance.ts
@@ -1,18 +1,19 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, getMaxBankCapacity } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, getMaxBankCapacity } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName('balance')
- .setDescription("Check your or someone else's balance")
+ .setDescription("Zeige deinen Kontostand oder den eines anderen Nutzers")
.addUserOption(option =>
option
.setName('user')
- .setDescription('User to check balance for')
+ .setDescription('Nutzer, dessen Kontostand du sehen möchtest')
.setRequired(false)
),
@@ -22,9 +23,9 @@ export default {
const userOption = interaction.options.getUser("user");
const targetUser = userOption || interaction.user;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
- logger.info(`[ECONOMY] Balance check - userOption: ${userOption?.id || 'null'}, targetUser: ${targetUser.id}, guildId: ${guildId}, isPrefix: ${!!interaction._commandStartTime}`);
+ logger.info(`[ECONOMY] Balance check - userOption: ${userOption?.id || 'null'}, targetUser: ${targetUser.id}, guildId: ${guildId}, isPrefix: ${!!(interaction as typeof interaction & { _commandStartTime?: number })._commandStartTime}`);
logger.debug(`[ECONOMY] Balance check for ${targetUser.id}`, { userId: targetUser.id, guildId });
@@ -32,7 +33,7 @@ export default {
throw createError(
"Bot user queried for balance",
ErrorTypes.VALIDATION,
- "Bots don't have an economy balance."
+ "Bots haben keinen Kontostand."
);
}
@@ -44,7 +45,7 @@ export default {
throw createError(
"Failed to load economy data",
ErrorTypes.DATABASE,
- "Failed to load economy data. Please try again later.",
+ "Die Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId: targetUser.id, guildId }
);
}
@@ -55,12 +56,12 @@ export default {
const bank = typeof userData.bank === 'number' ? userData.bank : 0;
const embed = createEmbed({
- title: `${targetUser.username}'s Balance`,
- description: `Here is the current financial status for ${targetUser.username}.`,
+ title: `Kontostand von ${targetUser.username}`,
+ description: `Hier ist der aktuelle Kontostand von ${targetUser.username}.`,
})
.addFields(
{
- name: "💵 Cash",
+ name: "💵 Geldbörse",
value: `$${wallet.toLocaleString()}`,
inline: true,
},
@@ -70,13 +71,13 @@ export default {
inline: true,
},
{
- name: "💰 Total",
+ name: "💰 Gesamt",
value: `$${(wallet + bank).toLocaleString()}`,
inline: true,
}
)
.setFooter({
- text: `Requested by ${interaction.user.tag}`,
+ text: `Angefragt von ${interaction.user.tag}`,
iconURL: interaction.user.displayAvatarURL(),
});
@@ -84,4 +85,4 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'balance' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/beg.js b/src/commands/Economy/modules/beg.ts
similarity index 58%
rename from src/commands/Economy/beg.js
rename to src/commands/Economy/modules/beg.ts
index 9b2fc717f9..c25e64319f 100644
--- a/src/commands/Economy/beg.js
+++ b/src/commands/Economy/modules/beg.ts
@@ -1,9 +1,11 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { successEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { botConfig } from '../../config/bot.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { successEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import type { EconomyData } from '../../../utils/economy.js';
+import { botConfig } from '../../../config/bot.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
const COOLDOWN = 30 * 60 * 1000;
const MIN_WIN = Number(botConfig?.economy?.begMin) || 50;
@@ -13,22 +15,22 @@ const SUCCESS_CHANCE = 0.7;
export default {
data: new SlashCommandBuilder()
.setName('beg')
- .setDescription('Beg for a small amount of money'),
+ .setDescription('Bettle um ein bisschen Geld'),
execute: withErrorHandling(async (interaction, config, client) => {
const deferred = await InteractionHelper.safeDefer(interaction);
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
- let userData = await getEconomyData(client, guildId, userId);
+ let userData: EconomyData & { lastBeg?: number } = await getEconomyData(client, guildId, userId);
if (!userData) {
throw createError(
"Failed to load economy data",
ErrorTypes.DATABASE,
- "Failed to load your economy data. Please try again later.",
+ "Deine Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId, guildId }
);
}
@@ -41,12 +43,12 @@ export default {
const seconds = Math.floor((remainingTime % 60000) / 1000);
let timeMessage =
- minutes > 0 ? `${minutes} minute(s)` : `${seconds} second(s)`;
+ minutes > 0 ? `${minutes} Minute(n)` : `${seconds} Sekunde(n)`;
throw createError(
"Beg cooldown active",
ErrorTypes.RATE_LIMIT,
- `You are tired from begging! Try again in **${timeMessage}**.`,
+ `Du bist müde vom Betteln! Versuch es in **${timeMessage}** erneut.`,
{ remainingTime, minutes, seconds, cooldownType: 'beg' }
);
}
@@ -63,28 +65,28 @@ export default {
newCash += amountWon;
const successMessages = [
- `A kind stranger drops **$${amountWon.toLocaleString()}** into your cup.`,
- `You spotted an unattended wallet! You grab **$${amountWon.toLocaleString()}** and run.`,
- `Someone took pity on you and gave you **$${amountWon.toLocaleString()}**!`,
- `You found **$${amountWon.toLocaleString()}** under a park bench.`,
+ `Ein netter Fremder wirft **$${amountWon.toLocaleString()}** in deinen Becher.`,
+ `Du entdeckst eine herrenlose Geldbörse! Du schnappst dir **$${amountWon.toLocaleString()}** und rennst los.`,
+ `Jemand hatte Mitleid und hat dir **$${amountWon.toLocaleString()}** gegeben!`,
+ `Du hast **$${amountWon.toLocaleString()}** unter einer Parkbank gefunden.`,
];
replyEmbed = successEmbed(
- 'Begging Successful',
+ 'Betteln erfolgreich',
successMessages[
Math.floor(Math.random() * successMessages.length)
]
);
} else {
const failMessages = [
- "The police chased you off. You got nothing.",
- "Someone yelled, 'Get a job!' and walked past.",
- "A squirrel stole the single coin you had.",
- "You tried to beg, but you were too embarrassed and gave up.",
+ "Die Polizei hat dich verjagt. Du hast nichts bekommen.",
+ "Jemand rief „Such dir einen Job!“ und ging weiter.",
+ "Ein Eichhörnchen hat deine einzige Münze geklaut.",
+ "Du wolltest betteln, aber es war dir zu peinlich und du hast aufgegeben.",
];
replyEmbed = warningEmbed(
- 'Insufficient Funds',
+ 'Leider nichts bekommen',
failMessages[Math.floor(Math.random() * failMessages.length)]
);
}
@@ -96,4 +98,4 @@ userData.lastBeg = Date.now();
await InteractionHelper.safeEditReply(interaction, { embeds: [replyEmbed] });
}, { command: 'beg' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/modules/buy.ts
similarity index 65%
rename from src/commands/Economy/buy.js
rename to src/commands/Economy/modules/buy.ts
index 1a90387315..c1cc332462 100644
--- a/src/commands/Economy/buy.js
+++ b/src/commands/Economy/modules/buy.ts
@@ -1,27 +1,28 @@
-import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { shopItems } from '../../config/shop/items.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { getGuildConfig } from '../../services/config/guildConfig.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, MessageFlags, type GuildMember } from 'discord.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { shopItems } from '../../../config/shop/items.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import { getGuildConfig } from '../../../services/config/guildConfig.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
const SHOP_ITEMS = shopItems;
export default {
data: new SlashCommandBuilder()
.setName('buy')
- .setDescription('Buy an item from the shop')
+ .setDescription('Kauf ein Item im Shop')
.addStringOption(option =>
option
.setName('item_id')
- .setDescription('ID of the item to buy')
+ .setDescription('ID des Items, das du kaufen möchtest')
.setRequired(true)
)
.addIntegerOption(option =>
option
.setName('quantity')
- .setDescription('Quantity to buy (default: 1)')
+ .setDescription('Anzahl (Standard: 1)')
.setRequired(false)
.setMinValue(1)
.setMaxValue(10)
@@ -32,8 +33,8 @@ export default {
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
- const itemId = interaction.options.getString("item_id").toLowerCase();
+ const guildId = interaction.guildId!;
+ const itemId = interaction.options.getString("item_id")!.toLowerCase();
const quantity = interaction.options.getInteger("quantity") || 1;
const item = SHOP_ITEMS.find(i => i.id === itemId);
@@ -42,7 +43,7 @@ export default {
throw createError(
`Item ${itemId} not found`,
ErrorTypes.VALIDATION,
- `The item ID \`${itemId}\` does not exist in the shop.`,
+ `Die Item-ID \`${itemId}\` gibt es im Shop nicht.`,
{ itemId }
);
}
@@ -51,7 +52,7 @@ export default {
throw createError(
"Invalid quantity",
ErrorTypes.VALIDATION,
- "You must purchase a quantity of 1 or more.",
+ "Du musst mindestens 1 Stück kaufen.",
{ quantity }
);
}
@@ -67,7 +68,7 @@ export default {
throw createError(
"Insufficient funds",
ErrorTypes.VALIDATION,
- `You need **$${totalCost.toLocaleString()}** to purchase ${quantity}x **${item.name}**, but you only have **$${userData.wallet.toLocaleString()}** in cash.`,
+ `Du brauchst **$${totalCost.toLocaleString()}** für ${quantity}x **${item.name}**, hast aber nur **$${userData.wallet.toLocaleString()}** in deiner Geldbörse.`,
{ required: totalCost, current: userData.wallet, itemId, quantity }
);
}
@@ -77,15 +78,15 @@ export default {
throw createError(
"Premium role not configured",
ErrorTypes.CONFIGURATION,
- "The **Premium Shop Role** has not been configured by a server administrator yet.",
+ "Die **Premium-Shop-Rolle** wurde noch nicht von einem Server-Admin eingerichtet.",
{ itemId }
);
}
- if (interaction.member.roles.cache.has(PREMIUM_ROLE_ID)) {
+ if ((interaction.member as GuildMember).roles.cache.has(PREMIUM_ROLE_ID)) {
throw createError(
"Role already owned",
ErrorTypes.VALIDATION,
- `You already have the **${item.name}** role.`,
+ `Du hast die Rolle **${item.name}** bereits.`,
{ itemId, roleId: PREMIUM_ROLE_ID }
);
}
@@ -93,7 +94,7 @@ export default {
throw createError(
"Invalid quantity for role",
ErrorTypes.VALIDATION,
- `You can only purchase the **${item.name}** role once.`,
+ `Du kannst die Rolle **${item.name}** nur einmal kaufen.`,
{ itemId, quantity }
);
}
@@ -101,18 +102,18 @@ export default {
userData.wallet -= totalCost;
- let successDescription = `You successfully purchased ${quantity}x **${item.name}** for **$${totalCost.toLocaleString()}**!`;
+ let successDescription = `Du hast ${quantity}x **${item.name}** für **$${totalCost.toLocaleString()}** gekauft!`;
if (item.type === "role" && itemId === "premium_role") {
- const member = interaction.member;
+ const member = interaction.member as GuildMember;
- const role = interaction.guild.roles.cache.get(PREMIUM_ROLE_ID);
+ const role = interaction.guild!.roles.cache.get(PREMIUM_ROLE_ID!);
if (!role) {
throw createError(
"Role not found",
ErrorTypes.CONFIGURATION,
- "The configured premium role no longer exists in this guild.",
+ "Die eingerichtete Premium-Rolle existiert auf diesem Server nicht mehr.",
{ roleId: PREMIUM_ROLE_ID }
);
}
@@ -120,41 +121,41 @@ export default {
try {
await member.roles.add(
role,
- `Purchased role: ${item.name}`,
+ `Rolle gekauft: ${item.name}`,
);
- successDescription += `\n\n**👑 The role ${role.toString()} has been granted to you!**`;
+ successDescription += `\n\n**👑 Du hast die Rolle ${role.toString()} erhalten!**`;
} catch (roleError) {
userData.wallet += totalCost;
await setEconomyData(client, guildId, userId, userData);
throw createError(
"Role assignment failed",
ErrorTypes.DISCORD_API,
- "Successfully deducted money, but failed to grant the role. Your cash has been refunded.",
- { roleId: PREMIUM_ROLE_ID, originalError: roleError.message }
+ "Das Geld wurde abgebucht, aber die Rolle konnte nicht vergeben werden. Du hast dein Geld zurückerhalten.",
+ { roleId: PREMIUM_ROLE_ID, originalError: roleError instanceof Error ? roleError.message : String(roleError) }
);
}
} else if (item.type === "upgrade") {
- userData.upgrades[itemId] = true;
- successDescription += `\n\n**✨ Your upgrade is now active!**`;
+ userData.upgrades![itemId] = true;
+ successDescription += `\n\n**✨ Dein Upgrade ist jetzt aktiv!**`;
} else if (item.type === "consumable" || item.type === "tool") {
userData.inventory[itemId] =
(userData.inventory[itemId] || 0) + quantity;
if (item.type === "tool") {
- successDescription += `\n\n**🛠️ ${item.name} added to your inventory!**`;
+ successDescription += `\n\n**🛠️ ${item.name} wurde deinem Inventar hinzugefügt!**`;
}
}
await setEconomyData(client, guildId, userId, userData);
const embed = successEmbed(
- "💰 Purchase Successful",
+ "💰 Kauf erfolgreich",
successDescription,
).addFields({
- name: "New Balance",
+ name: "Neuer Kontostand",
value: `$${userData.wallet.toLocaleString()}`,
inline: true,
});
await InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: [MessageFlags.Ephemeral] });
}, { command: 'buy' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/crime.js b/src/commands/Economy/modules/crime.ts
similarity index 60%
rename from src/commands/Economy/crime.js
rename to src/commands/Economy/modules/crime.ts
index 101dc5a575..91786ea101 100644
--- a/src/commands/Economy/crime.js
+++ b/src/commands/Economy/modules/crime.ts
@@ -1,36 +1,38 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import type { EconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
const CRIME_COOLDOWN = 60 * 60 * 1000;
const JAIL_TIME = 2 * 60 * 60 * 1000;
const FINE_RATE = 0.2;
const CRIME_TYPES = [
- { name: "Pickpocketing", min: 100, max: 500, risk: 0.3 },
- { name: "Burglary", min: 300, max: 1000, risk: 0.4 },
- { name: "Bank Heist", min: 1000, max: 5000, risk: 0.6 },
- { name: "Art Theft", min: 2000, max: 10000, risk: 0.7 },
- { name: "Cybercrime", min: 5000, max: 20000, risk: 0.8 },
+ { name: "Pickpocketing", label: "Taschendiebstahl", min: 100, max: 500, risk: 0.3 },
+ { name: "Burglary", label: "Einbruch", min: 300, max: 1000, risk: 0.4 },
+ { name: "Bank Heist", label: "Banküberfall", min: 1000, max: 5000, risk: 0.6 },
+ { name: "Art Theft", label: "Kunstraub", min: 2000, max: 10000, risk: 0.7 },
+ { name: "Cybercrime", label: "Cyberkriminalität", min: 5000, max: 20000, risk: 0.8 },
];
export default {
data: new SlashCommandBuilder()
.setName('crime')
- .setDescription('Commit a crime to earn money (risky)')
+ .setDescription('Begehe ein Verbrechen, um Geld zu verdienen (riskant)')
.addStringOption(option =>
option
.setName('type')
- .setDescription('Type of crime to commit')
+ .setDescription('Art des Verbrechens')
.setRequired(true)
.addChoices(
- { name: 'Pickpocketing', value: 'pickpocketing' },
- { name: 'Burglary', value: 'burglary' },
- { name: 'Bank Heist', value: 'bank-heist' },
- { name: 'Art Theft', value: 'art-theft' },
- { name: 'Cybercrime', value: 'cybercrime' },
+ { name: 'Taschendiebstahl', value: 'pickpocketing' },
+ { name: 'Einbruch', value: 'burglary' },
+ { name: 'Banküberfall', value: 'bank-heist' },
+ { name: 'Kunstraub', value: 'art-theft' },
+ { name: 'Cyberkriminalität', value: 'cybercrime' },
)
),
@@ -38,20 +40,20 @@ export default {
await InteractionHelper.safeDefer(interaction);
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const now = Date.now();
- const userData = await getEconomyData(client, guildId, userId);
+ const userData: EconomyData & { jailedUntil?: number } = await getEconomyData(client, guildId, userId);
const lastCrime = userData.cooldowns?.crime || 0;
const isJailed = userData.jailedUntil && userData.jailedUntil > now;
if (isJailed) {
- const timeLeft = Math.ceil((userData.jailedUntil - now) / (1000 * 60));
+ const timeLeft = Math.ceil((userData.jailedUntil! - now) / (1000 * 60));
throw createError(
"User is in jail",
ErrorTypes.RATE_LIMIT,
- `You're in jail for ${timeLeft} more minutes!`,
- { jailTimeRemaining: userData.jailedUntil - now }
+ `Du sitzt noch ${timeLeft} Minuten im Gefängnis!`,
+ { jailTimeRemaining: userData.jailedUntil! - now }
);
}
@@ -60,12 +62,12 @@ export default {
throw createError(
"Crime cooldown active",
ErrorTypes.RATE_LIMIT,
- `You need to wait ${timeLeft} more minutes before committing another crime.`,
+ `Du musst noch ${timeLeft} Minuten warten, bevor du das nächste Verbrechen begehen kannst.`,
{ remaining: lastCrime + CRIME_COOLDOWN - now, cooldownType: 'crime' }
);
}
- const crimeType = interaction.options.getString("type").toLowerCase();
+ const crimeType = interaction.options.getString("type")!.toLowerCase();
const crime = CRIME_TYPES.find(
c => c.name.toLowerCase().replace(/\s+/g, '-') === crimeType
);
@@ -74,7 +76,7 @@ export default {
throw createError(
"Invalid crime type",
ErrorTypes.VALIDATION,
- "Please select a valid crime type.",
+ "Bitte wähle eine gültige Verbrechensart.",
{ crimeType }
);
}
@@ -93,8 +95,8 @@ export default {
await setEconomyData(client, guildId, userId, userData);
const embed = successEmbed(
- "🕵️ Crime Successful!",
- `You successfully committed ${crime.name} and earned **${amountEarned}** coins!`
+ "🕵️ Verbrechen erfolgreich!",
+ `Dein Coup „${crime.label}“ war erfolgreich und hat dir **${amountEarned}** Münzen eingebracht!`
);
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
@@ -108,12 +110,12 @@ export default {
await setEconomyData(client, guildId, userId, userData);
const embed = warningEmbed(
- "🚔 Crime Failed!",
- `You were caught while attempting ${crime.name} and have been sent to jail! ` +
- `You were fined ${fine.toLocaleString()} coins and will be in jail for 2 hours.`
+ "🚔 Verbrechen gescheitert!",
+ `Du wurdest bei „${crime.label}“ erwischt und ins Gefängnis gesteckt! ` +
+ `Du musst ${fine.toLocaleString()} Münzen Strafe zahlen und sitzt 2 Stunden im Gefängnis.`
);
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}
}, { command: 'crime' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/daily.js b/src/commands/Economy/modules/daily.ts
similarity index 66%
rename from src/commands/Economy/daily.js
rename to src/commands/Economy/modules/daily.ts
index 4045d23ed0..69df467e5d 100644
--- a/src/commands/Economy/daily.js
+++ b/src/commands/Economy/modules/daily.ts
@@ -1,12 +1,13 @@
-import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { getGuildConfig } from '../../services/config/guildConfig.js';
-import { formatDuration } from '../../utils/embeds.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { botConfig } from '../../config/bot.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, type GuildMember } from 'discord.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import { getGuildConfig } from '../../../services/config/guildConfig.js';
+import { formatDuration } from '../../../utils/embeds.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { botConfig } from '../../../config/bot.js';
const DAILY_COOLDOWN = 24 * 60 * 60 * 1000;
const DAILY_AMOUNT = botConfig.economy?.dailyAmount ?? 100;
@@ -15,14 +16,14 @@ const PREMIUM_BONUS_PERCENTAGE = 0.1;
export default {
data: new SlashCommandBuilder()
.setName('daily')
- .setDescription('Claim your daily cash reward'),
+ .setDescription('Hol dir deine tägliche Belohnung'),
execute: withErrorHandling(async (interaction, config, client) => {
const deferred = await InteractionHelper.safeDefer(interaction);
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const now = Date.now();
logger.debug(`[ECONOMY] Daily claimed started for ${userId}`, { userId, guildId });
@@ -33,7 +34,7 @@ export default {
throw createError(
"Failed to load economy data for daily",
ErrorTypes.DATABASE,
- "Failed to load your economy data. Please try again later.",
+ "Deine Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId, guildId }
);
}
@@ -45,7 +46,7 @@ export default {
throw createError(
"Daily cooldown active",
ErrorTypes.RATE_LIMIT,
- `You need to wait before claiming daily again. Try again in **${formatDuration(timeRemaining)}**.`,
+ `Du musst noch warten, bevor du deine tägliche Belohnung wieder abholen kannst. Versuch es in **${formatDuration(timeRemaining)}** erneut.`,
{ timeRemaining, cooldownType: 'daily' }
);
}
@@ -60,13 +61,13 @@ export default {
if (
PREMIUM_ROLE_ID &&
interaction.member &&
- interaction.member.roles.cache.has(PREMIUM_ROLE_ID)
+ (interaction.member as GuildMember).roles.cache.has(PREMIUM_ROLE_ID)
) {
const bonusAmount = Math.floor(
DAILY_AMOUNT * PREMIUM_BONUS_PERCENTAGE,
);
earned += bonusAmount;
- bonusMessage = `\n✨ **Premium Bonus:** +$${bonusAmount.toLocaleString()}`;
+ bonusMessage = `\n✨ **Premium-Bonus:** +$${bonusAmount.toLocaleString()}`;
hasPremiumRole = true;
}
@@ -85,20 +86,20 @@ export default {
});
const embed = successEmbed(
- "✅ Daily Claimed!",
- `You have claimed your daily **$${earned.toLocaleString()}**!${bonusMessage}`
+ "✅ Tägliche Belohnung abgeholt!",
+ `Du hast deine täglichen **$${earned.toLocaleString()}** abgeholt!${bonusMessage}`
)
.addFields({
- name: "New Cash Balance",
+ name: "Geldbörse (neu)",
value: `$${userData.wallet.toLocaleString()}`,
inline: true,
})
.setFooter({
text: hasPremiumRole
- ? `Next claim in 24 hours. (Premium Active)`
- : `Next claim in 24 hours.`,
+ ? `Nächste Abholung in 24 Stunden. (Premium aktiv)`
+ : `Nächste Abholung in 24 Stunden.`,
});
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'daily' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/deposit.js b/src/commands/Economy/modules/deposit.ts
similarity index 71%
rename from src/commands/Economy/deposit.js
rename to src/commands/Economy/modules/deposit.ts
index 56015b5fb4..ad4e938f8d 100644
--- a/src/commands/Economy/deposit.js
+++ b/src/commands/Economy/modules/deposit.ts
@@ -1,17 +1,18 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { successEmbed, buildUserErrorEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { successEmbed, buildUserErrorEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName('deposit')
- .setDescription('Deposit money from your wallet into your bank')
+ .setDescription('Zahle Geld aus deiner Geldbörse auf deine Bank ein')
.addStringOption(option =>
option
.setName('amount')
- .setDescription('Amount to deposit (number or "all")')
+ .setDescription('Betrag zum Einzahlen (Zahl oder "all")')
.setRequired(true)
),
@@ -20,8 +21,8 @@ export default {
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
- const amountInput = interaction.options.getString("amount");
+ const guildId = interaction.guildId!;
+ const amountInput = interaction.options.getString("amount")!;
const userData = await getEconomyData(client, guildId, userId);
@@ -29,7 +30,7 @@ export default {
throw createError(
"Failed to load economy data",
ErrorTypes.DATABASE,
- "Failed to load your economy data. Please try again later.",
+ "Deine Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId, guildId }
);
}
@@ -46,7 +47,7 @@ export default {
throw createError(
"Invalid deposit amount",
ErrorTypes.VALIDATION,
- `Please enter a valid number or 'all'. You entered: \`${amountInput}\``,
+ `Bitte gib eine gültige Zahl oder „all“ ein. Deine Eingabe: \`${amountInput}\``,
{ amountInput, userId }
);
}
@@ -56,7 +57,7 @@ export default {
throw createError(
"Zero deposit amount",
ErrorTypes.VALIDATION,
- "You have no cash to deposit.",
+ "Du hast kein Geld zum Einzahlen.",
{ userId, walletBalance: userData.wallet }
);
}
@@ -67,7 +68,7 @@ export default {
embeds: [
buildUserErrorEmbed(
'validation',
- `You tried to deposit more than you have. Depositing your remaining cash: **$${depositAmount.toLocaleString()}**`
+ `Du wolltest mehr einzahlen, als du hast. Dein restliches Geld wird eingezahlt: **$${depositAmount.toLocaleString()}**`
)
],
flags: MessageFlags.Ephemeral,
@@ -80,7 +81,7 @@ export default {
throw createError(
"Bank is full",
ErrorTypes.VALIDATION,
- `Your bank is currently full (Max Capacity: $${maxBank.toLocaleString()}). Purchase a **Bank Upgrade** to increase your limit.`,
+ `Deine Bank ist voll (max. Kapazität: $${maxBank.toLocaleString()}). Kauf ein **Bank-Upgrade**, um dein Limit zu erhöhen.`,
{ maxBank, currentBank: userData.bank, userId }
);
}
@@ -94,7 +95,7 @@ export default {
embeds: [
buildUserErrorEmbed(
'validation',
- `You only had space for **$${depositAmount.toLocaleString()}** in your bank account (Max: $${maxBank.toLocaleString()}). The rest remains in your cash.`
+ `Auf deinem Bankkonto war nur noch Platz für **$${depositAmount.toLocaleString()}** (max.: $${maxBank.toLocaleString()}). Der Rest bleibt in deiner Geldbörse.`
)
],
flags: MessageFlags.Ephemeral,
@@ -106,7 +107,7 @@ export default {
throw createError(
"No space or cash for deposit",
ErrorTypes.VALIDATION,
- "The amount you tried to deposit was either 0 or exceeded your bank capacity after checking your cash balance.",
+ "Der Betrag war entweder 0 oder hat nach Prüfung deiner Geldbörse die Kapazität deiner Bank überschritten.",
{ depositAmount, availableSpace, walletBalance: userData.wallet }
);
}
@@ -117,17 +118,17 @@ export default {
await setEconomyData(client, guildId, userId, userData);
const embed = successEmbed(
- 'Deposit Successful',
- `You successfully deposited **$${depositAmount.toLocaleString()}** into your bank.`
+ 'Einzahlung erfolgreich',
+ `Du hast **$${depositAmount.toLocaleString()}** auf deine Bank eingezahlt.`
)
.addFields(
{
- name: "New Cash Balance",
+ name: "Geldbörse (neu)",
value: `$${userData.wallet.toLocaleString()}`,
inline: true,
},
{
- name: "New Bank Balance",
+ name: "Bank (neu)",
value: `$${userData.bank.toLocaleString()} / $${maxBank.toLocaleString()}`,
inline: true,
},
@@ -135,4 +136,4 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'deposit' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/economy.js b/src/commands/Economy/modules/economy.ts
similarity index 68%
rename from src/commands/Economy/economy.js
rename to src/commands/Economy/modules/economy.ts
index d702eb6157..dda7f8092d 100644
--- a/src/commands/Economy/economy.js
+++ b/src/commands/Economy/modules/economy.ts
@@ -1,19 +1,20 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import economyDashboard from './modules/economy_dashboard.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import economyDashboard from './economy_dashboard.js';
export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName('economy')
- .setDescription('Economy management commands')
+ .setDescription('Commands zur Verwaltung der Wirtschaft')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.setDMPermission(false)
.addSubcommand(subcommand =>
subcommand
.setName('dashboard')
- .setDescription('Open the economy management dashboard')
+ .setDescription('Öffne das Wirtschafts-Dashboard')
),
category: 'Economy',
@@ -29,4 +30,4 @@ export default {
await economyDashboard.execute(interaction, config, client);
}
}
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/modules/economy_dashboard.js b/src/commands/Economy/modules/economy_dashboard.ts
similarity index 58%
rename from src/commands/Economy/modules/economy_dashboard.js
rename to src/commands/Economy/modules/economy_dashboard.ts
index 04f507ff97..2fec384131 100644
--- a/src/commands/Economy/modules/economy_dashboard.js
+++ b/src/commands/Economy/modules/economy_dashboard.ts
@@ -12,14 +12,20 @@ import {
MessageFlags,
ComponentType,
EmbedBuilder,
+ type ChatInputCommandInteraction,
+ type Guild,
+ type StringSelectMenuInteraction,
+ type SelectMenuModalData,
} from 'discord.js';
-import { getColor, BotConfig } from '../../../config/bot.js';
+import { getColor, BotConfig, isBotOwner } from '../../../config/bot.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { successEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
import { getEconomyPrefix } from '../../../utils/database.js';
import { getEconomyData, addMoney, removeMoney, getMaxBankCapacity } from '../../../utils/economy.js';
+import type { EconomyData } from '../../../utils/economy.js';
+import type { Command, NexoClient, SubcommandModule } from '../../../types/index.js';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
@@ -27,7 +33,7 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
-async function buildDashboardEmbed(guild, client) {
+async function buildDashboardEmbed(guild: Guild, client: NexoClient) {
const currencySymbol = BotConfig.economy.currency.symbol;
const currencyName = BotConfig.economy.currency.name;
@@ -41,10 +47,10 @@ async function buildDashboardEmbed(guild, client) {
for (const key of economyKeys) {
const userId = key.split(':').pop();
- const member = await guild.members.fetch(userId).catch(() => null);
+ const member = await guild.members.fetch(userId!).catch(() => null);
if (member?.user?.bot) continue;
- const userData = await client.db.get(key, {});
+ const userData = await client.db.get>(key, {});
if (userData) {
totalInCirculation += (userData.wallet || 0) + (userData.bank || 0);
userCount++;
@@ -58,76 +64,92 @@ async function buildDashboardEmbed(guild, client) {
const avgBalance = userCount > 0 ? Math.floor(totalInCirculation / userCount) : 0;
return new EmbedBuilder()
- .setTitle('💰 Economy Dashboard')
- .setDescription(`Manage the economy system for **${guild.name}**.\nSelect an option below to perform an action.`)
+ .setTitle('💰 Wirtschafts-Dashboard')
+ .setDescription(`Verwalte das Wirtschaftssystem von **${guild.name}**.\nWähle unten eine Option aus.`)
.setColor(getColor('economy'))
.addFields(
- { name: '💰 Total in Circulation', value: `\`${currencySymbol}${totalInCirculation.toLocaleString()}\``, inline: true },
- { name: '👥 Active Users', value: `\`${userCount.toLocaleString()}\``, inline: true },
- { name: '📊 Average Balance', value: `\`${currencySymbol}${avgBalance.toLocaleString()}\``, inline: true },
- { name: '💱 Currency Symbol', value: `\`${currencySymbol}\``, inline: true },
- { name: '📝 Currency Name', value: `\`${currencyName}\``, inline: true },
+ { name: '💰 Gesamt im Umlauf', value: `\`${currencySymbol}${totalInCirculation.toLocaleString()}\``, inline: true },
+ { name: '👥 Aktive Nutzer', value: `\`${userCount.toLocaleString()}\``, inline: true },
+ { name: '📊 Durchschnittlicher Kontostand', value: `\`${currencySymbol}${avgBalance.toLocaleString()}\``, inline: true },
+ { name: '💱 Währungssymbol', value: `\`${currencySymbol}\``, inline: true },
+ { name: '📝 Währungsname', value: `\`${currencyName}\``, inline: true },
)
- .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität' })
.setTimestamp();
}
-function buildSelectMenu(guildId) {
+function buildSelectMenu(guildId: string) {
return new StringSelectMenuBuilder()
.setCustomId(`economy_dashboard_${guildId}`)
- .setPlaceholder('Select an action...')
+ .setPlaceholder('Wähle eine Aktion...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Add Currency')
- .setDescription('Add currency to a user\'s wallet or bank')
+ .setLabel('Währung hinzufügen')
+ .setDescription('Füge der Geldbörse oder Bank eines Nutzers Währung hinzu')
.setValue('add_currency')
.setEmoji('💰'),
new StringSelectMenuOptionBuilder()
- .setLabel('Remove Currency')
- .setDescription('Remove currency from a user\'s wallet or bank')
+ .setLabel('Währung entfernen')
+ .setDescription('Entferne Währung aus der Geldbörse oder Bank eines Nutzers')
.setValue('remove_currency')
.setEmoji('💸'),
new StringSelectMenuOptionBuilder()
- .setLabel('Change Currency Symbol')
- .setDescription('Change the currency symbol (e.g., $, €, £)')
+ .setLabel('Währungssymbol ändern')
+ .setDescription('Ändere das Währungssymbol (z. B. $, €, £)')
.setValue('change_currency')
.setEmoji('💱'),
new StringSelectMenuOptionBuilder()
- .setLabel('Change Currency Name')
- .setDescription('Change the currency name (e.g., coins, credits)')
+ .setLabel('Währungsname ändern')
+ .setDescription('Ändere den Namen der Währung (z. B. Münzen, Credits)')
.setValue('change_name')
.setEmoji('📝'),
);
}
-async function refreshDashboard(rootInteraction, guild, client) {
+async function refreshDashboard(rootInteraction: ChatInputCommandInteraction, guild: Guild, client: NexoClient) {
const selectMenu = buildSelectMenu(guild.id);
await InteractionHelper.safeEditReply(rootInteraction, {
embeds: [await buildDashboardEmbed(guild, client)],
components: [
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
}).catch(() => {});
}
-async function updateConfigFile(currencySymbol, currencyName) {
+/**
+ * Quoted literal for a value written into the bot config source, or null when the value could break
+ * the file or the regexes below (quotes, backslashes, line breaks).
+ */
+export function toConfigLiteral(value: string): string | null {
+ return /["\\\r\n]/.test(value) ? null : JSON.stringify(value);
+}
+
+async function updateConfigFile(currencySymbol: string, currencyName: string) {
+ const symbolLiteral = toConfigLiteral(currencySymbol);
+ const nameLiteral = toConfigLiteral(currencyName);
+ if (!symbolLiteral || !nameLiteral) {
+ return false;
+ }
+
try {
- const configPath = path.join(__dirname, '../../../config/bot.js');
+ // tsx dev runs from src/ (config/bot.ts); the compiled build runs from dist/ (config/bot.js).
+ const tsConfigPath = path.join(__dirname, '../../../config/bot.ts');
+ const configPath = await fs.access(tsConfigPath).then(() => tsConfigPath, () => path.join(__dirname, '../../../config/bot.js'));
let configContent = await fs.readFile(configPath, 'utf-8');
configContent = configContent.replace(
/symbol:\s*"[^"]*"/,
- `symbol: "${currencySymbol}"`
+ `symbol: ${symbolLiteral}`
);
configContent = configContent.replace(
- /name:\s*"[^"]*",\s*\/\/\s*Currency display name/,
- `name: "${currencyName}", // Currency display name`
+ /(\/\/\s*Currency display name\.?\s*\n\s*)name:\s*"[^"]*"/,
+ (_match: string, prefix: string) => `${prefix}name: ${nameLiteral}`
);
configContent = configContent.replace(
- /namePlural:\s*"[^"]*",\s*\/\/\s*Plural display name/,
- `namePlural: "${currencyName}s", // Plural display name`
+ /(\/\/\s*Plural display name\.?\s*\n\s*)namePlural:\s*"[^"]*"/,
+ (_match: string, prefix: string) => `${prefix}namePlural: ${nameLiteral}`
);
await fs.writeFile(configPath, configContent, 'utf-8');
@@ -143,16 +165,16 @@ export default {
prefixOnly: false,
async execute(interaction, config, client) {
try {
- const guild = interaction.guild;
+ const guild = interaction.guild!;
const selectMenu = buildSelectMenu(guild.id);
- const selectRow = new ActionRowBuilder().addComponents(selectMenu);
+ const selectRow = new ActionRowBuilder().addComponents(selectMenu);
await InteractionHelper.safeEditReply(interaction, {
embeds: [await buildDashboardEmbed(guild, client)],
components: [selectRow],
});
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === interaction.user.id && i.customId === `economy_dashboard_${guild.id}`,
@@ -177,16 +199,16 @@ export default {
break;
}
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
logger.debug(`Economy dashboard validation error: ${error.message}`);
} else {
logger.error('Unexpected economy dashboard error:', error);
}
const errorMessage =
- error instanceof TitanBotError
- ? error.userMessage || 'An error occurred while processing your selection.'
- : 'An unexpected error occurred while processing your request.';
+ error instanceof NexoBotError
+ ? error.userMessage || 'Beim Verarbeiten deiner Auswahl ist ein Fehler aufgetreten.'
+ : 'Beim Verarbeiten deiner Anfrage ist ein unerwarteter Fehler aufgetreten.';
if (!selectInteraction.replied && !selectInteraction.deferred) {
await selectInteraction.deferUpdate().catch(() => {});
@@ -202,8 +224,8 @@ export default {
collector.on('end', async (collected, reason) => {
if (reason === 'time') {
const timeoutEmbed = new EmbedBuilder()
- .setTitle('Dashboard Timed Out')
- .setDescription('This dashboard has been closed due to inactivity. Please run the command again to continue.')
+ .setTitle('Dashboard abgelaufen')
+ .setDescription('Dieses Dashboard wurde wegen Inaktivität geschlossen. Führe den Command erneut aus, um weiterzumachen.')
.setColor(getColor('error'));
await InteractionHelper.safeEditReply(interaction, {
@@ -213,37 +235,37 @@ export default {
}
});
} catch (error) {
- if (error instanceof TitanBotError) throw error;
+ if (error instanceof NexoBotError) throw error;
logger.error('Unexpected error in economy_dashboard:', error);
- throw new TitanBotError(
- `Economy dashboard failed: ${error.message}`,
+ throw new NexoBotError(
+ `Economy dashboard failed: ${error instanceof Error ? error.message : String(error)}`,
ErrorTypes.UNKNOWN,
- 'Failed to open the economy dashboard.',
+ 'Das Wirtschafts-Dashboard konnte nicht geöffnet werden.',
);
}
},
-};
+} satisfies SubcommandModule & Pick;
-async function handleAddCurrency(selectInteraction, rootInteraction, guild, client) {
+async function handleAddCurrency(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guild: Guild, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId(`economy_add_currency_${guild.id}`)
- .setTitle('Add Currency');
+ .setTitle('Währung hinzufügen');
const userSelect = new UserSelectMenuBuilder()
.setCustomId('target_user')
- .setPlaceholder('Select a user...')
+ .setPlaceholder('Wähle einen Nutzer...')
.setMinValues(1)
.setMaxValues(1)
.setRequired(true);
const userLabel = new LabelBuilder()
- .setLabel('Target User')
- .setDescription('User to add currency to')
+ .setLabel('Ziel-Nutzer')
+ .setDescription('Nutzer, dem Währung hinzugefügt wird')
.setUserSelectMenuComponent(userSelect);
const amountInput = new TextInputBuilder()
.setCustomId('amount')
- .setLabel('Amount to add')
+ .setLabel('Betrag zum Hinzufügen')
.setStyle(TextInputStyle.Short)
.setPlaceholder('100')
.setMinLength(1)
@@ -252,7 +274,7 @@ async function handleAddCurrency(selectInteraction, rootInteraction, guild, clie
const typeInput = new TextInputBuilder()
.setCustomId('type')
- .setLabel('Type (wallet or bank)')
+ .setLabel('Typ (wallet = Geldbörse, bank = Bank)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('wallet')
.setMinLength(1)
@@ -261,8 +283,8 @@ async function handleAddCurrency(selectInteraction, rootInteraction, guild, clie
modal.addLabelComponents(userLabel);
modal.addComponents(
- new ActionRowBuilder().addComponents(amountInput),
- new ActionRowBuilder().addComponents(typeInput),
+ new ActionRowBuilder().addComponents(amountInput),
+ new ActionRowBuilder().addComponents(typeInput),
);
await selectInteraction.showModal(modal);
@@ -276,28 +298,28 @@ async function handleAddCurrency(selectInteraction, rootInteraction, guild, clie
if (!submitted) return;
- const userId = submitted.fields.getField('target_user').values[0];
+ const userId = (submitted.fields.getField('target_user') as SelectMenuModalData).values[0];
const amount = parseInt(submitted.fields.getTextInputValue('amount').trim(), 10);
const type = submitted.fields.getTextInputValue('type').trim().toLowerCase();
if (isNaN(amount) || amount <= 0) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Amount must be a positive number.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Der Betrag muss eine positive Zahl sein.' });
return;
}
if (type !== 'wallet' && type !== 'bank') {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Type must be either "wallet" or "bank".' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Der Typ muss entweder „wallet“ oder „bank“ sein.' });
return;
}
const member = await guild.members.fetch(userId).catch(() => null);
if (!member) {
- await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'The specified user is not in this server.' });
+ await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'Der angegebene Nutzer ist nicht auf diesem Server.' });
return;
}
if (member.user.bot) {
- await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Bots do not have economy accounts.' });
+ await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Bots haben kein Wirtschaftskonto.' });
return;
}
@@ -306,7 +328,7 @@ async function handleAddCurrency(selectInteraction, rootInteraction, guild, clie
const currencySymbol = BotConfig.economy.currency.symbol;
await submitted.reply({
- embeds: [successEmbed('Currency Added', `Successfully added ${currencySymbol}${amount.toLocaleString()} to ${member.user.tag}'s ${type}.\n**New Balance:** ${currencySymbol}${newBalance.toLocaleString()}`)],
+ embeds: [successEmbed('Währung hinzugefügt', `${currencySymbol}${amount.toLocaleString()} wurden ${type === 'wallet' ? 'der Geldbörse' : 'der Bank'} von ${member.user.tag} hinzugefügt.\n**Neuer Kontostand:** ${currencySymbol}${newBalance.toLocaleString()}`)],
flags: MessageFlags.Ephemeral,
});
@@ -321,26 +343,26 @@ async function handleAddCurrency(selectInteraction, rootInteraction, guild, clie
await refreshDashboard(rootInteraction, guild, client);
}
-async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, client) {
+async function handleRemoveCurrency(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guild: Guild, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId(`economy_remove_currency_${guild.id}`)
- .setTitle('Remove Currency');
+ .setTitle('Währung entfernen');
const userSelect = new UserSelectMenuBuilder()
.setCustomId('target_user')
- .setPlaceholder('Select a user...')
+ .setPlaceholder('Wähle einen Nutzer...')
.setMinValues(1)
.setMaxValues(1)
.setRequired(true);
const userLabel = new LabelBuilder()
- .setLabel('Target User')
- .setDescription('User to remove currency from')
+ .setLabel('Ziel-Nutzer')
+ .setDescription('Nutzer, dem Währung entfernt wird')
.setUserSelectMenuComponent(userSelect);
const amountInput = new TextInputBuilder()
.setCustomId('amount')
- .setLabel('Amount to remove')
+ .setLabel('Betrag zum Entfernen')
.setStyle(TextInputStyle.Short)
.setPlaceholder('100')
.setMinLength(1)
@@ -349,7 +371,7 @@ async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, c
const typeInput = new TextInputBuilder()
.setCustomId('type')
- .setLabel('Type (wallet or bank)')
+ .setLabel('Typ (wallet = Geldbörse, bank = Bank)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('wallet')
.setMinLength(1)
@@ -358,8 +380,8 @@ async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, c
modal.addLabelComponents(userLabel);
modal.addComponents(
- new ActionRowBuilder().addComponents(amountInput),
- new ActionRowBuilder().addComponents(typeInput),
+ new ActionRowBuilder().addComponents(amountInput),
+ new ActionRowBuilder().addComponents(typeInput),
);
await selectInteraction.showModal(modal);
@@ -373,28 +395,28 @@ async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, c
if (!submitted) return;
- const userId = submitted.fields.getField('target_user').values[0];
+ const userId = (submitted.fields.getField('target_user') as SelectMenuModalData).values[0];
const amount = parseInt(submitted.fields.getTextInputValue('amount').trim(), 10);
const type = submitted.fields.getTextInputValue('type').trim().toLowerCase();
if (isNaN(amount) || amount <= 0) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Amount must be a positive number.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Der Betrag muss eine positive Zahl sein.' });
return;
}
if (type !== 'wallet' && type !== 'bank') {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Type must be either "wallet" or "bank".' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Der Typ muss entweder „wallet“ oder „bank“ sein.' });
return;
}
const member = await guild.members.fetch(userId).catch(() => null);
if (!member) {
- await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'The specified user is not in this server.' });
+ await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'Der angegebene Nutzer ist nicht auf diesem Server.' });
return;
}
if (member.user.bot) {
- await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Bots do not have economy accounts.' });
+ await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Bots haben kein Wirtschaftskonto.' });
return;
}
@@ -403,7 +425,7 @@ async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, c
const currencySymbol = BotConfig.economy.currency.symbol;
await submitted.reply({
- embeds: [successEmbed('Currency Removed', `Successfully removed ${currencySymbol}${amount.toLocaleString()} from ${member.user.tag}'s ${type}.\n**New Balance:** ${currencySymbol}${newBalance.toLocaleString()}`)],
+ embeds: [successEmbed('Währung entfernt', `${currencySymbol}${amount.toLocaleString()} wurden aus ${type === 'wallet' ? 'der Geldbörse' : 'der Bank'} von ${member.user.tag} entfernt.\n**Neuer Kontostand:** ${currencySymbol}${newBalance.toLocaleString()}`)],
flags: MessageFlags.Ephemeral,
});
@@ -418,14 +440,20 @@ async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, c
await refreshDashboard(rootInteraction, guild, client);
}
-async function handleChangeCurrency(selectInteraction, rootInteraction, guild) {
+async function handleChangeCurrency(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guild: Guild) {
+ // The currency lives in the global bot config file, so only bot owners may change it (not every guild's admins).
+ if (!isBotOwner(selectInteraction.user.id)) {
+ await replyUserError(selectInteraction, { type: ErrorTypes.PERMISSION, message: 'Die Währung gilt für alle Server. Nur Bot-Besitzer können sie ändern.' });
+ return;
+ }
+
const modal = new ModalBuilder()
.setCustomId(`economy_change_currency_${guild.id}`)
- .setTitle('Change Currency Symbol');
+ .setTitle('Währungssymbol ändern');
const symbolInput = new TextInputBuilder()
.setCustomId('currency_symbol')
- .setLabel('New Currency Symbol')
+ .setLabel('Neues Währungssymbol')
.setStyle(TextInputStyle.Short)
.setValue(BotConfig.economy.currency.symbol)
.setPlaceholder('$')
@@ -433,7 +461,7 @@ async function handleChangeCurrency(selectInteraction, rootInteraction, guild) {
.setMaxLength(3)
.setRequired(true);
- modal.addComponents(new ActionRowBuilder().addComponents(symbolInput));
+ modal.addComponents(new ActionRowBuilder().addComponents(symbolInput));
await selectInteraction.showModal(modal);
@@ -448,20 +476,20 @@ async function handleChangeCurrency(selectInteraction, rootInteraction, guild) {
const newSymbol = submitted.fields.getTextInputValue('currency_symbol').trim();
- if (newSymbol.length === 0 || newSymbol.length > 3) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Currency symbol must be 1-3 characters long.' });
+ if (newSymbol.length === 0 || newSymbol.length > 3 || !toConfigLiteral(newSymbol)) {
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Das Währungssymbol muss 1–3 Zeichen lang sein und darf keine Anführungszeichen oder Backslashes enthalten.' });
return;
}
const success = await updateConfigFile(newSymbol, BotConfig.economy.currency.name);
if (!success) {
- await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Could not update the config file. Please check the logs.' });
+ await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Die Konfigurationsdatei konnte nicht aktualisiert werden. Bitte prüf die Logs.' });
return;
}
await submitted.reply({
- embeds: [successEmbed('Currency Symbol Updated', `Currency symbol changed to **${newSymbol}**.\n\n**Note:** The bot needs to be restarted for changes to take effect.`)],
+ embeds: [successEmbed('Währungssymbol aktualisiert', `Das Währungssymbol ist jetzt **${newSymbol}**.\n\n**Hinweis:** Der Bot muss neu gestartet werden, damit die Änderung wirksam wird.`)],
flags: MessageFlags.Ephemeral,
});
@@ -472,22 +500,28 @@ async function handleChangeCurrency(selectInteraction, rootInteraction, guild) {
});
}
-async function handleChangeName(selectInteraction, rootInteraction, guild) {
+async function handleChangeName(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guild: Guild) {
+ // The currency lives in the global bot config file, so only bot owners may change it (not every guild's admins).
+ if (!isBotOwner(selectInteraction.user.id)) {
+ await replyUserError(selectInteraction, { type: ErrorTypes.PERMISSION, message: 'Die Währung gilt für alle Server. Nur Bot-Besitzer können sie ändern.' });
+ return;
+ }
+
const modal = new ModalBuilder()
.setCustomId(`economy_change_name_${guild.id}`)
- .setTitle('Change Currency Name');
+ .setTitle('Währungsname ändern');
const nameInput = new TextInputBuilder()
.setCustomId('currency_name')
- .setLabel('New Currency Name')
+ .setLabel('Neuer Währungsname')
.setStyle(TextInputStyle.Short)
.setValue(BotConfig.economy.currency.name)
- .setPlaceholder('coins')
+ .setPlaceholder('Münzen')
.setMinLength(1)
.setMaxLength(20)
.setRequired(true);
- modal.addComponents(new ActionRowBuilder().addComponents(nameInput));
+ modal.addComponents(new ActionRowBuilder().addComponents(nameInput));
await selectInteraction.showModal(modal);
@@ -502,20 +536,20 @@ async function handleChangeName(selectInteraction, rootInteraction, guild) {
const newName = submitted.fields.getTextInputValue('currency_name').trim();
- if (newName.length === 0 || newName.length > 20) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Currency name must be 1-20 characters long.' });
+ if (newName.length === 0 || newName.length > 20 || !toConfigLiteral(newName)) {
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Der Währungsname muss 1–20 Zeichen lang sein und darf keine Anführungszeichen oder Backslashes enthalten.' });
return;
}
const success = await updateConfigFile(BotConfig.economy.currency.symbol, newName);
if (!success) {
- await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Could not update the config file. Please check the logs.' });
+ await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Die Konfigurationsdatei konnte nicht aktualisiert werden. Bitte prüf die Logs.' });
return;
}
await submitted.reply({
- embeds: [successEmbed('Currency Name Updated', `Currency name changed to **${newName}**.\n\n**Note:** The bot needs to be restarted for changes to take effect.`)],
+ embeds: [successEmbed('Währungsname aktualisiert', `Der Währungsname ist jetzt **${newName}**.\n\n**Hinweis:** Der Bot muss neu gestartet werden, damit die Änderung wirksam wird.`)],
flags: MessageFlags.Ephemeral,
});
diff --git a/src/commands/Economy/eleaderboard.js b/src/commands/Economy/modules/eleaderboard.ts
similarity index 69%
rename from src/commands/Economy/eleaderboard.js
rename to src/commands/Economy/modules/eleaderboard.ts
index e4603797a7..5ea31dbb65 100644
--- a/src/commands/Economy/eleaderboard.js
+++ b/src/commands/Economy/modules/eleaderboard.ts
@@ -1,21 +1,23 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { getEconomyPrefix } from '../../utils/database.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { getEconomyPrefix } from '../../../utils/database.js';
+import type { EconomyData } from '../../../utils/economy.js';
export default {
data: new SlashCommandBuilder()
.setName("eleaderboard")
- .setDescription("View the server's top 10 richest users.")
+ .setDescription("Zeige die 10 reichsten Nutzer des Servers.")
.setDMPermission(false),
execute: withErrorHandling(async (interaction, config, client) => {
const deferred = await InteractionHelper.safeDefer(interaction);
if (!deferred) return;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
logger.debug(`[ECONOMY] Leaderboard requested`, { guildId });
@@ -31,15 +33,15 @@ export default {
throw createError(
"No economy data found",
ErrorTypes.VALIDATION,
- "No economy data found for this server."
+ "Für diesen Server wurden keine Wirtschaftsdaten gefunden."
);
}
- let allUserData = [];
+ let allUserData: { userId: string; net_worth: number }[] = [];
for (const key of allKeys) {
const userId = key.replace(prefix, "");
- const userData = await client.db.get(key);
+ const userData = await client.db.get(key);
if (userData) {
allUserData.push({
@@ -56,7 +58,7 @@ export default {
allUserData.findIndex((u) => u.userId === interaction.user.id) +
1;
const rankEmoji = ["🥇", "🥈", "🥉"];
- const leaderboardEntries = [];
+ const leaderboardEntries: string[] = [];
for (let i = 0; i < topUsers.length; i++) {
const user = topUsers[i];
@@ -76,14 +78,14 @@ export default {
const description = leaderboardEntries.length > 0
? leaderboardEntries.join("\n")
- : "No economy data is available for this server yet.";
+ : "Für diesen Server gibt es noch keine Wirtschaftsdaten.";
const embed = createEmbed({
- title: `Economy Leaderboard`,
+ title: `Wirtschafts-Bestenliste`,
description,
- footer: `Your Rank: ${userRank > 0 ?`#${userRank}`: "No ranking data available"}`,
+ footer: `Dein Rang: ${userRank > 0 ?`#${userRank}`: "Keine Rangdaten verfügbar"}`,
});
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'eleaderboard' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/modules/fish.ts
similarity index 59%
rename from src/commands/Economy/fish.js
rename to src/commands/Economy/modules/fish.ts
index 5288f048bb..02056a2af3 100644
--- a/src/commands/Economy/fish.js
+++ b/src/commands/Economy/modules/fish.ts
@@ -1,8 +1,10 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import type { EconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
const FISH_COOLDOWN = 45 * 60 * 1000;
const BASE_MIN_REWARD = 300;
@@ -10,39 +12,47 @@ const BASE_MAX_REWARD = 900;
const FISHING_ROD_MULTIPLIER = 1.5;
const FISH_TYPES = [
- { name: 'Bass', emoji: '🐟', rarity: 'common' },
- { name: 'Salmon', emoji: '🐟', rarity: 'common' },
- { name: 'Trout', emoji: '🐟', rarity: 'common' },
- { name: 'Tuna', emoji: '🐠', rarity: 'uncommon' },
- { name: 'Swordfish', emoji: '🐠', rarity: 'uncommon' },
- { name: 'Octopus', emoji: '🐙', rarity: 'rare' },
- { name: 'Lobster', emoji: '🦞', rarity: 'rare' },
- { name: 'Shark', emoji: '🦈', rarity: 'epic' },
- { name: 'Whale', emoji: '🐋', rarity: 'legendary' },
+ { name: 'Barsch', emoji: '🐟', rarity: 'common' },
+ { name: 'Lachs', emoji: '🐟', rarity: 'common' },
+ { name: 'Forelle', emoji: '🐟', rarity: 'common' },
+ { name: 'Thunfisch', emoji: '🐠', rarity: 'uncommon' },
+ { name: 'Schwertfisch', emoji: '🐠', rarity: 'uncommon' },
+ { name: 'Oktopus', emoji: '🐙', rarity: 'rare' },
+ { name: 'Hummer', emoji: '🦞', rarity: 'rare' },
+ { name: 'Hai', emoji: '🦈', rarity: 'epic' },
+ { name: 'Wal', emoji: '🐋', rarity: 'legendary' },
];
+const RARITY_LABELS: Record = {
+ common: 'Gewöhnlich',
+ uncommon: 'Ungewöhnlich',
+ rare: 'Selten',
+ epic: 'Episch',
+ legendary: 'Legendär',
+};
+
const CATCH_MESSAGES = [
- "You cast your line into the crystal clear waters...",
- "You wait patiently as your bobber floats...",
- "After a few minutes of waiting, you feel a tug...",
- "The water ripples as something takes your bait...",
- "You reel in your catch with expert precision...",
+ "Du wirfst deine Angel ins kristallklare Wasser...",
+ "Du wartest geduldig, während dein Schwimmer treibt...",
+ "Nach ein paar Minuten spürst du einen Ruck...",
+ "Das Wasser kräuselt sich, als etwas anbeißt...",
+ "Du holst deinen Fang mit Profi-Präzision ein...",
];
export default {
data: new SlashCommandBuilder()
.setName('fish')
- .setDescription('Go fishing to catch fish and earn money'),
+ .setDescription('Geh angeln, fang Fische und verdiene Geld'),
execute: withErrorHandling(async (interaction, config, client) => {
const deferred = await InteractionHelper.safeDefer(interaction);
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const now = Date.now();
- const userData = await getEconomyData(client, guildId, userId);
+ const userData: EconomyData & { lastFish?: number } = await getEconomyData(client, guildId, userId);
const lastFish = userData.lastFish || 0;
const hasFishingRod = userData.inventory["fishing_rod"] || 0;
@@ -56,7 +66,7 @@ export default {
throw createError(
"Fishing cooldown active",
ErrorTypes.RATE_LIMIT,
- `You're too tired to fish right now. Rest for **${hours}h ${minutes}m** before fishing again.`,
+ `Du bist gerade zu müde zum Angeln. Ruh dich **${hours}h ${minutes}m** aus, bevor du wieder angelst.`,
{ remaining, cooldownType: 'fish' }
);
}
@@ -75,10 +85,10 @@ export default {
fishCaught = FISH_TYPES.filter(f => f.rarity === 'rare')[Math.floor(Math.random() * 2)];
} else if (rand < 0.98) {
- fishCaught = FISH_TYPES.find(f => f.rarity === 'epic');
+ fishCaught = FISH_TYPES.find(f => f.rarity === 'epic')!;
} else {
- fishCaught = FISH_TYPES.find(f => f.rarity === 'legendary');
+ fishCaught = FISH_TYPES.find(f => f.rarity === 'legendary')!;
}
const baseEarned = Math.floor(
@@ -90,7 +100,7 @@ export default {
if (hasFishingRod > 0) {
finalEarned = Math.floor(baseEarned * FISHING_ROD_MULTIPLIER);
- multiplierMessage = `\n🎣 **Fishing Rod Bonus: +50%**`;
+ multiplierMessage = `\n🎣 **Angel-Bonus: +50 %**`;
}
const catchMessage = CATCH_MESSAGES[Math.floor(Math.random() * CATCH_MESSAGES.length)];
@@ -100,7 +110,7 @@ export default {
await setEconomyData(client, guildId, userId, userData);
- const rarityColors = {
+ const rarityColors: Record = {
common: '#95A5A6',
uncommon: '#2ECC71',
rare: '#3498DB',
@@ -109,24 +119,24 @@ export default {
};
const embed = createEmbed({
- title: 'Fishing Success!',
- description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}`,
+ title: 'Erfolgreich geangelt!',
+ description: `${catchMessage}\n\nDein Fang: **${fishCaught.emoji} ${fishCaught.name}**! Du hast ihn für **$${finalEarned.toLocaleString()}** verkauft!${multiplierMessage}`,
color: rarityColors[fishCaught.rarity]
})
.addFields(
{
- name: "New Cash Balance",
+ name: "Geldbörse (neu)",
value: `$${userData.wallet.toLocaleString()}`,
inline: true,
},
{
- name: "Rarity",
- value: fishCaught.rarity.charAt(0).toUpperCase() + fishCaught.rarity.slice(1),
+ name: "Seltenheit",
+ value: RARITY_LABELS[fishCaught.rarity] ?? fishCaught.rarity,
inline: true,
}
)
- .setFooter({ text: `Next fishing trip available in 45 minutes.` });
+ .setFooter({ text: `Nächster Angelausflug in 45 Minuten möglich.` });
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'fish' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/gamble.js b/src/commands/Economy/modules/gamble.ts
similarity index 65%
rename from src/commands/Economy/gamble.js
rename to src/commands/Economy/modules/gamble.ts
index b4acf1745d..22c89c9b4c 100644
--- a/src/commands/Economy/gamble.js
+++ b/src/commands/Economy/modules/gamble.ts
@@ -1,8 +1,10 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import type { EconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
const BASE_WIN_CHANCE = 0.4;
const CLOVER_WIN_BONUS = 0.1;
@@ -13,11 +15,11 @@ const GAMBLE_COOLDOWN = 5 * 60 * 1000;
export default {
data: new SlashCommandBuilder()
.setName('gamble')
- .setDescription('Gamble your money for a chance to win more')
+ .setDescription('Setze dein Geld ein und gewinne mit etwas Glück mehr')
.addIntegerOption(option =>
option
.setName('amount')
- .setDescription('Amount of cash to gamble')
+ .setDescription('Betrag, den du setzen möchtest')
.setRequired(true)
.setMinValue(1)
),
@@ -27,11 +29,11 @@ export default {
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
- const betAmount = interaction.options.getInteger("amount");
+ const guildId = interaction.guildId!;
+ const betAmount = interaction.options.getInteger("amount")!;
const now = Date.now();
- const userData = await getEconomyData(client, guildId, userId);
+ const userData: EconomyData & { lastGamble?: number } = await getEconomyData(client, guildId, userId);
const lastGamble = userData.lastGamble || 0;
let cloverCount = userData.inventory["lucky_clover"] || 0;
let charmCount = userData.inventory["lucky_charm"] || 0;
@@ -44,7 +46,7 @@ export default {
throw createError(
"Gamble cooldown active",
ErrorTypes.RATE_LIMIT,
- `You need to cool down before gambling again. Wait **${minutes}m ${seconds}s**.`,
+ `Mach mal Pause, bevor du wieder spielst. Warte **${minutes}m ${seconds}s**.`,
{ remaining, cooldownType: 'gamble' }
);
}
@@ -53,7 +55,7 @@ export default {
throw createError(
"Insufficient cash for gamble",
ErrorTypes.VALIDATION,
- `You only have $${userData.wallet.toLocaleString()} cash, but you are trying to bet $${betAmount.toLocaleString()}.`,
+ `Du hast nur $${userData.wallet.toLocaleString()} in deiner Geldbörse, willst aber $${betAmount.toLocaleString()} setzen.`,
{ required: betAmount, current: userData.wallet }
);
}
@@ -66,14 +68,14 @@ export default {
if (cloverCount > 0) {
winChance += CLOVER_WIN_BONUS;
userData.inventory["lucky_clover"] -= 1;
- cloverMessage = `\n🍀 **Lucky Clover Consumed:** Your win chance was boosted!`;
+ cloverMessage = `\n🍀 **Glücksklee verbraucht:** Deine Gewinnchance wurde erhöht!`;
usedClover = true;
}
else if (charmCount > 0) {
winChance += CHARM_WIN_BONUS;
userData.inventory["lucky_charm"] -= 1;
- cloverMessage = `\n🍀 **Lucky Charm Used (${charmCount - 1} uses remaining):** Your win chance was boosted!`;
+ cloverMessage = `\n🍀 **Glücksbringer benutzt (noch ${charmCount - 1} Nutzungen):** Deine Gewinnchance wurde erhöht!`;
usedCharm = true;
}
@@ -87,15 +89,15 @@ export default {
cashChange = amountWon - betAmount;
resultEmbed = successEmbed(
- "🎉 You Won!",
- `You successfully gambled and turned your **$${betAmount.toLocaleString()}** bet into **$${amountWon.toLocaleString()}**!${cloverMessage}`,
+ "🎉 Gewonnen!",
+ `Du hast aus deinem Einsatz von **$${betAmount.toLocaleString()}** ganze **$${amountWon.toLocaleString()}** gemacht!${cloverMessage}`,
);
} else {
cashChange = -betAmount;
resultEmbed = warningEmbed(
- "💔 You Lost...",
- `The dice rolled against you. You lost your **$${betAmount.toLocaleString()}** bet.`,
+ "💔 Verloren...",
+ `Das Glück war nicht auf deiner Seite. Dein Einsatz von **$${betAmount.toLocaleString()}** ist weg.`,
);
}
@@ -107,25 +109,25 @@ userData.lastGamble = now;
const newCash = userData.wallet;
resultEmbed.addFields({
- name: "New Cash Balance",
+ name: "Geldbörse (neu)",
value: `$${newCash.toLocaleString()}`,
inline: true,
});
if (usedClover) {
resultEmbed.setFooter({
- text: `You have ${userData.inventory["lucky_clover"]} Lucky Clovers left. Win chance was ${Math.round(winChance * 100)}%.`,
+ text: `Du hast noch ${userData.inventory["lucky_clover"]} Glücksklee. Gewinnchance war ${Math.round(winChance * 100)} %.`,
});
} else if (usedCharm) {
resultEmbed.setFooter({
- text: `You have ${userData.inventory["lucky_charm"]} Lucky Charm uses left. Win chance was ${Math.round(winChance * 100)}%.`,
+ text: `Du hast noch ${userData.inventory["lucky_charm"]} Glücksbringer-Nutzungen. Gewinnchance war ${Math.round(winChance * 100)} %.`,
});
} else {
resultEmbed.setFooter({
- text: `Next gamble available in 5 minutes. Base win chance: ${Math.round(BASE_WIN_CHANCE * 100)}%.`,
+ text: `Nächstes Spiel in 5 Minuten möglich. Basis-Gewinnchance: ${Math.round(BASE_WIN_CHANCE * 100)} %.`,
});
}
await InteractionHelper.safeEditReply(interaction, { embeds: [resultEmbed] });
}, { command: 'gamble' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/inventory.js b/src/commands/Economy/modules/inventory.ts
similarity index 72%
rename from src/commands/Economy/inventory.js
rename to src/commands/Economy/modules/inventory.ts
index b7efcfec80..fe2b0ab169 100644
--- a/src/commands/Economy/inventory.js
+++ b/src/commands/Economy/modules/inventory.ts
@@ -1,24 +1,25 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { shopItems } from '../../config/shop/items.js';
-import { getEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { shopItems } from '../../../config/shop/items.js';
+import { getEconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
const SHOP_ITEMS = shopItems;
export default {
data: new SlashCommandBuilder()
.setName('inventory')
- .setDescription('View your economy inventory'),
+ .setDescription('Zeige dein Inventar an'),
execute: withErrorHandling(async (interaction, config, client) => {
const deferred = await InteractionHelper.safeDefer(interaction);
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
logger.debug(`[ECONOMY] Inventory requested for ${userId}`, { userId, guildId });
@@ -28,14 +29,14 @@ export default {
throw createError(
"Failed to load economy data for inventory",
ErrorTypes.DATABASE,
- "Failed to load your economy data. Please try again later.",
+ "Deine Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId, guildId }
);
}
const inventory = userData.inventory || {};
- let inventoryDescription = "Your inventory is currently empty.";
+ let inventoryDescription = "Dein Inventar ist gerade leer.";
if (Object.keys(inventory).length > 0) {
inventoryDescription = Object.entries(inventory)
@@ -48,7 +49,7 @@ export default {
.map(
([itemId, quantity]) => {
const item = SHOP_ITEMS.find(i => i.id === itemId);
- return `**${item.name}:** ${quantity}x`;
+ return `**${item!.name}:** ${quantity}x`;
}
)
.join("\n");
@@ -61,10 +62,10 @@ export default {
});
const embed = createEmbed({
- title: `🎒 ${interaction.user.username}'s Inventory`,
+ title: `🎒 Inventar von ${interaction.user.username}`,
description: inventoryDescription,
}).setThumbnail(interaction.user.displayAvatarURL());
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'inventory' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/mine.js b/src/commands/Economy/modules/mine.ts
similarity index 65%
rename from src/commands/Economy/mine.js
rename to src/commands/Economy/modules/mine.ts
index fda8ba1dda..6d3635feef 100644
--- a/src/commands/Economy/mine.js
+++ b/src/commands/Economy/modules/mine.ts
@@ -1,8 +1,10 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import type { EconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
const MINE_COOLDOWN = 60 * 60 * 1000;
const BASE_MIN_REWARD = 400;
@@ -11,27 +13,27 @@ const PICKAXE_MULTIPLIER = 1.2;
const DIAMOND_PICKAXE_MULTIPLIER = 2.0;
const MINE_LOCATIONS = [
- "abandoned gold mine",
- "dark, damp cave",
- "backyard rock quarry",
- "volcanic obsidian vent",
- "deep-sea mineral trench",
+ "eine verlassene Goldmine",
+ "eine dunkle, feuchte Höhle",
+ "einen Steinbruch im Hinterhof",
+ "einen vulkanischen Obsidianschlot",
+ "einen Tiefsee-Mineraliengraben",
];
export default {
data: new SlashCommandBuilder()
.setName('mine')
- .setDescription('Go mining to earn money'),
+ .setDescription('Geh in die Mine und verdiene Geld'),
execute: withErrorHandling(async (interaction, config, client) => {
const deferred = await InteractionHelper.safeDefer(interaction);
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const now = Date.now();
- const userData = await getEconomyData(client, guildId, userId);
+ const userData: EconomyData & { lastMine?: number } = await getEconomyData(client, guildId, userId);
const lastMine = userData.lastMine || 0;
const hasDiamondPickaxe = userData.inventory["diamond_pickaxe"] || 0;
const hasPickaxe = userData.inventory["pickaxe"] || 0;
@@ -46,7 +48,7 @@ export default {
throw createError(
"Mining cooldown active",
ErrorTypes.RATE_LIMIT,
- `Your pickaxe is cooling down. Wait for **${hours}h ${minutes}m** before mining again.`,
+ `Deine Spitzhacke muss sich erholen. Warte **${hours}h ${minutes}m**, bevor du wieder schürfst.`,
{ remaining, cooldownType: 'mine' }
);
}
@@ -61,10 +63,10 @@ export default {
if (hasDiamondPickaxe > 0) {
finalEarned = Math.floor(baseEarned * DIAMOND_PICKAXE_MULTIPLIER);
- multiplierMessage = `\n💎 **Diamond Pickaxe Bonus: +100%**`;
+ multiplierMessage = `\n💎 **Diamant-Spitzhacken-Bonus: +100 %**`;
} else if (hasPickaxe > 0) {
finalEarned = Math.floor(baseEarned * PICKAXE_MULTIPLIER);
- multiplierMessage = `\n⛏️ **Pickaxe Bonus: +20%**`;
+ multiplierMessage = `\n⛏️ **Spitzhacken-Bonus: +20 %**`;
}
const location =
@@ -78,16 +80,16 @@ userData.lastMine = now;
await setEconomyData(client, guildId, userId, userData);
const embed = successEmbed(
- "💰 Mining Expedition Successful!",
- `You explored a **${location}** and managed to find minerals worth **$${finalEarned.toLocaleString()}**!${multiplierMessage}`,
+ "💰 Bergbau-Expedition erfolgreich!",
+ `Du hast **${location}** erkundet und Mineralien im Wert von **$${finalEarned.toLocaleString()}** gefunden!${multiplierMessage}`,
)
.addFields({
- name: "New Cash Balance",
+ name: "Geldbörse (neu)",
value: `$${userData.wallet.toLocaleString()}`,
inline: true,
})
- .setFooter({ text: `Next mine available in 1 hour.` });
+ .setFooter({ text: `Nächstes Schürfen in 1 Stunde möglich.` });
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'mine' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/pay.js b/src/commands/Economy/modules/pay.ts
similarity index 73%
rename from src/commands/Economy/pay.js
rename to src/commands/Economy/modules/pay.ts
index 5e8e904862..8e83d84271 100644
--- a/src/commands/Economy/pay.js
+++ b/src/commands/Economy/modules/pay.ts
@@ -1,25 +1,26 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, addMoney, removeMoney, setEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import EconomyService from '../../services/economyService.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, addMoney, removeMoney, setEconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import EconomyService from '../../../services/economyService.js';
export default {
data: new SlashCommandBuilder()
.setName('pay')
- .setDescription('Pay another user some of your cash')
+ .setDescription('Gib einem anderen Nutzer etwas von deinem Geld')
.addUserOption(option =>
option
.setName('user')
- .setDescription('User to pay')
+ .setDescription('Nutzer, den du bezahlen möchtest')
.setRequired(true)
)
.addIntegerOption(option =>
option
.setName('amount')
- .setDescription('Amount to pay')
+ .setDescription('Betrag, den du zahlen möchtest')
.setRequired(true)
.setMinValue(1)
),
@@ -29,9 +30,9 @@ export default {
if (!deferred) return;
const senderId = interaction.user.id;
- const receiver = interaction.options.getUser("user");
- const amount = interaction.options.getInteger("amount");
- const guildId = interaction.guildId;
+ const receiver = interaction.options.getUser("user")!;
+ const amount = interaction.options.getInteger("amount")!;
+ const guildId = interaction.guildId!;
logger.debug(`[ECONOMY] Pay command initiated`, {
senderId,
@@ -44,7 +45,7 @@ export default {
throw createError(
"Cannot pay bot",
ErrorTypes.VALIDATION,
- "You cannot pay a bot.",
+ "Du kannst keinen Bot bezahlen.",
{ receiverId: receiver.id, isBot: true }
);
}
@@ -53,7 +54,7 @@ export default {
throw createError(
"Cannot pay self",
ErrorTypes.VALIDATION,
- "You cannot pay yourself.",
+ "Du kannst dich nicht selbst bezahlen.",
{ senderId, receiverId: receiver.id }
);
}
@@ -62,7 +63,7 @@ export default {
throw createError(
"Invalid payment amount",
ErrorTypes.VALIDATION,
- "Amount must be greater than zero.",
+ "Der Betrag muss größer als null sein.",
{ amount, senderId }
);
}
@@ -76,7 +77,7 @@ export default {
throw createError(
"Failed to load sender economy data",
ErrorTypes.DATABASE,
- "Failed to load your economy data. Please try again later.",
+ "Deine Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId: senderId, guildId }
);
}
@@ -85,7 +86,7 @@ export default {
throw createError(
"Failed to load receiver economy data",
ErrorTypes.DATABASE,
- "Failed to load the receiver's economy data. Please try again later.",
+ "Die Wirtschaftsdaten des Empfängers konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId: receiver.id, guildId }
);
}
@@ -102,23 +103,23 @@ export default {
const updatedReceiverData = await getEconomyData(client, guildId, receiver.id);
const embed = successEmbed(
- 'Payment Successful',
- `You successfully paid **${receiver.username}** the amount of **$${amount.toLocaleString()}**!`
+ 'Zahlung erfolgreich',
+ `Du hast **${receiver.username}** erfolgreich **$${amount.toLocaleString()}** gezahlt!`
)
.addFields(
{
- name: "Payment Amount",
+ name: "Betrag",
value: `$${amount.toLocaleString()}`,
inline: true,
},
{
- name: "Your New Balance",
+ name: "Dein neuer Kontostand",
value: `$${updatedSenderData.wallet.toLocaleString()}`,
inline: true,
},
)
.setFooter({
- text: `Paid to ${receiver.tag}`,
+ text: `Gezahlt an ${receiver.tag}`,
iconURL: receiver.displayAvatarURL(),
});
@@ -134,16 +135,16 @@ export default {
try {
const receiverEmbed = createEmbed({
- title: "Incoming Payment!",
- description: `${interaction.user.username} paid you **$${amount.toLocaleString()}**.`
+ title: "Zahlung erhalten!",
+ description: `${interaction.user.username} hat dir **$${amount.toLocaleString()}** gezahlt.`
}).addFields({
- name: "Your New Cash",
+ name: "Deine Geldbörse (neu)",
value: `$${updatedReceiverData.wallet.toLocaleString()}`,
inline: true,
});
await receiver.send({ embeds: [receiverEmbed] });
} catch (e) {
- logger.warn(`Could not DM user ${receiver.id}: ${e.message}`);
+ logger.warn(`Could not DM user ${receiver.id}: ${e instanceof Error ? e.message : String(e)}`);
}
}, { command: 'pay' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/rob.js b/src/commands/Economy/modules/rob.ts
similarity index 71%
rename from src/commands/Economy/rob.js
rename to src/commands/Economy/modules/rob.ts
index 68f74fbfee..a8d8b07aa9 100644
--- a/src/commands/Economy/rob.js
+++ b/src/commands/Economy/modules/rob.ts
@@ -1,9 +1,10 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { successEmbed, warningEmbed, buildUserErrorEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { BotConfig } from '../../config/bot.js';
+import { successEmbed, warningEmbed, buildUserErrorEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { BotConfig } from '../../../config/bot.js';
const ROB_COOLDOWN = BotConfig.economy?.cooldowns?.rob ?? 4 * 60 * 60 * 1000;
const BASE_ROB_SUCCESS_CHANCE = BotConfig.economy?.robSuccessRate ?? 0.4;
@@ -13,11 +14,11 @@ const FINE_PERCENTAGE = 0.1;
export default {
data: new SlashCommandBuilder()
.setName('rob')
- .setDescription('Attempt to rob another user (very risky)')
+ .setDescription('Versuche, einen anderen Nutzer auszurauben (sehr riskant)')
.addUserOption(option =>
option
.setName('user')
- .setDescription('User to rob')
+ .setDescription('Nutzer, den du ausrauben willst')
.setRequired(true)
),
@@ -26,15 +27,15 @@ export default {
if (!deferred) return;
const robberId = interaction.user.id;
- const victimUser = interaction.options.getUser("user");
- const guildId = interaction.guildId;
+ const victimUser = interaction.options.getUser("user")!;
+ const guildId = interaction.guildId!;
const now = Date.now();
if (robberId === victimUser.id) {
throw createError(
"Cannot rob self",
ErrorTypes.VALIDATION,
- "You cannot rob yourself.",
+ "Du kannst dich nicht selbst ausrauben.",
{ robberId, victimId: victimUser.id }
);
}
@@ -43,7 +44,7 @@ export default {
throw createError(
"Cannot rob bot",
ErrorTypes.VALIDATION,
- "You cannot rob a bot.",
+ "Du kannst keinen Bot ausrauben.",
{ victimId: victimUser.id, isBot: true }
);
}
@@ -55,7 +56,7 @@ export default {
throw createError(
"Failed to load economy data",
ErrorTypes.DATABASE,
- "Failed to load economy data. Please try again later.",
+ "Die Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ robberId: !!robberData, victimId: !!victimData, guildId }
);
}
@@ -70,7 +71,7 @@ export default {
throw createError(
"Robbery cooldown active",
ErrorTypes.RATE_LIMIT,
- `You need to lay low. Wait **${hours}h ${minutes}m** before attempting another robbery.`,
+ `Du musst untertauchen. Warte **${hours}h ${minutes}m**, bevor du den nächsten Raub versuchst.`,
{ remaining, hours, minutes, cooldownType: 'rob' }
);
}
@@ -79,7 +80,7 @@ export default {
throw createError(
"Victim too poor",
ErrorTypes.VALIDATION,
- `${victimUser.username} is too poor. They need at least $500 cash to be worth robbing.`,
+ `${victimUser.username} ist zu arm. Es lohnt sich erst ab mindestens $500 in der Geldbörse.`,
{ victimWallet: victimData.wallet, required: 500 }
);
}
@@ -93,8 +94,8 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
warningEmbed(
- 'Robbery Blocked',
- `${victimUser.username} was prepared! Your attempt failed because they own a **Personal Safe**. You got away clean but didn't gain anything.`
+ 'Raub verhindert',
+ `${victimUser.username} war vorbereitet! Dein Versuch ist an einem **persönlichen Safe** gescheitert. Du bist unerkannt entkommen, hast aber nichts erbeutet.`
)
],
});
@@ -110,8 +111,8 @@ export default {
victimData.wallet = (victimData.wallet || 0) - amountStolen;
resultEmbed = successEmbed(
- 'Robbery Successful',
- `You successfully stole **$${amountStolen.toLocaleString()}** from ${victimUser.username}!`
+ 'Raub erfolgreich',
+ `Du hast ${victimUser.username} erfolgreich **$${amountStolen.toLocaleString()}** abgenommen!`
);
} else {
const fineAmount = Math.floor((robberData.wallet || 0) * FINE_PERCENTAGE);
@@ -124,8 +125,8 @@ export default {
resultEmbed = buildUserErrorEmbed(
'unknown',
- `You failed the robbery and were caught! You were fined **$${fineAmount.toLocaleString()}** of your own cash.`,
- { titleOverride: 'Robbery Failed' }
+ `Der Raub ist gescheitert und du wurdest erwischt! Du musst **$${fineAmount.toLocaleString()}** aus deiner eigenen Geldbörse Strafe zahlen.`,
+ { titleOverride: 'Raub gescheitert' }
);
}
@@ -137,18 +138,18 @@ export default {
resultEmbed
.addFields(
{
- name: `Your New Cash (${interaction.user.username})`,
+ name: `Deine Geldbörse (${interaction.user.username})`,
value: `$${robberData.wallet.toLocaleString()}`,
inline: true,
},
{
- name: `Victim's New Cash (${victimUser.username})`,
+ name: `Geldbörse des Opfers (${victimUser.username})`,
value: `$${victimData.wallet.toLocaleString()}`,
inline: true,
},
)
- .setFooter({ text: `Next robbery available in ${Math.ceil(ROB_COOLDOWN / (60 * 60 * 1000))} hours.` });
+ .setFooter({ text: `Nächster Raub in ${Math.ceil(ROB_COOLDOWN / (60 * 60 * 1000))} Stunden möglich.` });
await InteractionHelper.safeEditReply(interaction, { embeds: [resultEmbed] });
}, { command: 'rob' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/shop-config.js b/src/commands/Economy/modules/shop-config.ts
similarity index 60%
rename from src/commands/Economy/shop-config.js
rename to src/commands/Economy/modules/shop-config.ts
index fb3be41dc3..00598fb770 100644
--- a/src/commands/Economy/shop-config.js
+++ b/src/commands/Economy/modules/shop-config.ts
@@ -1,19 +1,20 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import shopConfigSetrole from './modules/shop_config_setrole.js';
+import shopConfigSetrole from './shop_config_setrole.js';
export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName('shop-config')
- .setDescription('Configure shop settings. (Manage Server required)')
+ .setDescription('Shop-Einstellungen konfigurieren. („Server verwalten“ erforderlich)')
.addSubcommand(subcommand =>
subcommand
.setName('setrole')
- .setDescription('Set the Discord role granted when the Premium Role shop item is purchased.')
+ .setDescription('Lege die Rolle fest, die beim Kauf des Items „Premium-Rolle“ vergeben wird.')
.addRoleOption(option =>
option
.setName('role')
- .setDescription('The role to grant for Premium Role purchases.')
+ .setDescription('Die Rolle, die beim Kauf der Premium-Rolle vergeben wird.')
.setRequired(true),
),
),
@@ -25,4 +26,4 @@ export default {
return shopConfigSetrole.execute(interaction, config, client);
}
},
-};
+} satisfies Command;
diff --git a/src/commands/Economy/shop.js b/src/commands/Economy/modules/shop.ts
similarity index 61%
rename from src/commands/Economy/shop.js
rename to src/commands/Economy/modules/shop.ts
index 16b2b62d96..6b22426581 100644
--- a/src/commands/Economy/shop.js
+++ b/src/commands/Economy/modules/shop.ts
@@ -1,13 +1,14 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import shopBrowse from './modules/shop_browse.js';
+import shopBrowse from './shop_browse.js';
export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName('shop')
- .setDescription('Browse the economy shop.'),
+ .setDescription('Durchstöbere den Shop.'),
async execute(interaction, config, client) {
return shopBrowse.execute(interaction, config, client);
},
-};
+} satisfies Command;
diff --git a/src/commands/Economy/modules/shop_browse.js b/src/commands/Economy/modules/shop_browse.ts
similarity index 76%
rename from src/commands/Economy/modules/shop_browse.js
rename to src/commands/Economy/modules/shop_browse.ts
index b7adcf40dd..cb3349af65 100644
--- a/src/commands/Economy/modules/shop_browse.js
+++ b/src/commands/Economy/modules/shop_browse.ts
@@ -1,4 +1,5 @@
-import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, EmbedBuilder, MessageFlags } from 'discord.js';
+import type { SubcommandModule } from '../../../types/index.js';
+import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, EmbedBuilder, MessageFlags, type InteractionReplyOptions } from 'discord.js';
import { shopItems } from '../../../config/shop/items.js';
import { getColor } from '../../../config/bot.js';
import { logger } from '../../../utils/logger.js';
@@ -12,36 +13,36 @@ export default {
const totalPages = Math.ceil(shopItems.length / ITEMS_PER_PAGE);
let currentPage = 1;
- const createShopEmbed = (page) => {
+ const createShopEmbed = (page: number) => {
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const pageItems = shopItems.slice(startIndex, startIndex + ITEMS_PER_PAGE);
const embed = new EmbedBuilder()
- .setTitle('Store')
+ .setTitle('Shop')
.setColor(getColor('primary'))
- .setDescription('Use `/buy item_id: quantity:` to purchase an item.');
+ .setDescription('Nutze `/eco buy item_id: quantity:`, um ein Item zu kaufen.');
pageItems.forEach(item => {
embed.addFields({
name: `${item.name} (${item.id})`,
- value: `**Type:** ${item.type}\n **Price:** $${item.price.toLocaleString()}\n${item.description}`,
+ value: `**Typ:** ${item.type}\n **Preis:** $${item.price.toLocaleString()}\n${item.description}`,
inline: false,
});
});
- embed.setFooter({ text: `Page ${page}/${totalPages}` });
+ embed.setFooter({ text: `Seite ${page}/${totalPages}` });
return embed;
};
- const createShopComponents = (page) => {
+ const createShopComponents = (page: number) => {
if (totalPages <= 1) return [];
return [
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('shop_prev')
- .setLabel('⬅️ Previous')
+ .setLabel('⬅️ Vorherige')
.setStyle(ButtonStyle.Secondary)
.setDisabled(page === 1),
new ButtonBuilder()
.setCustomId('shop_next')
- .setLabel('Next ➡️')
+ .setLabel('Weiter ➡️')
.setStyle(ButtonStyle.Secondary)
.setDisabled(page === totalPages),
),
@@ -51,7 +52,7 @@ export default {
const message = await interaction.reply({
embeds: [createShopEmbed(currentPage)],
components: createShopComponents(currentPage),
- flags: 0,
+ flags: 0 as InteractionReplyOptions['flags'],
});
const collector = message.createMessageComponentCollector({
@@ -61,7 +62,7 @@ export default {
collector.on('collect', async (buttonInteraction) => {
if (buttonInteraction.user.id !== interaction.user.id) {
- await buttonInteraction.reply({ content: '❌ You cannot use these buttons. Run `/shop` to get your own shop view.', flags: 64 });
+ await buttonInteraction.reply({ content: '❌ Du kannst diese Buttons nicht nutzen. Führe `/eco shop` aus, um deine eigene Shop-Ansicht zu öffnen.', flags: 64 });
return;
}
const { customId } = buttonInteraction;
@@ -83,7 +84,7 @@ export default {
await message.edit({ components: disabledComponents });
} catch (error) {
logger.debug('shop_browse: could not disable components on collector end', {
- error: error.message,
+ error: error instanceof Error ? error.message : String(error),
});
}
});
@@ -91,4 +92,4 @@ export default {
await handleInteractionError(interaction, error, { command: 'shop_browse' });
}
},
-};
\ No newline at end of file
+} satisfies SubcommandModule;
diff --git a/src/commands/Economy/modules/shop_config_setrole.js b/src/commands/Economy/modules/shop_config_setrole.ts
similarity index 55%
rename from src/commands/Economy/modules/shop_config_setrole.js
rename to src/commands/Economy/modules/shop_config_setrole.ts
index e917e9e69a..ac1b08edff 100644
--- a/src/commands/Economy/modules/shop_config_setrole.js
+++ b/src/commands/Economy/modules/shop_config_setrole.ts
@@ -1,4 +1,5 @@
-import { PermissionsBitField } from 'discord.js';
+import type { SubcommandModule } from '../../../types/index.js';
+import { PermissionsBitField, type GuildMember } from 'discord.js';
import { successEmbed } from '../../../utils/embeds.js';
import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
@@ -7,12 +8,12 @@ import { logger } from '../../../utils/logger.js';
import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
export default {
async execute(interaction, config, client) {
- if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Server** permissions to set the premium role.' });
+ if (!(interaction.member as GuildMember).permissions.has(PermissionsBitField.Flags.ManageGuild)) {
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um die Premium-Rolle festzulegen.' });
}
- const role = interaction.options.getRole('role');
- const guildId = interaction.guildId;
+ const role = interaction.options.getRole('role')!;
+ const guildId = interaction.guildId!;
try {
const currentConfig = await getGuildConfig(client, guildId);
@@ -20,12 +21,12 @@ export default {
await setGuildConfig(client, guildId, currentConfig);
return InteractionHelper.safeReply(interaction, {
- embeds: [successEmbed('Premium Role Set', `The **Premium Shop Role** has been set to ${role.toString()}. Members who purchase the Premium Role item will be granted this role.`)],
+ embeds: [successEmbed('Premium-Rolle festgelegt', `Die **Premium-Shop-Rolle** ist jetzt ${role.toString()}. Mitglieder, die das Item „Premium-Rolle“ kaufen, erhalten diese Rolle.`)],
ephemeral: true,
});
} catch (error) {
logger.error('shop_config_setrole error:', error);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Could not save the guild configuration.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Die Server-Konfiguration konnte nicht gespeichert werden.' });
}
},
-};
\ No newline at end of file
+} satisfies SubcommandModule;
diff --git a/src/commands/Economy/slut.js b/src/commands/Economy/modules/slut.ts
similarity index 61%
rename from src/commands/Economy/slut.js
rename to src/commands/Economy/modules/slut.ts
index 98b83e369e..17a719d5e2 100644
--- a/src/commands/Economy/slut.js
+++ b/src/commands/Economy/modules/slut.ts
@@ -1,54 +1,56 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import type { EconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
const SLUT_COOLDOWN = 45 * 60 * 1000;
const SLUT_ACTIVITIES = [
- { name: "Cam Stream", min: 120, max: 450, risk: 0.2 },
- { name: "Private Dance Session", min: 220, max: 700, risk: 0.25 },
- { name: "After-Hours Club Host", min: 320, max: 900, risk: 0.3 },
- { name: "VIP Companion Booking", min: 550, max: 1400, risk: 0.35 },
- { name: "Exclusive Livestream", min: 850, max: 2200, risk: 0.4 },
+ { name: "Cam-Stream", min: 120, max: 450, risk: 0.2 },
+ { name: "Private Tanz-Session", min: 220, max: 700, risk: 0.25 },
+ { name: "Nachtclub-Host", min: 320, max: 900, risk: 0.3 },
+ { name: "VIP-Begleitbuchung", min: 550, max: 1400, risk: 0.35 },
+ { name: "Exklusiver Livestream", min: 850, max: 2200, risk: 0.4 },
];
const POSITIVE_OUTCOMES = [
- "Your stream blew up and tips poured in.",
- "A VIP booking paid far above average.",
- "Your after-hours shift was packed and profitable.",
- "Premium requests came through and your payout jumped.",
+ "Dein Stream ging durch die Decke und das Trinkgeld floss nur so.",
+ "Eine VIP-Buchung hat weit über dem Durchschnitt gezahlt.",
+ "Deine Nachtschicht war voll und hat sich richtig gelohnt.",
+ "Premium-Anfragen kamen rein und deine Auszahlung ist gestiegen.",
];
const FINE_OUTCOMES = [
- "Venue security issued a compliance fine.",
- "A moderation strike triggered a platform fee.",
- "You were flagged and had to pay a penalty.",
+ "Die Security vor Ort hat dir ein Bußgeld verpasst.",
+ "Ein Moderations-Strike hat eine Plattformgebühr ausgelöst.",
+ "Du wurdest gemeldet und musstest eine Strafe zahlen.",
];
const ROBBED_OUTCOMES = [
- "A fake buyer chargeback wiped part of your earnings.",
- "A scam booking cleaned out a chunk of your cash.",
- "You got baited by a fraud account and lost money.",
+ "Die Rückbuchung eines Fake-Käufers hat einen Teil deiner Einnahmen gekostet.",
+ "Eine Betrugsbuchung hat dir einen Batzen Geld abgeknöpft.",
+ "Du bist auf einen Fake-Account reingefallen und hast Geld verloren.",
];
const LOSS_OUTCOMES = [
- "The set flopped and you had to cover operating costs.",
- "You burned budget on prep and made no return.",
- "The shift went sideways and left you in the red.",
+ "Der Auftritt war ein Flop und du musstest die Kosten selbst tragen.",
+ "Du hast Geld in die Vorbereitung gesteckt und nichts zurückbekommen.",
+ "Die Schicht lief schief und du bist im Minus gelandet.",
];
-function randomInt(min, max) {
+function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
-function randomChoice(items) {
+function randomChoice(items: T[]): T {
return items[Math.floor(Math.random() * items.length)];
}
-function resolveOutcome(activity, wallet) {
+function resolveOutcome(activity: typeof SLUT_ACTIVITIES[number], wallet: number) {
const successChance = Math.max(0.35, 0.55 - activity.risk * 0.2);
const fineChance = 0.22;
const robbedChance = 0.2;
@@ -60,7 +62,7 @@ function resolveOutcome(activity, wallet) {
type: 'payout',
delta: amount,
message: randomChoice(POSITIVE_OUTCOMES),
- title: `${activity.name} - Payout`
+ title: `${activity.name} – Auszahlung`
};
}
@@ -74,7 +76,7 @@ function resolveOutcome(activity, wallet) {
type: 'fine',
delta: -amount,
message: randomChoice(FINE_OUTCOMES),
- title: `${activity.name} - Fined`
+ title: `${activity.name} – Strafe`
};
}
@@ -86,7 +88,7 @@ function resolveOutcome(activity, wallet) {
type: 'robbed',
delta: -amount,
message: randomChoice(ROBBED_OUTCOMES),
- title: `${activity.name} - Robbed`
+ title: `${activity.name} – Abgezockt`
};
}
@@ -97,32 +99,32 @@ function resolveOutcome(activity, wallet) {
type: 'loss',
delta: -amount,
message: randomChoice(LOSS_OUTCOMES),
- title: `${activity.name} - Loss`
+ title: `${activity.name} – Verlust`
};
}
export default {
data: new SlashCommandBuilder()
.setName('slut')
- .setDescription('Take a risky provocative job for random payout or loss'),
+ .setDescription('Nimm einen riskanten, anrüchigen Job an – Gewinn oder Verlust per Zufall'),
execute: withErrorHandling(async (interaction, config, client) => {
const deferred = await InteractionHelper.safeDefer(interaction);
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const now = Date.now();
logger.debug(`[ECONOMY] Slut command started for ${userId}`, { userId, guildId });
- const userData = await getEconomyData(client, guildId, userId);
+ const userData: EconomyData & { lastSlut?: number; totalSluts?: number; totalSlutEarnings?: number; totalSlutLosses?: number; failedSluts?: number } = await getEconomyData(client, guildId, userId);
if (!userData) {
throw createError(
"Failed to load economy data for slut command",
ErrorTypes.DATABASE,
- "Failed to load your economy data. Please try again later.",
+ "Deine Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId, guildId }
);
}
@@ -134,7 +136,7 @@ export default {
throw createError(
"Slut cooldown active",
ErrorTypes.RATE_LIMIT,
- `You need to wait before you can work again! Try again in **${Math.ceil(remainingTime / 60000)}** minutes.`,
+ `Du musst noch warten, bevor du wieder arbeiten kannst! Versuch es in **${Math.ceil(remainingTime / 60000)}** Minuten erneut.`,
{ timeRemaining: remainingTime, cooldownType: 'slut' }
);
}
@@ -169,11 +171,11 @@ export default {
const amountLabel = `${outcome.delta >= 0 ? '+' : '-'}$${Math.abs(outcome.delta).toLocaleString()}`;
const summaryLines = [
`${outcome.message}`,
- `💸 **Net Result:** ${amountLabel}`,
- `💳 **Current Balance:** $${userData.wallet.toLocaleString()}`,
- `📊 **Total Sessions:** ${userData.totalSluts}`,
- `💵 **Total Earned:** $${(userData.totalSlutEarnings || 0).toLocaleString()}`,
- `🧾 **Total Lost:** $${(userData.totalSlutLosses || 0).toLocaleString()}`
+ `💸 **Ergebnis:** ${amountLabel}`,
+ `💳 **Aktueller Kontostand:** $${userData.wallet.toLocaleString()}`,
+ `📊 **Jobs gesamt:** ${userData.totalSluts}`,
+ `💵 **Gesamt verdient:** $${(userData.totalSlutEarnings || 0).toLocaleString()}`,
+ `🧾 **Gesamt verloren:** $${(userData.totalSlutLosses || 0).toLocaleString()}`
];
const embed = createEmbed({
@@ -185,4 +187,4 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'slut' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/withdraw.js b/src/commands/Economy/modules/withdraw.ts
similarity index 72%
rename from src/commands/Economy/withdraw.js
rename to src/commands/Economy/modules/withdraw.ts
index bf45af02ad..f0e0f061d3 100644
--- a/src/commands/Economy/withdraw.js
+++ b/src/commands/Economy/modules/withdraw.ts
@@ -1,17 +1,18 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName('withdraw')
- .setDescription('Withdraw money from your bank to your wallet')
+ .setDescription('Hebe Geld von deiner Bank in deine Geldbörse ab')
.addIntegerOption(option =>
option
.setName('amount')
- .setDescription('Amount to withdraw')
+ .setDescription('Betrag, den du abheben möchtest')
.setRequired(true)
.setMinValue(1)
),
@@ -20,8 +21,8 @@ export default {
await InteractionHelper.safeDefer(interaction);
const userId = interaction.user.id;
- const guildId = interaction.guildId;
- const amountInput = interaction.options.getInteger("amount");
+ const guildId = interaction.guildId!;
+ const amountInput = interaction.options.getInteger("amount")!;
const userData = await getEconomyData(client, guildId, userId);
@@ -29,7 +30,7 @@ export default {
throw createError(
"Failed to load economy data",
ErrorTypes.DATABASE,
- "Failed to load your economy data. Please try again later.",
+ "Deine Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId, guildId }
);
}
@@ -40,7 +41,7 @@ export default {
throw createError(
"Invalid withdrawal amount",
ErrorTypes.VALIDATION,
- "You must withdraw a positive amount.",
+ "Du musst einen positiven Betrag abheben.",
{ amount: withdrawAmount, userId }
);
}
@@ -53,7 +54,7 @@ export default {
throw createError(
"Empty bank account",
ErrorTypes.VALIDATION,
- "Your bank account is empty.",
+ "Dein Bankkonto ist leer.",
{ userId, bankBalance: userData.bank }
);
}
@@ -64,17 +65,17 @@ export default {
await setEconomyData(client, guildId, userId, userData);
const embed = successEmbed(
- 'Withdrawal Successful',
- `You successfully withdrew **$${withdrawAmount.toLocaleString()}** from your bank.`
+ 'Abhebung erfolgreich',
+ `Du hast **$${withdrawAmount.toLocaleString()}** von deiner Bank abgehoben.`
)
.addFields(
{
- name: "New Cash Balance",
+ name: "Geldbörse (neu)",
value: `$${userData.wallet.toLocaleString()}`,
inline: true,
},
{
- name: "New Bank Balance",
+ name: "Bank (neu)",
value: `$${userData.bank.toLocaleString()}`,
inline: true,
},
@@ -82,4 +83,4 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'withdraw' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Economy/work.js b/src/commands/Economy/modules/work.ts
similarity index 72%
rename from src/commands/Economy/work.js
rename to src/commands/Economy/modules/work.ts
index 775be897ad..ef704a473f 100644
--- a/src/commands/Economy/work.js
+++ b/src/commands/Economy/modules/work.ts
@@ -1,39 +1,40 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { getEconomyData, setEconomyData } from '../../utils/economy.js';
-import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { botConfig } from '../../config/bot.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { getEconomyData, setEconomyData } from '../../../utils/economy.js';
+import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { botConfig } from '../../../config/bot.js';
const WORK_COOLDOWN = botConfig.economy?.cooldowns?.work ?? 30 * 60 * 1000;
const MIN_WORK_AMOUNT = botConfig.economy?.workMin ?? 10;
const MAX_WORK_AMOUNT = botConfig.economy?.workMax ?? 100;
const LAPTOP_MULTIPLIER = 1.5;
const WORK_JOBS = [
- "Software Developer",
+ "Softwareentwickler",
"Barista",
- "Janitor",
+ "Hausmeister",
"YouTuber",
- "Discord Bot Developer",
- "Cashier",
- "Pizza Delivery Driver",
- "Librarian",
- "Gardener",
- "Data Analyst",
+ "Discord-Bot-Entwickler",
+ "Kassierer",
+ "Pizzalieferant",
+ "Bibliothekar",
+ "Gärtner",
+ "Datenanalyst",
];
export default {
data: new SlashCommandBuilder()
.setName('work')
- .setDescription('Work to earn some money'),
+ .setDescription('Arbeite, um etwas Geld zu verdienen'),
execute: withErrorHandling(async (interaction, config, client) => {
const deferred = await InteractionHelper.safeDefer(interaction);
if (!deferred) return;
const userId = interaction.user.id;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const now = Date.now();
const userData = await getEconomyData(client, guildId, userId);
@@ -42,7 +43,7 @@ export default {
throw createError(
"Failed to load economy data for work",
ErrorTypes.DATABASE,
- "Failed to load your economy data. Please try again later.",
+ "Deine Wirtschaftsdaten konnten nicht geladen werden. Bitte versuch es später erneut.",
{ userId, guildId }
);
}
@@ -66,7 +67,7 @@ export default {
throw createError(
"Work cooldown active",
ErrorTypes.RATE_LIMIT,
- `You're working too fast! Wait **${Math.floor(remaining / 3600000)}h ${Math.floor((remaining % 3600000) / 60000)}m** before working again.`,
+ `Du arbeitest zu schnell! Warte **${Math.floor(remaining / 3600000)}h ${Math.floor((remaining % 3600000) / 60000)}m**, bevor du wieder arbeitest.`,
{ timeRemaining: remaining, cooldownType: 'work' }
);
}
@@ -78,7 +79,7 @@ export default {
let multiplierMessage = "";
if (hasLaptop > 0) {
earned = Math.floor(earned * LAPTOP_MULTIPLIER);
- multiplierMessage = "\n💻 **Laptop Bonus:** +50% earnings!";
+ multiplierMessage = "\n💻 **Laptop-Bonus:** +50 % Verdienst!";
}
userData.wallet = (userData.wallet || 0) + earned;
@@ -98,26 +99,26 @@ export default {
});
const embed = successEmbed(
- "💼 Work Complete!",
- `You worked as a **${job}** and earned **$${earned.toLocaleString()}**!${multiplierMessage}`
+ "💼 Arbeit erledigt!",
+ `Du hast als **${job}** gearbeitet und **$${earned.toLocaleString()}** verdient!${multiplierMessage}`
)
.addFields(
{
- name: "New Balance",
+ name: "Neuer Kontostand",
value: `$${userData.wallet.toLocaleString()}`,
inline: true,
},
{
- name: "Next Work",
+ name: "Nächste Schicht",
value: ``,
inline: true,
}
)
.setFooter({
- text: `Requested by ${interaction.user.tag}`,
+ text: `Angefragt von ${interaction.user.tag}`,
iconURL: interaction.user.displayAvatarURL(),
});
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}, { command: 'work' })
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Fun/count.js b/src/commands/Fun/count.ts
similarity index 62%
rename from src/commands/Fun/count.js
rename to src/commands/Fun/count.ts
index 1dbd6014b1..7e91fa9108 100644
--- a/src/commands/Fun/count.js
+++ b/src/commands/Fun/count.ts
@@ -1,3 +1,4 @@
+import type { Command } from '../../types/index.js';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, MessageFlags } from 'discord.js';
import { createEmbed, successEmbed, infoEmbed } from '../../utils/embeds.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
@@ -17,47 +18,47 @@ import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName('count')
- .setDescription('Manage the server counting game')
+ .setDescription('Verwalte das Zählspiel des Servers')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.setDMPermission(false)
.addSubcommand((subcommand) =>
subcommand
.setName('setup')
- .setDescription('Start a counting game in a text channel')
+ .setDescription('Starte ein Zählspiel in einem Textkanal')
.addChannelOption((option) =>
option
.setName('channel')
- .setDescription('The channel where counting will take place')
+ .setDescription('Der Kanal, in dem gezählt wird')
.setRequired(true)
.addChannelTypes(ChannelType.GuildText),
)
.addStringOption((option) =>
option
.setName('system')
- .setDescription('The counting system to use')
+ .setDescription('Das Zahlensystem, das genutzt wird')
.setRequired(true)
.addChoices(...getCountingSystemChoices()),
),
)
.addSubcommand((subcommand) =>
- subcommand.setName('disable').setDescription('Disable the counting game for this server'),
+ subcommand.setName('disable').setDescription('Deaktiviere das Zählspiel für diesen Server'),
)
.addSubcommand((subcommand) =>
- subcommand.setName('status').setDescription('View current counting game status'),
+ subcommand.setName('status').setDescription('Zeige den aktuellen Status des Zählspiels'),
)
.addSubcommand((subcommand) =>
subcommand
.setName('reset')
- .setDescription('Reset the current counting sequence')
+ .setDescription('Setze die aktuelle Zählfolge zurück')
.addIntegerOption((option) =>
option
.setName('start')
- .setDescription('The number to start at after reset')
+ .setDescription('Die Startzahl nach dem Zurücksetzen')
.setMinValue(1),
),
)
.addSubcommand((subcommand) =>
- subcommand.setName('leaderboard').setDescription('Show the counting game leaderboard'),
+ subcommand.setName('leaderboard').setDescription('Zeige die Bestenliste des Zählspiels'),
),
category: 'Fun',
@@ -70,30 +71,30 @@ export default {
}
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the **Manage Server** permission to use this command.' });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um diesen Command zu nutzen.' });
}
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
const subcommand = interaction.options.getSubcommand();
const config = await getCountingGameConfig(interaction.client, guildId);
if (subcommand === 'setup') {
const channel = interaction.options.getChannel('channel');
- const system = interaction.options.getString('system');
+ const system = interaction.options.getString('system')!;
if (!channel || channel.type !== ChannelType.GuildText) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please choose a text channel for the counting game.' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte wähle einen Textkanal für das Zählspiel.' });
}
if (config.enabled && config.channelId && config.channelId !== channel.id) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `This server already has an active counting channel configured: <#${config.channelId}>. Disable the current counting game first, or use that existing channel.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Auf diesem Server ist bereits ein Zählkanal aktiv: <#${config.channelId}>. Deaktiviere zuerst das aktuelle Zählspiel oder nutze diesen Kanal.` });
}
await activateCountingGame(interaction.client, guildId, channel.id, system);
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- 'Counting Game Enabled',
- `The counting game is now active in ${channel} using the **${getCountingSystemLabel(system)}** system. Players must count up from **1** and may not post two numbers in a row.`,
+ 'Zählspiel aktiviert',
+ `Das Zählspiel ist jetzt in ${channel} mit dem System **${getCountingSystemLabel(system)}** aktiv. Gezählt wird ab **1**, und niemand darf zwei Zahlen hintereinander posten.`,
),
],
});
@@ -102,32 +103,32 @@ export default {
if (subcommand === 'disable') {
if (!config.enabled) {
return await InteractionHelper.safeEditReply(interaction, {
- embeds: [infoEmbed('Counting Game Disabled', 'The counting game is already disabled for this server.')],
+ embeds: [infoEmbed('Zählspiel deaktiviert', 'Das Zählspiel ist auf diesem Server bereits deaktiviert.')],
});
}
await disableCountingGame(interaction.client, guildId);
return await InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed('Counting Game Disabled', 'The counting game has been disabled.')],
+ embeds: [successEmbed('Zählspiel deaktiviert', 'Das Zählspiel wurde deaktiviert.')],
});
}
if (subcommand === 'status') {
const fields = [
- { name: 'Enabled', value: config.enabled ? 'Yes' : 'No', inline: true },
- { name: 'Channel', value: config.channelId ? `<#${config.channelId}>` : 'Not configured', inline: true },
+ { name: 'Aktiviert', value: config.enabled ? 'Ja' : 'Nein', inline: true },
+ { name: 'Kanal', value: config.channelId ? `<#${config.channelId}>` : 'Nicht eingerichtet', inline: true },
{ name: 'System', value: getCountingSystemLabel(config.system), inline: true },
- { name: 'Next count', value: getExpectedCountValue(config), inline: true },
- { name: 'Current streak', value: `${config.currentStreak}`, inline: true },
- { name: 'Best streak', value: `${config.bestStreak || 0}`, inline: true },
- { name: 'Last counter', value: config.lastUserId ? `<@${config.lastUserId}>` : 'None', inline: true },
+ { name: 'Nächste Zahl', value: getExpectedCountValue(config), inline: true },
+ { name: 'Aktuelle Serie', value: `${config.currentStreak}`, inline: true },
+ { name: 'Beste Serie', value: `${config.bestStreak || 0}`, inline: true },
+ { name: 'Zuletzt gezählt von', value: config.lastUserId ? `<@${config.lastUserId}>` : 'Niemand', inline: true },
];
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
createEmbed({
- title: 'Counting Game Status',
- description: 'Overview of the currently configured counting game.',
+ title: 'Status des Zählspiels',
+ description: 'Übersicht über das aktuell eingerichtete Zählspiel.',
fields,
color: 'primary',
}),
@@ -137,7 +138,7 @@ export default {
if (subcommand === 'reset') {
if (!config.enabled) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Enable the counting game first with `/count setup`.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Aktiviere das Zählspiel zuerst mit `/count setup`.' });
}
const startNumber = interaction.options.getInteger('start') || 1;
@@ -146,8 +147,8 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- 'Counting Game Reset',
- `The counting sequence has been reset. Start again with **${startNumber}** in <#${config.channelId}>.`,
+ 'Zählspiel zurückgesetzt',
+ `Die Zählfolge wurde zurückgesetzt. Fang in <#${config.channelId}> wieder mit **${startNumber}** an.`,
),
],
});
@@ -159,18 +160,18 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
createEmbed({
- title: 'Counting Game Leaderboard',
- description: leaderboard.length > 0 ? leaderboard.join('\n') : 'No counts have been recorded yet.',
+ title: 'Bestenliste des Zählspiels',
+ description: leaderboard.length > 0 ? leaderboard.join('\n') : 'Bisher wurde noch nicht gezählt.',
color: 'primary',
}),
],
});
}
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please choose a valid counting game action.' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte wähle eine gültige Aktion für das Zählspiel.' });
} catch (error) {
logger.error('Count command error:', error);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Something went wrong while managing the counting game.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Verwalten des Zählspiels ist etwas schiefgelaufen.' });
}
},
-};
+} satisfies Command;
diff --git a/src/commands/Fun/flip.js b/src/commands/Fun/flip.js
deleted file mode 100644
index 2eeef5cfa9..0000000000
--- a/src/commands/Fun/flip.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-export default {
- data: new SlashCommandBuilder()
- .setName("flip")
- .setDescription("Flips a coin (Heads or Tails)."),
- category: 'Fun',
-
- async execute(interaction, config, client) {
- const result = Math.random() < 0.5 ? "Heads" : "Tails";
- const emoji = result === "Heads" ? "🪙" : "🔮";
-
- const embed = successEmbed(
- "Heads or Tails?",
- `The coin landed on... **${result}** ${emoji}!`,
- );
-
- await InteractionHelper.safeReply(interaction, { embeds: [embed] });
- logger.debug(`Flip command executed by user ${interaction.user.id} in guild ${interaction.guildId}`);
- },
-};
\ No newline at end of file
diff --git a/src/commands/Fun/fun.ts b/src/commands/Fun/fun.ts
new file mode 100644
index 0000000000..e07106a8b5
--- /dev/null
+++ b/src/commands/Fun/fun.ts
@@ -0,0 +1,15 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import fightCommand from './modules/fight.js';
+import flipCommand from './modules/flip.js';
+import rollCommand from './modules/roll.js';
+
+export default createRouterCommand({
+ name: 'fun',
+ description: 'Spaß-Commands und Minispiele',
+ category: 'Fun',
+ routes: [
+ { legacyName: 'fight', module: fightCommand },
+ { legacyName: 'flip', module: flipCommand },
+ { legacyName: 'roll', module: rollCommand },
+ ],
+});
diff --git a/src/commands/Fun/fight.js b/src/commands/Fun/modules/fight.ts
similarity index 55%
rename from src/commands/Fun/fight.js
rename to src/commands/Fun/modules/fight.ts
index 3093af2419..c91aa03131 100644
--- a/src/commands/Fun/fight.js
+++ b/src/commands/Fun/modules/fight.ts
@@ -1,19 +1,20 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { successEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
+import { successEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+const rand = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
const EMBED_DESCRIPTION_LIMIT = 4096;
export default {
data: new SlashCommandBuilder()
.setName("fight")
- .setDescription("Starts a simulated 1v1 text-based battle.")
+ .setDescription("Startet einen simulierten 1-gegen-1-Textkampf.")
.addUserOption((option) =>
option
.setName("opponent")
- .setDescription("The user to fight.")
+ .setDescription("Der Nutzer, gegen den du kämpfst.")
.setRequired(true),
),
category: 'Fun',
@@ -22,20 +23,20 @@ export default {
await InteractionHelper.safeDefer(interaction);
const challenger = interaction.user;
- const opponent = interaction.options.getUser("opponent");
+ const opponent = interaction.options.getUser("opponent")!;
if (challenger.id === opponent.id) {
const embed = warningEmbed(
- "⚔️ Invalid Challenge",
- `**${challenger.username}**, you can't fight yourself! That's a draw before it even starts.`
+ "⚔️ Ungültige Herausforderung",
+ `**${challenger.username}**, du kannst nicht gegen dich selbst kämpfen! Das wäre ein Unentschieden, bevor es überhaupt losgeht.`
);
return await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}
if (opponent.bot) {
const embed = warningEmbed(
- "⚔️ Invalid Opponent",
- "You can't fight bots! Challenge a real person instead."
+ "⚔️ Ungültiger Gegner",
+ "Du kannst nicht gegen Bots kämpfen! Fordere lieber eine echte Person heraus."
);
return await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}
@@ -45,27 +46,27 @@ export default {
const rounds = rand(3, 7);
const damage = rand(10, 50);
- const log = [];
+ const log: string[] = [];
log.push(
- `💥 **${challenger.username}** challenges **${opponent.username}** to a duel! (Best of ${rounds} rounds)`,
+ `💥 **${challenger.username}** fordert **${opponent.username}** zum Duell heraus! (${rounds} Runden)`,
);
for (let i = 1; i <= rounds; i++) {
const attacker = rand(0, 1) === 0 ? challenger : opponent;
const target = attacker.id === challenger.id ? opponent : challenger;
const action = [
- "throws a wild punch",
- "lands a critical hit",
- "uses a weak spell",
- "parries and counterattacks",
+ "schlägt wild zu",
+ "landet einen kritischen Treffer",
+ "wirkt einen schwachen Zauber",
+ "pariert und kontert",
][rand(0, 3)];
log.push(
- `\n**Round ${i}:** ${attacker.username} ${action} on ${target.username} for ${rand(1, damage)} damage!`,
+ `\n**Runde ${i}:** ${attacker.username} ${action} gegen ${target.username} und verursacht ${rand(1, damage)} Schaden!`,
);
}
const outcomeText = log.join("\n");
- const winnerText = `👑 **${winner.username}** has defeated ${loser.username} and claims the victory!`;
+ const winnerText = `👑 **${winner.username}** hat ${loser.username} besiegt und holt sich den Sieg!`;
const fullDescription = `${outcomeText}\n\n${winnerText}`;
const description = fullDescription.length <= EMBED_DESCRIPTION_LIMIT
@@ -73,11 +74,11 @@ export default {
: `${fullDescription.slice(0, EMBED_DESCRIPTION_LIMIT - 15)}\n\n...`;
const embed = successEmbed(
- "🏆 Duel Complete!",
+ "🏆 Duell beendet!",
description
);
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
logger.debug(`Fight command executed between ${challenger.id} and ${opponent.id} in guild ${interaction.guildId}`);
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Fun/modules/flip.ts b/src/commands/Fun/modules/flip.ts
new file mode 100644
index 0000000000..a036e1ff78
--- /dev/null
+++ b/src/commands/Fun/modules/flip.ts
@@ -0,0 +1,26 @@
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder } from 'discord.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+export default {
+ data: new SlashCommandBuilder()
+ .setName("flip")
+ .setDescription("Wirft eine Münze (Kopf oder Zahl)."),
+ category: 'Fun',
+
+ async execute(interaction, config, client) {
+ const result = Math.random() < 0.5 ? "Kopf" : "Zahl";
+ const emoji = result === "Kopf" ? "🪙" : "🔮";
+
+ const embed = successEmbed(
+ "Kopf oder Zahl?",
+ `Die Münze zeigt... **${result}** ${emoji}!`,
+ );
+
+ await InteractionHelper.safeReply(interaction, { embeds: [embed] });
+ logger.debug(`Flip command executed by user ${interaction.user.id} in guild ${interaction.guildId}`);
+ },
+} satisfies Command;
diff --git a/src/commands/Fun/roll.js b/src/commands/Fun/modules/roll.ts
similarity index 63%
rename from src/commands/Fun/roll.js
rename to src/commands/Fun/modules/roll.ts
index 988fe1b663..8b6acbaa74 100644
--- a/src/commands/Fun/roll.js
+++ b/src/commands/Fun/modules/roll.ts
@@ -1,17 +1,18 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("roll")
- .setDescription("Rolls dice using standard notation (e.g., 2d20, 1d6 + 5).")
+ .setDescription("Würfelt mit Standardnotation (z. B. 2d20, 1d6 + 5).")
.addStringOption((option) =>
option
.setName("notation")
- .setDescription("The dice notation (e.g., 2d6, 1d20 + 4)")
+ .setDescription("Die Würfelnotation (z. B. 2d6, 1d20 + 4)")
.setRequired(true)
.setMaxLength(50),
),
@@ -21,17 +22,17 @@ export default {
await InteractionHelper.safeDefer(interaction);
const notation = interaction.options
- .getString("notation")
+ .getString("notation")!
.toLowerCase()
.replace(/\s/g, "");
const match = notation.match(/^(\d*)d(\d+)([\+\-]\d+)?$/);
if (!match) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Invalid dice notation: ${notation}`,
ErrorTypes.USER_INPUT,
- 'Invalid notation. Use format like `1d20` or `3d6+5`.'
+ 'Ungültige Notation. Nutze ein Format wie `1d20` oder `3d6+5`.'
);
}
@@ -40,18 +41,18 @@ export default {
const modifier = parseInt(match[3] || "0", 10);
if (numDice < 1 || numDice > 20) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Too many dice requested: ${numDice}`,
ErrorTypes.VALIDATION,
- 'Please keep the number of dice between 1 and 20.'
+ 'Die Anzahl der Würfel muss zwischen 1 und 20 liegen.'
);
}
if (numSides < 1 || numSides > 1000) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Invalid number of sides: ${numSides}`,
ErrorTypes.VALIDATION,
- 'Please keep the number of sides between 1 and 1000.'
+ 'Die Anzahl der Seiten muss zwischen 1 und 1000 liegen.'
);
}
@@ -67,15 +68,15 @@ export default {
const finalTotal = totalRoll + modifier;
const resultsDetail =
- numDice > 1 ? `**Rolls:** ${rolls.join(" + ")}\n` : "";
+ numDice > 1 ? `**Würfe:** ${rolls.join(" + ")}\n` : "";
const modifierText = modifier !== 0 ? `+ (${modifier})` : "";
const embed = successEmbed(
- `🎲 Rolling ${numDice}d${numSides}${modifier !== 0 ? match[3] : ""}`,
- `${resultsDetail}**Total Roll:** ${totalRoll}${modifierText} = **${finalTotal}**`,
+ `🎲 Würfle ${numDice}d${numSides}${modifier !== 0 ? match[3] : ""}`,
+ `${resultsDetail}**Gesamtwurf:** ${totalRoll}${modifierText} = **${finalTotal}**`,
);
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
logger.debug(`Roll command executed by user ${interaction.user.id} with notation ${notation} in guild ${interaction.guildId}`);
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Giveaway/giveaway.ts b/src/commands/Giveaway/giveaway.ts
new file mode 100644
index 0000000000..e3989a0aee
--- /dev/null
+++ b/src/commands/Giveaway/giveaway.ts
@@ -0,0 +1,19 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import gcreateCommand from './modules/gcreate.js';
+import gdeleteCommand from './modules/gdelete.js';
+import gendCommand from './modules/gend.js';
+import grerollCommand from './modules/greroll.js';
+
+export default createRouterCommand({
+ name: 'giveaway',
+ description: 'Giveaways erstellen und verwalten',
+ category: 'Giveaway',
+ defaultMemberPermissions: PermissionFlagsBits.ManageGuild,
+ routes: [
+ { legacyName: 'gcreate', module: gcreateCommand },
+ { legacyName: 'gdelete', module: gdeleteCommand },
+ { legacyName: 'gend', module: gendCommand },
+ { legacyName: 'greroll', module: grerollCommand },
+ ],
+});
diff --git a/src/commands/Giveaway/gcreate.js b/src/commands/Giveaway/modules/gcreate.ts
similarity index 69%
rename from src/commands/Giveaway/gcreate.js
rename to src/commands/Giveaway/modules/gcreate.ts
index e5f19c048c..230cc7a3c5 100644
--- a/src/commands/Giveaway/gcreate.js
+++ b/src/commands/Giveaway/modules/gcreate.ts
@@ -1,19 +1,21 @@
-import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, MessageFlags } from 'discord.js';
-import { errorEmbed, successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { saveGiveaway } from '../../utils/giveaways.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, MessageFlags, type GuildMember, type GuildBasedChannel } from 'discord.js';
+import { errorEmbed, successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { saveGiveaway } from '../../../utils/giveaways.js';
+import type { Giveaway } from '../../../utils/giveaways.js';
import {
parseDuration,
validatePrize,
validateWinnerCount,
createGiveawayEmbed,
createGiveawayButtons
-} from '../../services/giveawayService.js';
-import { logEvent, EVENT_TYPES } from '../../services/loggingService.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+} from '../../../services/giveawayService.js';
+import { logEvent, EVENT_TYPES } from '../../../services/loggingService.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
-import { botConfig } from '../../config/bot.js';
+import { botConfig } from '../../../config/bot.js';
const GIVEAWAY_MIN_WINNERS = botConfig.giveaways?.minimumWinners ?? 1;
const GIVEAWAY_MAX_WINNERS = botConfig.giveaways?.maximumWinners ?? 10;
@@ -21,19 +23,19 @@ const GIVEAWAY_MAX_WINNERS = botConfig.giveaways?.maximumWinners ?? 10;
export default {
data: new SlashCommandBuilder()
.setName("gcreate")
- .setDescription("Starts a new giveaway in a specified channel.")
+ .setDescription("Startet ein neues Giveaway in einem bestimmten Kanal.")
.addStringOption((option) =>
option
.setName("duration")
.setDescription(
- "How long the giveaway should last (e.g., 1h, 30m, 5d).",
+ "Wie lange das Giveaway laufen soll (z. B. 1h, 30m, 5d).",
)
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName("winners")
- .setDescription("The number of winners to pick.")
+ .setDescription("Die Anzahl der Gewinner.")
.setMinValue(GIVEAWAY_MIN_WINNERS)
.setMaxValue(GIVEAWAY_MAX_WINNERS)
.setRequired(true),
@@ -41,13 +43,13 @@ export default {
.addStringOption((option) =>
option
.setName("prize")
- .setDescription("The prize being given away.")
+ .setDescription("Der Preis, der verlost wird.")
.setRequired(true),
)
.addChannelOption((option) =>
option
.setName("channel")
- .setDescription("The channel to send the giveaway to (defaults to current channel).")
+ .setDescription("Der Kanal für das Giveaway (Standard: aktueller Kanal).")
.addChannelTypes(ChannelType.GuildText)
.setRequired(false),
)
@@ -58,19 +60,19 @@ export default {
await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
if (!interaction.inGuild()) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Giveaway command used outside guild',
ErrorTypes.VALIDATION,
- 'This command can only be used in a server.',
+ 'Dieser Command kann nur auf einem Server genutzt werden.',
{ userId: interaction.user.id }
);
}
- if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) {
- throw new TitanBotError(
+ if (!(interaction.member as GuildMember).permissions.has(PermissionFlagsBits.ManageGuild)) {
+ throw new NexoBotError(
'User lacks ManageGuild permission',
ErrorTypes.PERMISSION,
- "You need the 'Manage Server' permission to start a giveaway.",
+ "Du brauchst die Berechtigung „Server verwalten“, um ein Giveaway zu starten.",
{ userId: interaction.user.id, guildId: interaction.guildId }
);
}
@@ -78,26 +80,26 @@ export default {
logger.info(`Giveaway creation started by ${interaction.user.tag} in guild ${interaction.guildId}`);
const durationString = interaction.options.getString("duration");
- const winnerCount = interaction.options.getInteger("winners");
+ const winnerCount = interaction.options.getInteger("winners")!;
const prize = interaction.options.getString("prize");
- const targetChannel = interaction.options.getChannel("channel") || interaction.channel;
+ const targetChannel = (interaction.options.getChannel("channel") || interaction.channel) as GuildBasedChannel;
const durationMs = parseDuration(durationString);
validateWinnerCount(winnerCount);
const prizeName = validatePrize(prize);
if (!targetChannel.isTextBased()) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Target channel is not text-based',
ErrorTypes.VALIDATION,
- 'The channel must be a text channel.',
+ 'Der Kanal muss ein Textkanal sein.',
{ channelId: targetChannel.id, channelType: targetChannel.type }
);
}
const endTime = Date.now() + durationMs;
- const initialGiveawayData = {
+ const initialGiveawayData: Giveaway = {
messageId: "placeholder",
channelId: targetChannel.id,
guildId: interaction.guildId,
@@ -116,7 +118,7 @@ export default {
const row = createGiveawayButtons(false);
const giveawayMessage = await targetChannel.send({
- content: "🎉 **NEW GIVEAWAY** 🎉",
+ content: "🎉 **NEUES GIVEAWAY** 🎉",
embeds: [embed],
components: [row],
});
@@ -138,27 +140,27 @@ export default {
guildId: interaction.guildId,
eventType: EVENT_TYPES.GIVEAWAY_CREATE,
data: {
- description: `Giveaway created: ${prizeName}`,
+ description: `Giveaway erstellt: ${prizeName}`,
channelId: targetChannel.id,
userId: interaction.user.id,
fields: [
{
- name: 'Prize',
+ name: 'Preis',
value: prizeName,
inline: true
},
{
- name: 'Winners',
+ name: 'Gewinner',
value: winnerCount.toString(),
inline: true
},
{
- name: 'Duration',
+ name: 'Dauer',
value: durationString,
inline: true
},
{
- name: 'Channel',
+ name: 'Kanal',
value: targetChannel.toString(),
inline: true
}
@@ -174,11 +176,11 @@ export default {
await InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- `Giveaway Started! 🎉`,
- `A new giveaway for **${prizeName}** has been started in ${targetChannel} and will end in **${durationString}**.`,
+ `Giveaway gestartet! 🎉`,
+ `Ein neues Giveaway für **${prizeName}** läuft jetzt in ${targetChannel} und endet in **${durationString}**.`,
),
],
flags: MessageFlags.Ephemeral,
});
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Giveaway/gdelete.js b/src/commands/Giveaway/modules/gdelete.ts
similarity index 65%
rename from src/commands/Giveaway/gdelete.js
rename to src/commands/Giveaway/modules/gdelete.ts
index 838209c0c0..f8b9825fa3 100644
--- a/src/commands/Giveaway/gdelete.js
+++ b/src/commands/Giveaway/modules/gdelete.ts
@@ -1,40 +1,41 @@
-import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { errorEmbed, successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { getGuildGiveaways, deleteGiveaway } from '../../utils/giveaways.js';
-import { logEvent, EVENT_TYPES } from '../../services/loggingService.js';
-
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, type GuildMember, type Channel } from 'discord.js';
+import { errorEmbed, successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { getGuildGiveaways, deleteGiveaway } from '../../../utils/giveaways.js';
+import { logEvent, EVENT_TYPES } from '../../../services/loggingService.js';
+
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("gdelete")
.setDescription(
- "Deletes a giveaway message and removes it from the database.",
+ "Löscht eine Giveaway-Nachricht und entfernt sie aus der Datenbank.",
)
.addStringOption((option) =>
option
.setName("messageid")
- .setDescription("The message ID of the giveaway to delete.")
+ .setDescription("Die Nachrichten-ID des Giveaways, das gelöscht werden soll.")
.setRequired(true),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild),
async execute(interaction) {
if (!interaction.inGuild()) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Giveaway command used outside guild',
ErrorTypes.VALIDATION,
- 'This command can only be used in a server.',
+ 'Dieser Command kann nur auf einem Server genutzt werden.',
{ userId: interaction.user.id }
);
}
- if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) {
- throw new TitanBotError(
+ if (!(interaction.member as GuildMember).permissions.has(PermissionFlagsBits.ManageGuild)) {
+ throw new NexoBotError(
'User lacks ManageGuild permission',
ErrorTypes.PERMISSION,
- "You need the 'Manage Server' permission to delete a giveaway.",
+ "Du brauchst die Berechtigung „Server verwalten“, um ein Giveaway zu löschen.",
{ userId: interaction.user.id, guildId: interaction.guildId }
);
}
@@ -44,10 +45,10 @@ export default {
const messageId = interaction.options.getString("messageid");
if (!messageId || !/^\d+$/.test(messageId)) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Invalid message ID format',
ErrorTypes.VALIDATION,
- 'Please provide a valid message ID.',
+ 'Bitte gib eine gültige Nachrichten-ID an.',
{ providedId: messageId }
);
}
@@ -56,18 +57,18 @@ export default {
const giveaway = giveaways.find(g => g.messageId === messageId);
if (!giveaway) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Giveaway not found: ${messageId}`,
ErrorTypes.VALIDATION,
- "No giveaway was found with that message ID.",
+ "Mit dieser Nachrichten-ID wurde kein Giveaway gefunden.",
{ messageId, guildId: interaction.guildId }
);
}
let deletedMessage = false;
- let channelName = "Unknown Channel";
+ let channelName = "Unbekannter Kanal";
- const tryDeleteFromChannel = async (channel) => {
+ const tryDeleteFromChannel = async (channel: Channel | null) => {
if (!channel || !channel.isTextBased() || !channel.messages?.fetch) {
return false;
}
@@ -78,13 +79,13 @@ export default {
}
await message.delete();
- channelName = channel.name || 'unknown-channel';
+ channelName = (channel as { name?: string | null }).name || 'unbekannter-kanal';
deletedMessage = true;
return true;
};
try {
- const channel = await interaction.client.channels.fetch(giveaway.channelId).catch(() => null);
+ const channel = await interaction.client.channels.fetch(giveaway.channelId!).catch(() => null);
if (await tryDeleteFromChannel(channel)) {
logger.debug(`Deleted giveaway message ${messageId} from channel ${channelName}`);
}
@@ -103,7 +104,7 @@ export default {
}
}
} catch (error) {
- logger.warn(`Could not delete giveaway message: ${error.message}`);
+ logger.warn(`Could not delete giveaway message: ${error instanceof Error ? error.message : String(error)}`);
}
const removedFromDatabase = await deleteGiveaway(
@@ -113,10 +114,10 @@ export default {
);
if (!removedFromDatabase) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Failed to delete giveaway from database: ${messageId}`,
ErrorTypes.UNKNOWN,
- 'The giveaway could not be removed from the database. Please try again.',
+ 'Das Giveaway konnte nicht aus der Datenbank entfernt werden. Bitte versuch es erneut.',
{ messageId, guildId: interaction.guildId }
);
}
@@ -125,27 +126,27 @@ export default {
const stillExistsInDatabase = giveawaysAfterDelete.some(g => g.messageId === messageId);
if (stillExistsInDatabase) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Giveaway still exists after deletion: ${messageId}`,
ErrorTypes.UNKNOWN,
- 'Deletion did not persist in the database. Please try again.',
+ 'Das Löschen wurde nicht in der Datenbank gespeichert. Bitte versuch es erneut.',
{ messageId, guildId: interaction.guildId }
);
}
const statusMsg = deletedMessage
- ? `and the message was deleted from #${channelName}`
- : `but the message was already deleted or the channel was inaccessible.`;
+ ? ` und die Nachricht wurde aus #${channelName} entfernt`
+ : `, die Nachricht war aber bereits gelöscht oder der Kanal war nicht erreichbar`;
const winnerIds = Array.isArray(giveaway.winnerIds) ? giveaway.winnerIds : [];
const hasWinners = winnerIds.length > 0;
const wasEnded = giveaway.ended === true || giveaway.isEnded === true || hasWinners;
const winnerStatusMsg = hasWinners
- ? `This giveaway already had ${winnerIds.length} winner(s) selected.`
+ ? `Für dieses Giveaway wurden bereits ${winnerIds.length} Gewinner gezogen.`
: wasEnded
- ? 'This giveaway was ended with no valid winners.'
- : 'No winner was picked before deletion.';
+ ? 'Dieses Giveaway wurde ohne gültige Gewinner beendet.'
+ : 'Vor dem Löschen wurde kein Gewinner gezogen.';
logger.info(`Giveaway deleted: ${messageId} in ${channelName}`);
@@ -155,17 +156,17 @@ export default {
guildId: interaction.guildId,
eventType: EVENT_TYPES.GIVEAWAY_DELETE,
data: {
- description: `Giveaway deleted: ${giveaway.prize}`,
+ description: `Giveaway gelöscht: ${giveaway.prize}`,
channelId: giveaway.channelId,
userId: interaction.user.id,
fields: [
{
- name: 'Prize',
- value: giveaway.prize || 'Unknown',
+ name: 'Preis',
+ value: giveaway.prize || 'Unbekannt',
inline: true
},
{
- name: 'Entries',
+ name: 'Teilnahmen',
value: (giveaway.participants?.length || 0).toString(),
inline: true
}
@@ -179,11 +180,11 @@ export default {
return InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- "Giveaway Deleted",
- `Successfully deleted the giveaway for **${giveaway.prize}** ${statusMsg}. ${winnerStatusMsg}`,
+ "Giveaway gelöscht",
+ `Das Giveaway für **${giveaway.prize}** wurde gelöscht${statusMsg}. ${winnerStatusMsg}`,
),
],
flags: MessageFlags.Ephemeral,
});
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Giveaway/gend.js b/src/commands/Giveaway/modules/gend.ts
similarity index 64%
rename from src/commands/Giveaway/gend.js
rename to src/commands/Giveaway/modules/gend.ts
index 700ca5eaeb..bcc04cf561 100644
--- a/src/commands/Giveaway/gend.js
+++ b/src/commands/Giveaway/modules/gend.ts
@@ -1,45 +1,46 @@
-import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { errorEmbed, successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { getGuildGiveaways, saveGiveaway } from '../../utils/giveaways.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, type GuildMember, type SendableChannels } from 'discord.js';
+import { errorEmbed, successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { getGuildGiveaways, saveGiveaway } from '../../../utils/giveaways.js';
import {
endGiveaway as endGiveawayService,
createGiveawayEmbed,
createGiveawayButtons
-} from '../../services/giveawayService.js';
-import { logEvent, EVENT_TYPES } from '../../services/loggingService.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+} from '../../../services/giveawayService.js';
+import { logEvent, EVENT_TYPES } from '../../../services/loggingService.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("gend")
.setDescription(
- "Ends an active giveaway immediately and picks the winner(s).",
+ "Beendet ein aktives Giveaway sofort und zieht die Gewinner.",
)
.addStringOption((option) =>
option
.setName("messageid")
- .setDescription("The message ID of the giveaway to end.")
+ .setDescription("Die Nachrichten-ID des Giveaways, das beendet werden soll.")
.setRequired(true),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild),
async execute(interaction) {
if (!interaction.inGuild()) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Giveaway command used outside guild',
ErrorTypes.VALIDATION,
- 'This command can only be used in a server.',
+ 'Dieser Command kann nur auf einem Server genutzt werden.',
{ userId: interaction.user.id }
);
}
- if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) {
- throw new TitanBotError(
+ if (!(interaction.member as GuildMember).permissions.has(PermissionFlagsBits.ManageGuild)) {
+ throw new NexoBotError(
'User lacks ManageGuild permission',
ErrorTypes.PERMISSION,
- "You need the 'Manage Server' permission to end a giveaway.",
+ "Du brauchst die Berechtigung „Server verwalten“, um ein Giveaway zu beenden.",
{ userId: interaction.user.id, guildId: interaction.guildId }
);
}
@@ -49,10 +50,10 @@ export default {
const messageId = interaction.options.getString("messageid");
if (!messageId || !/^\d+$/.test(messageId)) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Invalid message ID format',
ErrorTypes.VALIDATION,
- 'Please provide a valid message ID.',
+ 'Bitte gib eine gültige Nachrichten-ID an.',
{ providedId: messageId }
);
}
@@ -61,10 +62,10 @@ export default {
const giveaway = giveaways.find(g => g.messageId === messageId);
if (!giveaway) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Giveaway not found: ${messageId}`,
ErrorTypes.VALIDATION,
- "No giveaway was found with that message ID in the database.",
+ "In der Datenbank wurde kein Giveaway mit dieser Nachrichten-ID gefunden.",
{ messageId, guildId: interaction.guildId }
);
}
@@ -80,17 +81,17 @@ export default {
const winners = endResult.winners;
const channel = await interaction.client.channels.fetch(
- updatedGiveaway.channelId,
+ updatedGiveaway.channelId!,
).catch(err => {
logger.warn(`Could not fetch channel ${updatedGiveaway.channelId}:`, err.message);
return null;
});
if (!channel || !channel.isTextBased()) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Channel not found: ${updatedGiveaway.channelId}`,
ErrorTypes.VALIDATION,
- "Could not find the channel where the giveaway was hosted. The giveaway state has been updated.",
+ "Der Kanal des Giveaways wurde nicht gefunden. Der Status des Giveaways wurde trotzdem aktualisiert.",
{ channelId: updatedGiveaway.channelId, messageId }
);
}
@@ -103,10 +104,10 @@ export default {
});
if (!message) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Message not found: ${messageId}`,
ErrorTypes.VALIDATION,
- "Could not find the giveaway message. The giveaway state has been updated.",
+ "Die Giveaway-Nachricht wurde nicht gefunden. Der Status des Giveaways wurde trotzdem aktualisiert.",
{ messageId, channelId: updatedGiveaway.channelId }
);
}
@@ -121,7 +122,7 @@ export default {
const newRow = createGiveawayButtons(true);
await message.edit({
- content: "🎉 **GIVEAWAY ENDED** 🎉",
+ content: "🎉 **GIVEAWAY BEENDET** 🎉",
embeds: [newEmbed],
components: [newRow],
});
@@ -130,8 +131,8 @@ export default {
const winnerMentions = winners
.map((id) => `<@${id}>`)
.join(",");
- const winnerPingMsg = await channel.send({
- content: `🎉 CONGRATULATIONS ${winnerMentions}! You won the **${updatedGiveaway.prize}** giveaway! Please contact the host <@${updatedGiveaway.hostId}> to claim your prize.`,
+ const winnerPingMsg = await (channel as SendableChannels).send({
+ content: `🎉 HERZLICHEN GLÜCKWUNSCH ${winnerMentions}! Du hast das Giveaway **${updatedGiveaway.prize}** gewonnen! Melde dich beim Host <@${updatedGiveaway.hostId}>, um deinen Preis abzuholen.`,
});
updatedGiveaway.winnerPingMessageId = winnerPingMsg.id;
await saveGiveaway(interaction.client, interaction.guildId, updatedGiveaway);
@@ -144,22 +145,22 @@ export default {
guildId: interaction.guildId,
eventType: EVENT_TYPES.GIVEAWAY_WINNER,
data: {
- description: `Giveaway ended with ${winners.length} winner(s)`,
+ description: `Giveaway mit ${winners.length} Gewinner(n) beendet`,
channelId: channel.id,
userId: interaction.user.id,
fields: [
{
- name: 'Prize',
- value: updatedGiveaway.prize || 'Mystery Prize!',
+ name: 'Preis',
+ value: updatedGiveaway.prize || 'Überraschungspreis!',
inline: true
},
{
- name: 'Winners',
+ name: 'Gewinner',
value: winnerMentions,
inline: false
},
{
- name: 'Entries',
+ name: 'Teilnahmen',
value: endResult.participantCount.toString(),
inline: true
}
@@ -170,8 +171,8 @@ export default {
logger.debug('Error logging giveaway winner event:', logError);
}
} else {
- await channel.send({
- content: `The giveaway for **${updatedGiveaway.prize}** has ended with no valid entries.`,
+ await (channel as SendableChannels).send({
+ content: `Das Giveaway für **${updatedGiveaway.prize}** ist ohne gültige Teilnahmen zu Ende gegangen.`,
});
logger.info(`Giveaway ended with no winners: ${messageId}`);
}
@@ -181,11 +182,11 @@ export default {
return InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- "Giveaway Ended ✅",
- `Successfully ended the giveaway for **${updatedGiveaway.prize}** in ${channel}. Selected ${winners.length} winner(s) from ${endResult.participantCount} entries.`,
+ "Giveaway beendet ✅",
+ `Das Giveaway für **${updatedGiveaway.prize}** in ${channel} wurde beendet. ${winners.length} Gewinner aus ${endResult.participantCount} Teilnahmen gezogen.`,
),
],
flags: MessageFlags.Ephemeral,
});
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Giveaway/greroll.js b/src/commands/Giveaway/modules/greroll.ts
similarity index 65%
rename from src/commands/Giveaway/greroll.js
rename to src/commands/Giveaway/modules/greroll.ts
index 64be4b3e03..e4a26bbfdb 100644
--- a/src/commands/Giveaway/greroll.js
+++ b/src/commands/Giveaway/modules/greroll.ts
@@ -1,43 +1,45 @@
-import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { errorEmbed, successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { getGuildGiveaways, saveGiveaway } from '../../utils/giveaways.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, type GuildMember, type SendableChannels } from 'discord.js';
+import { errorEmbed, successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { getGuildGiveaways, saveGiveaway } from '../../../utils/giveaways.js';
+import type { Giveaway } from '../../../utils/giveaways.js';
import {
selectWinners,
createGiveawayEmbed,
createGiveawayButtons
-} from '../../services/giveawayService.js';
-import { logEvent, EVENT_TYPES } from '../../services/loggingService.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+} from '../../../services/giveawayService.js';
+import { logEvent, EVENT_TYPES } from '../../../services/loggingService.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("greroll")
- .setDescription("Rerolls the winner(s) for an ended giveaway.")
+ .setDescription("Zieht neue Gewinner für ein beendetes Giveaway.")
.addStringOption((option) =>
option
.setName("messageid")
- .setDescription("The message ID of the ended giveaway.")
+ .setDescription("Die Nachrichten-ID des beendeten Giveaways.")
.setRequired(true),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild),
async execute(interaction) {
if (!interaction.inGuild()) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Giveaway command used outside guild',
ErrorTypes.VALIDATION,
- 'This command can only be used in a server.',
+ 'Dieser Command kann nur auf einem Server genutzt werden.',
{ userId: interaction.user.id }
);
}
- if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) {
- throw new TitanBotError(
+ if (!(interaction.member as GuildMember).permissions.has(PermissionFlagsBits.ManageGuild)) {
+ throw new NexoBotError(
'User lacks ManageGuild permission',
ErrorTypes.PERMISSION,
- "You need the 'Manage Server' permission to reroll a giveaway.",
+ "Du brauchst die Berechtigung „Server verwalten“, um neue Gewinner zu ziehen.",
{ userId: interaction.user.id, guildId: interaction.guildId }
);
}
@@ -47,10 +49,10 @@ export default {
const messageId = interaction.options.getString("messageid");
if (!messageId || !/^\d+$/.test(messageId)) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Invalid message ID format',
ErrorTypes.VALIDATION,
- 'Please provide a valid message ID.',
+ 'Bitte gib eine gültige Nachrichten-ID an.',
{ providedId: messageId }
);
}
@@ -63,40 +65,40 @@ export default {
const giveaway = giveaways.find(g => g.messageId === messageId);
if (!giveaway) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Giveaway not found: ${messageId}`,
ErrorTypes.VALIDATION,
- "No giveaway was found with that message ID in the database.",
+ "In der Datenbank wurde kein Giveaway mit dieser Nachrichten-ID gefunden.",
{ messageId, guildId: interaction.guildId }
);
}
if (!giveaway.isEnded && !giveaway.ended) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Giveaway still active: ${messageId}`,
ErrorTypes.VALIDATION,
- "This giveaway is still active. Please use `/gend` to end it first.",
+ "Dieses Giveaway läuft noch. Beende es zuerst mit `/giveaway end`.",
{ messageId, status: 'active' }
);
}
const participants = giveaway.participants || [];
- if (participants.length < giveaway.winnerCount) {
- throw new TitanBotError(
+ if (participants.length < giveaway.winnerCount!) {
+ throw new NexoBotError(
`Insufficient participants for reroll: ${participants.length} < ${giveaway.winnerCount}`,
ErrorTypes.VALIDATION,
- "Not enough entries to pick the required number of winners.",
+ "Nicht genug Teilnahmen, um die nötige Anzahl an Gewinnern zu ziehen.",
{ participantsCount: participants.length, winnersNeeded: giveaway.winnerCount }
);
}
const newWinners = selectWinners(
participants,
- giveaway.winnerCount,
+ giveaway.winnerCount!,
);
- const updatedGiveaway = {
+ const updatedGiveaway: Giveaway = {
...giveaway,
winnerIds: newWinners,
rerolledAt: new Date().toISOString(),
@@ -104,7 +106,7 @@ export default {
};
const channel = await interaction.client.channels.fetch(
- giveaway.channelId,
+ giveaway.channelId!,
).catch(err => {
logger.warn(`Could not fetch channel ${giveaway.channelId}:`, err.message);
return null;
@@ -123,8 +125,8 @@ export default {
return InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- "Reroll Complete",
- "The new winners have been selected and saved to the database. Could not find channel to announce.",
+ "Neu gezogen",
+ "Die neuen Gewinner wurden gezogen und gespeichert. Der Kanal für die Ankündigung wurde nicht gefunden.",
),
],
flags: MessageFlags.Ephemeral,
@@ -151,15 +153,15 @@ export default {
.join(",");
const existingPingMsg = giveaway.winnerPingMessageId
- ? await channel.messages.fetch(giveaway.winnerPingMessageId).catch(() => null)
+ ? await channel.messages.fetch(giveaway.winnerPingMessageId as string).catch(() => null)
: null;
if (existingPingMsg) {
await existingPingMsg.edit({
- content: `🔄 **GIVEAWAY REROLL** 🔄 New winners for **${giveaway.prize}**: ${winnerMentions}!`,
+ content: `🔄 **GIVEAWAY NEU GEZOGEN** 🔄 Neue Gewinner für **${giveaway.prize}**: ${winnerMentions}!`,
});
} else {
- const newPingMsg = await channel.send({
- content: `🔄 **GIVEAWAY REROLL** 🔄 New winners for **${giveaway.prize}**: ${winnerMentions}!`,
+ const newPingMsg = await (channel as SendableChannels).send({
+ content: `🔄 **GIVEAWAY NEU GEZOGEN** 🔄 Neue Gewinner für **${giveaway.prize}**: ${winnerMentions}!`,
});
updatedGiveaway.winnerPingMessageId = newPingMsg.id;
}
@@ -172,22 +174,22 @@ export default {
guildId: interaction.guildId,
eventType: EVENT_TYPES.GIVEAWAY_REROLL,
data: {
- description: `Giveaway rerolled: ${giveaway.prize}`,
+ description: `Giveaway neu gezogen: ${giveaway.prize}`,
channelId: giveaway.channelId,
userId: interaction.user.id,
fields: [
{
- name: 'Prize',
- value: giveaway.prize || 'Mystery Prize!',
+ name: 'Preis',
+ value: giveaway.prize || 'Überraschungspreis!',
inline: true
},
{
- name: 'New Winners',
+ name: 'Neue Gewinner',
value: winnerMentions,
inline: false
},
{
- name: 'Total Entries',
+ name: 'Teilnahmen gesamt',
value: participants.length.toString(),
inline: true
}
@@ -201,8 +203,8 @@ export default {
return InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- "Reroll Complete",
- `The new winners have been announced in ${channel}. (Original message not found).`,
+ "Neu gezogen",
+ `Die neuen Gewinner wurden in ${channel} bekannt gegeben. (Ursprüngliche Nachricht nicht gefunden.)`,
),
],
flags: MessageFlags.Ephemeral,
@@ -219,7 +221,7 @@ export default {
const newRow = createGiveawayButtons(true);
await message.edit({
- content: "🔄 **GIVEAWAY REROLLED** 🔄",
+ content: "🔄 **GIVEAWAY NEU GEZOGEN** 🔄",
embeds: [newEmbed],
components: [newRow],
});
@@ -229,15 +231,15 @@ export default {
.join(",");
const existingPingMsg = giveaway.winnerPingMessageId
- ? await channel.messages.fetch(giveaway.winnerPingMessageId).catch(() => null)
+ ? await channel.messages.fetch(giveaway.winnerPingMessageId as string).catch(() => null)
: null;
if (existingPingMsg) {
await existingPingMsg.edit({
- content: `🔄 **REROLL WINNERS** 🔄 CONGRATULATIONS ${winnerMentions}! You are the new winner(s) for the **${giveaway.prize}** giveaway! Please contact the host <@${giveaway.hostId}> to claim your prize.`,
+ content: `🔄 **NEUE GEWINNER** 🔄 HERZLICHEN GLÜCKWUNSCH ${winnerMentions}! Du hast das Giveaway **${giveaway.prize}** gewonnen! Melde dich beim Host <@${giveaway.hostId}>, um deinen Preis abzuholen.`,
});
} else {
- const newPingMsg = await channel.send({
- content: `🔄 **REROLL WINNERS** 🔄 CONGRATULATIONS ${winnerMentions}! You are the new winner(s) for the **${giveaway.prize}** giveaway! Please contact the host <@${giveaway.hostId}> to claim your prize.`,
+ const newPingMsg = await (channel as SendableChannels).send({
+ content: `🔄 **NEUE GEWINNER** 🔄 HERZLICHEN GLÜCKWUNSCH ${winnerMentions}! Du hast das Giveaway **${giveaway.prize}** gewonnen! Melde dich beim Host <@${giveaway.hostId}>, um deinen Preis abzuholen.`,
});
updatedGiveaway.winnerPingMessageId = newPingMsg.id;
}
@@ -250,22 +252,22 @@ export default {
guildId: interaction.guildId,
eventType: EVENT_TYPES.GIVEAWAY_REROLL,
data: {
- description: `Giveaway rerolled: ${giveaway.prize}`,
+ description: `Giveaway neu gezogen: ${giveaway.prize}`,
channelId: giveaway.channelId,
userId: interaction.user.id,
fields: [
{
- name: 'Prize',
- value: giveaway.prize || 'Mystery Prize!',
+ name: 'Preis',
+ value: giveaway.prize || 'Überraschungspreis!',
inline: true
},
{
- name: 'New Winners',
+ name: 'Neue Gewinner',
value: winnerMentions,
inline: false
},
{
- name: 'Total Entries',
+ name: 'Teilnahmen gesamt',
value: participants.length.toString(),
inline: true
}
@@ -279,11 +281,11 @@ export default {
return InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- "Reroll Successful ✅",
- `Successfully rerolled the giveaway for **${giveaway.prize}** in ${channel}. Selected ${newWinners.length} new winner(s).`,
+ "Erfolgreich neu gezogen ✅",
+ `Für das Giveaway **${giveaway.prize}** in ${channel} wurden ${newWinners.length} neue Gewinner gezogen.`,
),
],
flags: MessageFlags.Ephemeral,
});
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/JoinToCreate/jointocreate.js b/src/commands/JoinToCreate/jointocreate.ts
similarity index 58%
rename from src/commands/JoinToCreate/jointocreate.js
rename to src/commands/JoinToCreate/jointocreate.ts
index 593212f856..edcc872e9e 100644
--- a/src/commands/JoinToCreate/jointocreate.js
+++ b/src/commands/JoinToCreate/jointocreate.ts
@@ -2,7 +2,7 @@ import { getColor } from '../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, EmbedBuilder, LabelBuilder } from 'discord.js';
import { successEmbed, warningEmbed } from '../../utils/embeds.js';
import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import {
initializeJoinToCreate,
@@ -13,59 +13,67 @@ import {
logConfigurationChange,
getConfiguration
} from '../../services/joinToCreateService.js';
+import type { ButtonInteraction, CategoryChannel, GuildBasedChannel, ChatInputCommandInteraction, GuildMember, VoiceChannel } from 'discord.js';
+import type { Command, NexoClient } from '../../types/index.js';
+import type { JoinToCreateChannelOptions } from '../../utils/database.js';
+
+// getChannelConfiguration infers `channelConfig` as `StoredChannelOptions | {}`; widen it to the stored option shape.
+type ChannelConfiguration = Omit>, 'channelConfig'> & {
+ channelConfig: Partial;
+};
export default {
data: new SlashCommandBuilder()
.setName("jointocreate")
- .setDescription("Manage Join to Create voice channels system.")
+ .setDescription("Join-to-Create-Sprachkanäle verwalten.")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.setDMPermission(false)
.addSubcommand((subcommand) =>
subcommand
.setName("setup")
- .setDescription("Set up a new Join to Create voice channel.")
+ .setDescription("Neuen Join-to-Create-Sprachkanal einrichten.")
.addChannelOption((option) =>
option
.setName("category")
- .setDescription("Category to create the channel in.")
+ .setDescription("Kategorie, in der der Kanal erstellt wird.")
.addChannelTypes(ChannelType.GuildCategory)
)
.addStringOption((option) =>
option
.setName("channel_name")
- .setDescription("Select a template for naming temporary voice channels.")
+ .setDescription("Vorlage für die Namen temporärer Sprachkanäle.")
.addChoices(
- { name: "{username}'s Room (Default)", value: "{username}'s Room" },
- { name: "{username}'s Channel", value: "{username}'s Channel" },
- { name: "{username}'s Lounge", value: "{username}'s Lounge" },
- { name: "{username}'s Space", value: "{username}'s Space" },
- { name: "{displayName}'s Room", value: "{displayName}'s Room" },
- { name: "{username}'s VC", value: "{username}'s VC" },
- { name: "{username}'s Music Room", value: "{username}'s Music Room" },
- { name: "{username}'s Gaming Room", value: "{username}'s Gaming Room" },
- { name: "{username}'s Chat Room", value: "{username}'s Chat Room" },
- { name: "{username}'s Private Room", value: "{username}'s Private Room" }
+ { name: "Raum von {username}", value: "Raum von {username}" },
+ { name: "Kanal von {username}", value: "Kanal von {username}" },
+ { name: "Lounge von {username}", value: "Lounge von {username}" },
+ { name: "Bereich von {username}", value: "Bereich von {username}" },
+ { name: "Raum von {displayName}", value: "Raum von {displayName}" },
+ { name: "VC von {username}", value: "VC von {username}" },
+ { name: "Musikraum von {username}", value: "Musikraum von {username}" },
+ { name: "Gaming-Raum von {username}", value: "Gaming-Raum von {username}" },
+ { name: "Chatraum von {username}", value: "Chatraum von {username}" },
+ { name: "Privater Raum von {username}", value: "Privater Raum von {username}" }
)
)
.addIntegerOption((option) =>
option
.setName("user_limit")
- .setDescription("Maximum number of users in temporary channels. (0 = unlimited)")
+ .setDescription("Maximale Nutzerzahl in temporären Kanälen (0 = unbegrenzt).")
)
.addIntegerOption((option) =>
option
.setName("bitrate")
- .setDescription("Bitrate for temporary channels in kbps (8-96).")
+ .setDescription("Bitrate temporärer Kanäle in kbps (8-96).")
)
)
.addSubcommand((subcommand) =>
subcommand
.setName("dashboard")
- .setDescription("Configure an existing Join to Create system.")
+ .setDescription("Bestehendes Join-to-Create-System konfigurieren.")
.addChannelOption((option) =>
option
.setName("trigger_channel")
- .setDescription("The Join to Create trigger channel to configure.")
+ .setDescription("Der Join-to-Create-Trigger-Kanal, der konfiguriert werden soll.")
.setRequired(true)
.addChannelTypes(ChannelType.GuildVoice)
)
@@ -75,18 +83,18 @@ export default {
async execute(interaction, config, client) {
try {
- if (!hasManageGuildPermission(interaction.member)) {
- throw new TitanBotError(
+ if (!hasManageGuildPermission(interaction.member as GuildMember)) {
+ throw new NexoBotError(
'User lacks ManageGuild permission',
ErrorTypes.PERMISSION,
- 'You need **Manage Server** permission to use this command.'
+ 'Du brauchst die Berechtigung **Server verwalten**, um diesen Command zu nutzen.'
);
}
const subcommand = interaction.options.getSubcommand();
await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
- let responseEmbed;
+ let responseEmbed: unknown;
if (subcommand === "setup") {
await handleSetupSubcommand(interaction, client);
@@ -98,14 +106,14 @@ export default {
} catch (error) {
try {
- let errorMessage = 'An error occurred while executing this command.';
+ let errorMessage = 'Beim Ausführen dieses Commands ist ein Fehler aufgetreten.';
- if (error instanceof TitanBotError) {
- errorMessage = error.userMessage || 'An error occurred. Please try again.';
- logger.debug(`TitanBotError [${error.type}]: ${error.message}`, error.context || {});
+ if (error instanceof NexoBotError) {
+ errorMessage = error.userMessage || 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.';
+ logger.debug(`NexoBotError [${error.type}]: ${(error as Error).message}`, error.context || {});
} else {
logger.error('Unexpected error in jointocreate command:', error);
- errorMessage = 'An unexpected error occurred. Please try again or contact support.';
+ errorMessage = 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut oder wende dich an den Support.';
}
return replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: errorMessage });
@@ -114,26 +122,26 @@ export default {
}
}
}
-};
+} satisfies Command;
-async function handleSetupSubcommand(interaction, client) {
+async function handleSetupSubcommand(interaction: ChatInputCommandInteraction, client: NexoClient) {
try {
- const category = interaction.options.getChannel('category');
- const nameTemplate = interaction.options.getString('channel_name') || "{username}'s Room";
+ const category = interaction.options.getChannel('category') as CategoryChannel | null;
+ const nameTemplate = interaction.options.getString('channel_name') || "Raum von {username}";
const userLimit = interaction.options.getInteger('user_limit') || 0;
const bitrate = interaction.options.getInteger('bitrate') || 64;
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
logger.debug(`Setting up Join to Create in guild ${guildId} with template: ${nameTemplate}`);
const existingConfig = await getConfiguration(client, guildId);
if (Array.isArray(existingConfig.triggerChannels) && existingConfig.triggerChannels.length > 0) {
- const activeTriggerChannels = [];
- const staleTriggerChannelIds = [];
+ const activeTriggerChannels: GuildBasedChannel[] = [];
+ const staleTriggerChannelIds: string[] = [];
for (const existingChannelId of existingConfig.triggerChannels) {
- const existingChannel = await interaction.guild.channels.fetch(existingChannelId).catch(() => null);
+ const existingChannel = await interaction.guild!.channels.fetch(existingChannelId).catch(() => null);
if (existingChannel) {
activeTriggerChannels.push(existingChannel);
} else {
@@ -150,9 +158,9 @@ async function handleSetupSubcommand(interaction, client) {
if (activeTriggerChannels.length > 0) {
const primaryTrigger = activeTriggerChannels[0];
- const errorMessage = `This server already has a Join to Create channel set up: ${primaryTrigger}\n\nUse \`/jointocreate dashboard\` to modify it, or remove it first before creating a new one.`;
+ const errorMessage = `Dieser Server hat bereits einen Join-to-Create-Kanal: ${primaryTrigger}\n\nNutze \`/jointocreate dashboard\`, um ihn zu bearbeiten, oder entferne ihn zuerst, bevor du einen neuen erstellst.`;
- throw new TitanBotError(
+ throw new NexoBotError(
'Guild already has a Join to Create channel',
ErrorTypes.VALIDATION,
errorMessage,
@@ -167,15 +175,15 @@ async function handleSetupSubcommand(interaction, client) {
}
logger.debug('Creating Join to Create trigger channel...');
- let triggerChannel = await interaction.guild.channels.create({
- name: 'Join to Create',
+ let triggerChannel = await interaction.guild!.channels.create({
+ name: 'Join-to-Create',
type: ChannelType.GuildVoice,
parent: category?.id,
userLimit: 0,
bitrate: 64000,
permissionOverwrites: [
{
- id: interaction.guild.id,
+ id: interaction.guild!.id,
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.Connect],
},
],
@@ -190,7 +198,7 @@ async function handleSetupSubcommand(interaction, client) {
categoryId: category?.id
});
- await logConfigurationChange(client, guildId, interaction.user.id, 'Initialized Join to Create', {
+ await logConfigurationChange(client, guildId, interaction.user.id, 'Join-to-Create eingerichtet', {
channelId: triggerChannel.id,
nameTemplate,
userLimit,
@@ -200,51 +208,51 @@ async function handleSetupSubcommand(interaction, client) {
logger.info(`Successfully created Join to Create system in guild ${guildId}`);
const responseEmbed = successEmbed(
- '✅ Setup Complete',
- `Created Join to Create channel: ${triggerChannel}\n\n` +
- `**Settings:**\n` +
- `• Template: \`${nameTemplate}\`\n` +
- `• User Limit: ${userLimit === 0 ? 'Unlimited' : userLimit + ' users'}\n` +
+ '✅ Einrichtung abgeschlossen',
+ `Join-to-Create-Kanal erstellt: ${triggerChannel}\n\n` +
+ `**Einstellungen:**\n` +
+ `• Vorlage: \`${nameTemplate}\`\n` +
+ `• Nutzerlimit: ${userLimit === 0 ? 'Unbegrenzt' : userLimit + ' Nutzer'}\n` +
`• Bitrate: ${bitrate} kbps\n` +
- `${category ?`• Category: ${category.name}`: '• Category: Root level'}`
+ `${category ?`• Kategorie: ${category.name}`: '• Kategorie: Oberste Ebene'}`
);
return await InteractionHelper.safeEditReply(interaction, { embeds: [responseEmbed] });
} catch (error) {
logger.error('Error in handleSetupSubcommand:', error);
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
throw error;
}
- throw new TitanBotError(
- `Setup failed: ${error.message}`,
+ throw new NexoBotError(
+ `Setup failed: ${(error as Error).message}`,
ErrorTypes.DISCORD_API,
- 'Failed to set up Join to Create system. Please check bot permissions.'
+ 'Join-to-Create konnte nicht eingerichtet werden. Bitte prüfe die Berechtigungen des Bots.'
);
}
}
-async function handleConfigSubcommand(interaction, client) {
+async function handleConfigSubcommand(interaction: ChatInputCommandInteraction, client: NexoClient) {
try {
- const triggerChannel = interaction.options.getChannel('trigger_channel');
- const guildId = interaction.guild.id;
+ const triggerChannel = interaction.options.getChannel('trigger_channel') as VoiceChannel;
+ const guildId = interaction.guild!.id;
- const currentConfig = await getChannelConfiguration(client, guildId, triggerChannel.id);
+ const currentConfig: ChannelConfiguration = await getChannelConfiguration(client, guildId, triggerChannel.id);
const channelConfig = currentConfig.channelConfig || {};
const configEmbed = new EmbedBuilder()
- .setTitle('Join to Create Configuration')
- .setDescription(`Configuration for ${triggerChannel}`)
+ .setTitle('Join-to-Create-Konfiguration')
+ .setDescription(`Konfiguration für ${triggerChannel}`)
.setColor(getColor('info'))
.addFields(
{
- name: 'Channel Name Template',
- value: `\`${channelConfig.nameTemplate || currentConfig.channelNameTemplate || "{username}'s Room"}\``,
+ name: 'Kanalnamen-Vorlage',
+ value: `\`${channelConfig.nameTemplate || currentConfig.channelNameTemplate || "Raum von {username}"}\``,
inline: false
},
{
- name: 'User Limit',
- value: `${(channelConfig.userLimit ?? currentConfig.userLimit ?? 0) === 0 ? 'Unlimited' : (channelConfig.userLimit ?? currentConfig.userLimit ?? 0) + ' users'}`,
+ name: 'Nutzerlimit',
+ value: `${(channelConfig.userLimit ?? currentConfig.userLimit ?? 0) === 0 ? 'Unbegrenzt' : (channelConfig.userLimit ?? currentConfig.userLimit ?? 0) + ' Nutzer'}`,
inline: true
},
{
@@ -253,17 +261,17 @@ async function handleConfigSubcommand(interaction, client) {
inline: true
}
)
- .setFooter({ text: 'Use the buttons below to modify settings • Only one trigger channel is supported per guild' })
+ .setFooter({ text: 'Nutze die Buttons unten, um Einstellungen zu ändern • Nur ein Trigger-Kanal pro Server möglich' })
.setTimestamp();
const nameButton = new ButtonBuilder()
.setCustomId(`jtc_config_name_${triggerChannel.id}`)
- .setLabel('📝 Name Template')
+ .setLabel('📝 Namensvorlage')
.setStyle(ButtonStyle.Primary);
const limitButton = new ButtonBuilder()
.setCustomId(`jtc_config_limit_${triggerChannel.id}`)
- .setLabel('👥 User Limit')
+ .setLabel('👥 Nutzerlimit')
.setStyle(ButtonStyle.Primary);
const bitrateButton = new ButtonBuilder()
@@ -273,10 +281,10 @@ async function handleConfigSubcommand(interaction, client) {
const deleteButton = new ButtonBuilder()
.setCustomId(`jtc_config_delete_${triggerChannel.id}`)
- .setLabel('🗑️ Remove Channel')
+ .setLabel('🗑️ Kanal entfernen')
.setStyle(ButtonStyle.Danger);
- const row = new ActionRowBuilder().addComponents(nameButton, limitButton, bitrateButton, deleteButton);
+ const row = new ActionRowBuilder().addComponents(nameButton, limitButton, bitrateButton, deleteButton);
await InteractionHelper.safeEditReply(interaction, {
embeds: [configEmbed],
@@ -286,10 +294,10 @@ async function handleConfigSubcommand(interaction, client) {
const message = await interaction.fetchReply();
if (!message || typeof message.createMessageComponentCollector !== 'function') {
- throw new TitanBotError(
+ throw new NexoBotError(
'Failed to fetch interaction reply for collector setup',
ErrorTypes.DISCORD_API,
- 'Failed to open configuration controls. Please run `/jointocreate dashboard` again.'
+ 'Die Einstellungen konnten nicht geöffnet werden. Bitte führe `/jointocreate dashboard` erneut aus.'
);
}
@@ -301,9 +309,9 @@ async function handleConfigSubcommand(interaction, client) {
collector.on('collect', async (buttonInteraction) => {
try {
- if (!hasManageGuildPermission(buttonInteraction.member)) {
+ if (!hasManageGuildPermission(buttonInteraction.member as GuildMember)) {
await buttonInteraction.reply({
- content: '❌ You need **Manage Server** permission to use these controls.',
+ content: '❌ Du brauchst die Berechtigung **Server verwalten**, um diese Steuerung zu nutzen.',
flags: MessageFlags.Ephemeral
});
return;
@@ -321,12 +329,12 @@ async function handleConfigSubcommand(interaction, client) {
await handleChannelDeletion(buttonInteraction, triggerChannel, currentConfig, client);
}
} catch (error) {
- const userMessage = error instanceof TitanBotError
- ? error.userMessage || 'An error occurred.'
- : 'An error occurred while processing your request.';
+ const userMessage = error instanceof NexoBotError
+ ? error.userMessage || 'Ein Fehler ist aufgetreten.'
+ : 'Bei der Verarbeitung deiner Anfrage ist ein Fehler aufgetreten.';
- if (error instanceof TitanBotError) {
- logger.debug(`Button interaction validation error: ${error.message}`, error.context || {});
+ if (error instanceof NexoBotError) {
+ logger.debug(`Button interaction validation error: ${(error as Error).message}`, error.context || {});
} else {
logger.error('Unexpected error in config button interaction:', error);
}
@@ -339,7 +347,7 @@ async function handleConfigSubcommand(interaction, client) {
});
collector.on('end', () => {
- const disabledRow = new ActionRowBuilder().addComponents(
+ const disabledRow = new ActionRowBuilder().addComponents(
nameButton.setDisabled(true),
limitButton.setDisabled(true),
bitrateButton.setDisabled(true),
@@ -348,46 +356,51 @@ async function handleConfigSubcommand(interaction, client) {
message.edit({
components: [disabledRow],
- embeds: [configEmbed.setFooter({ text: 'Configuration session expired. Run the command again to make changes.' })]
+ embeds: [configEmbed.setFooter({ text: 'Sitzung abgelaufen. Führe den Command erneut aus, um Änderungen vorzunehmen.' })]
}).catch(() => {});
});
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
throw error;
}
- throw new TitanBotError(
- `Config failed: ${error.message}`,
+ throw new NexoBotError(
+ `Config failed: ${(error as Error).message}`,
ErrorTypes.DATABASE,
- 'Failed to load configuration.'
+ 'Konfiguration konnte nicht geladen werden.'
);
}
}
-async function handleNameTemplateModal(interaction, triggerChannel, currentConfig, client) {
+async function handleNameTemplateModal(interaction: ButtonInteraction, triggerChannel: VoiceChannel, currentConfig: ChannelConfiguration, client: NexoClient) {
try {
const TEMPLATE_OPTIONS = [
- { label: "{username}'s Room (Default)", value: "{username}'s Room" },
- { label: "{username}'s Channel", value: "{username}'s Channel" },
- { label: "{username}'s Lounge", value: "{username}'s Lounge" },
- { label: "{username}'s Space", value: "{username}'s Space" },
- { label: "{displayName}'s Room", value: "{displayName}'s Room" },
- { label: "{username}'s VC", value: "{username}'s VC" },
- { label: "{username}'s Music Room", value: "{username}'s Music Room" },
- { label: "{username}'s Gaming Room", value: "{username}'s Gaming Room" },
- { label: "{username}'s Chat Room", value: "{username}'s Chat Room" },
- { label: "{username}'s Private Room", value: "{username}'s Private Room" },
+ { label: "Raum von {username}", value: "Raum von {username}" },
+ { label: "Kanal von {username}", value: "Kanal von {username}" },
+ { label: "Lounge von {username}", value: "Lounge von {username}" },
+ { label: "Bereich von {username}", value: "Bereich von {username}" },
+ { label: "Raum von {displayName}", value: "Raum von {displayName}" },
+ { label: "VC von {username}", value: "VC von {username}" },
+ { label: "Musikraum von {username}", value: "Musikraum von {username}" },
+ { label: "Gaming-Raum von {username}", value: "Gaming-Raum von {username}" },
+ { label: "Chatraum von {username}", value: "Chatraum von {username}" },
+ { label: "Privater Raum von {username}", value: "Privater Raum von {username}" },
];
const currentTemplate = currentConfig.channelConfig?.nameTemplate
|| currentConfig.channelNameTemplate
- || "{username}'s Room";
+ || "Raum von {username}";
+
+ // Keep a stored template that is not in the list (e.g. older English ones) selectable and preselected.
+ const templateOptions = TEMPLATE_OPTIONS.some(o => o.value === currentTemplate)
+ ? TEMPLATE_OPTIONS
+ : [{ label: currentTemplate.slice(0, 100), value: currentTemplate.slice(0, 100) }, ...TEMPLATE_OPTIONS];
const templateSelect = new StringSelectMenuBuilder()
.setCustomId('template')
- .setPlaceholder('Pick a name template...')
+ .setPlaceholder('Wähle eine Namensvorlage...')
.setOptions(
- TEMPLATE_OPTIONS.map(o => ({
+ templateOptions.map(o => ({
label: o.label,
value: o.value,
default: o.value === currentTemplate,
@@ -395,12 +408,12 @@ async function handleNameTemplateModal(interaction, triggerChannel, currentConfi
);
const templateLabel = new LabelBuilder()
- .setLabel('Channel name template')
+ .setLabel('Kanalnamen-Vorlage')
.setStringSelectMenuComponent(templateSelect);
const modal = new ModalBuilder()
.setCustomId(`jtc_name_modal_${triggerChannel.id}`)
- .setTitle('Channel Name Template')
+ .setTitle('Kanalnamen-Vorlage')
.addLabelComponents(templateLabel);
await interaction.showModal(modal);
@@ -410,9 +423,9 @@ async function handleNameTemplateModal(interaction, triggerChannel, currentConfi
time: 60000
});
- if (!hasManageGuildPermission(modalSubmission.member)) {
+ if (!hasManageGuildPermission(modalSubmission.member as GuildMember)) {
await modalSubmission.reply({
- content: '❌ You need **Manage Server** permission to modify these settings.',
+ content: '❌ Du brauchst die Berechtigung **Server verwalten**, um diese Einstellungen zu ändern.',
flags: MessageFlags.Ephemeral
});
return;
@@ -420,49 +433,49 @@ async function handleNameTemplateModal(interaction, triggerChannel, currentConfi
const [newTemplate] = modalSubmission.fields.getStringSelectValues('template');
- await updateChannelConfig(client, interaction.guild.id, triggerChannel.id, {
+ await updateChannelConfig(client, interaction.guild!.id, triggerChannel.id, {
nameTemplate: newTemplate
});
- await logConfigurationChange(client, interaction.guild.id, interaction.user.id, 'Updated channel name template', {
+ await logConfigurationChange(client, interaction.guild!.id, interaction.user.id, 'Kanalnamen-Vorlage aktualisiert', {
channelId: triggerChannel.id,
newTemplate
});
await modalSubmission.reply({
- embeds: [successEmbed('Updated', `Channel name template changed to \`${newTemplate}\``)],
+ embeds: [successEmbed('Aktualisiert', `Kanalnamen-Vorlage geändert zu \`${newTemplate}\``)],
flags: MessageFlags.Ephemeral
});
} catch (error) {
- if (error.code === 'INTERACTION_COLLECTOR_ERROR') {
+ if ((error as { code?: unknown }).code === 'INTERACTION_COLLECTOR_ERROR') {
return;
}
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
throw error;
}
logger.error('Unexpected error in name template modal:', error);
- throw new TitanBotError(
- `Modal error: ${error.message}`,
+ throw new NexoBotError(
+ `Modal error: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'An error occurred while updating the template.'
+ 'Beim Aktualisieren der Vorlage ist ein Fehler aufgetreten.'
);
}
}
-async function handleUserLimitModal(interaction, triggerChannel, currentConfig, client) {
+async function handleUserLimitModal(interaction: ButtonInteraction, triggerChannel: VoiceChannel, currentConfig: ChannelConfiguration, client: NexoClient) {
try {
const currentLimit = currentConfig.channelConfig?.userLimit ?? currentConfig.userLimit ?? 0;
const modal = new ModalBuilder()
.setCustomId(`jtc_limit_modal_${triggerChannel.id}`)
- .setTitle('Configure User Limit')
+ .setTitle('Nutzerlimit festlegen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('user_limit')
- .setLabel('Enter user limit (0-99, 0 = unlimited)')
- .setPlaceholder('Enter a number between 0 and 99')
+ .setLabel('Nutzerlimit (0-99, 0 = unbegrenzt)')
+ .setPlaceholder('Gib eine Zahl zwischen 0 und 99 ein')
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setMinLength(1)
@@ -478,9 +491,9 @@ async function handleUserLimitModal(interaction, triggerChannel, currentConfig,
time: 60000
});
- if (!hasManageGuildPermission(modalSubmission.member)) {
+ if (!hasManageGuildPermission(modalSubmission.member as GuildMember)) {
await modalSubmission.reply({
- content: '❌ You need **Manage Server** permission to modify these settings.',
+ content: '❌ Du brauchst die Berechtigung **Server verwalten**, um diese Einstellungen zu ändern.',
flags: MessageFlags.Ephemeral
});
return;
@@ -488,49 +501,49 @@ async function handleUserLimitModal(interaction, triggerChannel, currentConfig,
const userInput = modalSubmission.fields.getTextInputValue('user_limit').trim();
- await updateChannelConfig(client, interaction.guild.id, triggerChannel.id, {
+ await updateChannelConfig(client, interaction.guild!.id, triggerChannel.id, {
userLimit: parseInt(userInput)
});
- await logConfigurationChange(client, interaction.guild.id, interaction.user.id, 'Updated user limit', {
+ await logConfigurationChange(client, interaction.guild!.id, interaction.user.id, 'Nutzerlimit aktualisiert', {
channelId: triggerChannel.id,
userLimit: parseInt(userInput)
});
await modalSubmission.reply({
- embeds: [successEmbed('Updated', `User limit changed to ${parseInt(userInput) === 0 ? 'Unlimited' : parseInt(userInput) + ' users'}`)],
+ embeds: [successEmbed('Aktualisiert', `Nutzerlimit geändert zu ${parseInt(userInput) === 0 ? 'Unbegrenzt' : parseInt(userInput) + ' Nutzer'}`)],
flags: MessageFlags.Ephemeral
});
} catch (error) {
- if (error.code === 'INTERACTION_COLLECTOR_ERROR') {
+ if ((error as { code?: unknown }).code === 'INTERACTION_COLLECTOR_ERROR') {
return;
}
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
throw error;
}
logger.error('Unexpected error in user limit modal:', error);
- throw new TitanBotError(
- `Modal error: ${error.message}`,
+ throw new NexoBotError(
+ `Modal error: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'An error occurred while updating the user limit.'
+ 'Beim Aktualisieren des Nutzerlimits ist ein Fehler aufgetreten.'
);
}
}
-async function handleBitrateModal(interaction, triggerChannel, currentConfig, client) {
+async function handleBitrateModal(interaction: ButtonInteraction, triggerChannel: VoiceChannel, currentConfig: ChannelConfiguration, client: NexoClient) {
try {
const currentBitrate = ((currentConfig.channelConfig.bitrate ?? currentConfig.bitrate ?? 64000) / 1000);
const modal = new ModalBuilder()
.setCustomId(`jtc_bitrate_modal_${triggerChannel.id}`)
- .setTitle('Configure Bitrate')
+ .setTitle('Bitrate festlegen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('bitrate')
- .setLabel('Enter bitrate in kbps (8-384)')
- .setPlaceholder('Enter a number between 8 and 384')
+ .setLabel('Bitrate in kbps (8-384)')
+ .setPlaceholder('Gib eine Zahl zwischen 8 und 384 ein')
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setMinLength(1)
@@ -546,9 +559,9 @@ async function handleBitrateModal(interaction, triggerChannel, currentConfig, cl
time: 60000
});
- if (!hasManageGuildPermission(modalSubmission.member)) {
+ if (!hasManageGuildPermission(modalSubmission.member as GuildMember)) {
await modalSubmission.reply({
- content: '❌ You need **Manage Server** permission to modify these settings.',
+ content: '❌ Du brauchst die Berechtigung **Server verwalten**, um diese Einstellungen zu ändern.',
flags: MessageFlags.Ephemeral
});
return;
@@ -556,51 +569,51 @@ async function handleBitrateModal(interaction, triggerChannel, currentConfig, cl
const userInput = modalSubmission.fields.getTextInputValue('bitrate').trim();
- await updateChannelConfig(client, interaction.guild.id, triggerChannel.id, {
+ await updateChannelConfig(client, interaction.guild!.id, triggerChannel.id, {
bitrate: parseInt(userInput) * 1000
});
- await logConfigurationChange(client, interaction.guild.id, interaction.user.id, 'Updated bitrate', {
+ await logConfigurationChange(client, interaction.guild!.id, interaction.user.id, 'Bitrate aktualisiert', {
channelId: triggerChannel.id,
bitrate: parseInt(userInput)
});
await modalSubmission.reply({
- embeds: [successEmbed('Updated', `Bitrate changed to ${parseInt(userInput)} kbps`)],
+ embeds: [successEmbed('Aktualisiert', `Bitrate geändert zu ${parseInt(userInput)} kbps`)],
flags: MessageFlags.Ephemeral
});
} catch (error) {
- if (error.code === 'INTERACTION_COLLECTOR_ERROR') {
+ if ((error as { code?: unknown }).code === 'INTERACTION_COLLECTOR_ERROR') {
return;
}
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
throw error;
}
logger.error('Unexpected error in bitrate modal:', error);
- throw new TitanBotError(
- `Modal error: ${error.message}`,
+ throw new NexoBotError(
+ `Modal error: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'An error occurred while updating the bitrate.'
+ 'Beim Aktualisieren der Bitrate ist ein Fehler aufgetreten.'
);
}
}
-async function handleChannelDeletion(interaction, triggerChannel, currentConfig, client) {
+async function handleChannelDeletion(interaction: ButtonInteraction, triggerChannel: VoiceChannel, currentConfig: ChannelConfiguration, client: NexoClient) {
try {
- const confirmRow = new ActionRowBuilder().addComponents(
+ const confirmRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`jtc_delete_confirm_${triggerChannel.id}`)
- .setLabel('🗑️ Yes, Delete')
+ .setLabel('🗑️ Ja, löschen')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`jtc_delete_cancel_${triggerChannel.id}`)
- .setLabel('❌ Cancel')
+ .setLabel('❌ Abbrechen')
.setStyle(ButtonStyle.Secondary)
);
await InteractionHelper.safeReply(interaction, {
- embeds: [warningEmbed('Confirm Deletion', `Are you sure you want to remove **${triggerChannel.name}** from the Join to Create system?\n\nThis action cannot be undone.`)],
+ embeds: [warningEmbed('Löschen bestätigen', `Willst du **${triggerChannel.name}** wirklich aus dem Join-to-Create-System entfernen?\n\nDas kann nicht rückgängig gemacht werden.`)],
components: [confirmRow],
flags: MessageFlags.Ephemeral
});
@@ -618,9 +631,9 @@ async function handleChannelDeletion(interaction, triggerChannel, currentConfig,
deleteCollector.on('collect', async (buttonInteraction) => {
try {
- if (!hasManageGuildPermission(buttonInteraction.member)) {
+ if (!hasManageGuildPermission(buttonInteraction.member as GuildMember)) {
await buttonInteraction.reply({
- content: '❌ You need **Manage Server** permission to remove channels.',
+ content: '❌ Du brauchst die Berechtigung **Server verwalten**, um Kanäle zu entfernen.',
flags: MessageFlags.Ephemeral
});
return;
@@ -628,37 +641,37 @@ async function handleChannelDeletion(interaction, triggerChannel, currentConfig,
if (buttonInteraction.customId === `jtc_delete_confirm_${triggerChannel.id}`) {
- await removeTriggerChannel(client, interaction.guild.id, triggerChannel.id);
+ await removeTriggerChannel(client, interaction.guild!.id, triggerChannel.id);
- await logConfigurationChange(client, interaction.guild.id, interaction.user.id, 'Removed Join to Create trigger', {
+ await logConfigurationChange(client, interaction.guild!.id, interaction.user.id, 'Join-to-Create-Trigger entfernt', {
channelId: triggerChannel.id,
channelName: triggerChannel.name
});
try {
if (triggerChannel.members.size === 0) {
- await triggerChannel.delete('Join to Create trigger removed by administrator');
+ await triggerChannel.delete('Join-to-Create-Trigger von einem Administrator entfernt');
}
} catch (deleteError) {
- logger.warn(`Could not delete channel ${triggerChannel.id}: ${deleteError.message}`);
+ logger.warn(`Could not delete channel ${triggerChannel.id}: ${(deleteError as Error).message}`);
}
await buttonInteraction.update({
- embeds: [successEmbed('Removed', `**${triggerChannel.name}** has been removed from the Join to Create system.`)],
+ embeds: [successEmbed('Entfernt', `**${triggerChannel.name}** wurde aus dem Join-to-Create-System entfernt.`)],
components: []
});
} else {
await buttonInteraction.update({
- embeds: [successEmbed('Cancelled', 'Channel removal has been cancelled.')],
+ embeds: [successEmbed('Abgebrochen', 'Das Entfernen des Kanals wurde abgebrochen.')],
components: []
});
}
} catch (collectError) {
logger.error('Error handling delete confirmation:', collectError);
await buttonInteraction.reply({
- content: '❌ An error occurred while processing your request.',
+ content: '❌ Bei der Verarbeitung deiner Anfrage ist ein Fehler aufgetreten.',
flags: MessageFlags.Ephemeral
}).catch(() => {});
}
@@ -671,14 +684,14 @@ async function handleChannelDeletion(interaction, triggerChannel, currentConfig,
});
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
throw error;
}
logger.error('Unexpected error in handleChannelDeletion:', error);
- throw new TitanBotError(
- `Deletion error: ${error.message}`,
+ throw new NexoBotError(
+ `Deletion error: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'An error occurred while removing the channel.'
+ 'Beim Entfernen des Kanals ist ein Fehler aufgetreten.'
);
}
}
\ No newline at end of file
diff --git a/src/commands/JoinToCreate/modules/config_setup.js b/src/commands/JoinToCreate/modules/config_setup.ts
similarity index 62%
rename from src/commands/JoinToCreate/modules/config_setup.js
rename to src/commands/JoinToCreate/modules/config_setup.ts
index 460d337f2f..a56e99d8e8 100644
--- a/src/commands/JoinToCreate/modules/config_setup.js
+++ b/src/commands/JoinToCreate/modules/config_setup.ts
@@ -13,81 +13,84 @@ import {
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { successEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
import {
getJoinToCreateConfig,
updateJoinToCreateConfig,
removeJoinToCreateTrigger,
addJoinToCreateTrigger
} from '../../../utils/database.js';
+import type { JoinToCreateChannelOptions, JoinToCreateConfig } from '../../../utils/database.js';
+import type { GuildTextBasedChannel, StringSelectMenuInteraction, VoiceChannel } from 'discord.js';
+import type { NexoClient, SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
try {
- const triggerChannel = interaction.options.getChannel('trigger_channel');
- const guildId = interaction.guild.id;
+ const triggerChannel = interaction.options.getChannel('trigger_channel') as VoiceChannel;
+ const guildId = interaction.guild!.id;
const currentConfig = await getJoinToCreateConfig(client, guildId);
if (!currentConfig.triggerChannels.includes(triggerChannel.id)) {
- throw new TitanBotError(
+ throw new NexoBotError(
`Channel ${triggerChannel.id} is not a Join to Create trigger`,
ErrorTypes.VALIDATION,
- `${triggerChannel} is not configured as a Join to Create trigger channel.`
+ `${triggerChannel} ist nicht als Join-to-Create-Trigger-Kanal eingerichtet.`
);
}
const embed = new EmbedBuilder()
- .setTitle('Join to Create Configuration')
- .setDescription(`Configure settings for ${triggerChannel}`)
+ .setTitle('Join-to-Create-Konfiguration')
+ .setDescription(`Einstellungen für ${triggerChannel} konfigurieren`)
.setColor(getColor('info'))
.addFields(
{
- name: 'Current Channel Name Template',
+ name: 'Aktuelle Kanalnamen-Vorlage',
value: `\`${currentConfig.channelOptions?.[triggerChannel.id]?.nameTemplate || currentConfig.channelNameTemplate}\``,
inline: false
},
{
- name: 'Current User Limit',
- value: `${currentConfig.channelOptions?.[triggerChannel.id]?.userLimit || currentConfig.userLimit === 0 ? 'No limit' : currentConfig.userLimit + ' users'}`,
+ name: 'Aktuelles Nutzerlimit',
+ value: `${currentConfig.channelOptions?.[triggerChannel.id]?.userLimit || currentConfig.userLimit === 0 ? 'Kein Limit' : currentConfig.userLimit + ' Nutzer'}`,
inline: true
},
{
- name: 'Current Bitrate',
+ name: 'Aktuelle Bitrate',
value: `${(currentConfig.channelOptions?.[triggerChannel.id]?.bitrate || currentConfig.bitrate) / 1000} kbps`,
inline: true
}
)
- .setFooter({ text: 'Select an option to configure below' })
+ .setFooter({ text: 'Wähle unten eine Option zum Konfigurieren' })
.setTimestamp();
const selectMenu = new StringSelectMenuBuilder()
.setCustomId(`jointocreate_config_${triggerChannel.id}`)
- .setPlaceholder('Select a configuration option')
+ .setPlaceholder('Wähle eine Einstellung')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Change Channel Name Template')
- .setDescription('Modify the template for temporary channel names')
+ .setLabel('Kanalnamen-Vorlage ändern')
+ .setDescription('Vorlage für temporäre Kanalnamen ändern')
.setValue('name_template'),
new StringSelectMenuOptionBuilder()
- .setLabel('Change User Limit')
- .setDescription('Set maximum users per temporary channel')
+ .setLabel('Nutzerlimit ändern')
+ .setDescription('Maximale Nutzerzahl pro temporärem Kanal festlegen')
.setValue('user_limit'),
new StringSelectMenuOptionBuilder()
- .setLabel('Change Bitrate')
- .setDescription('Adjust audio quality for temporary channels')
+ .setLabel('Bitrate ändern')
+ .setDescription('Audioqualität temporärer Kanäle anpassen')
.setValue('bitrate'),
new StringSelectMenuOptionBuilder()
- .setLabel('Remove This Trigger Channel')
- .setDescription('Remove this channel from the Join to Create system')
+ .setLabel('Diesen Trigger-Kanal entfernen')
+ .setDescription('Diesen Kanal aus dem Join-to-Create-System entfernen')
.setValue('remove_trigger'),
new StringSelectMenuOptionBuilder()
- .setLabel('View Current Settings')
- .setDescription('Show all current configuration details')
+ .setLabel('Aktuelle Einstellungen anzeigen')
+ .setDescription('Alle aktuellen Konfigurationsdetails anzeigen')
.setValue('view_settings')
);
- const row = new ActionRowBuilder().addComponents(selectMenu);
+ const row = new ActionRowBuilder().addComponents(selectMenu);
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed],
@@ -96,7 +99,7 @@ export default {
logger.error('Failed to edit reply in config_setup:', error);
});
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = (interaction.channel as GuildTextBasedChannel).createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: (i) => i.user.id === interaction.user.id && i.customId === `jointocreate_config_${triggerChannel.id}`,
time: 60000
@@ -126,15 +129,15 @@ time: 60000
break;
}
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
logger.debug(`Configuration validation error: ${error.message}`, error.context || {});
} else {
logger.error('Unexpected configuration menu error:', error);
}
- const errorMessage = error instanceof TitanBotError
- ? error.userMessage || 'An error occurred while processing your selection.'
- : 'An error occurred while processing your selection.';
+ const errorMessage = error instanceof NexoBotError
+ ? error.userMessage || 'Bei der Verarbeitung deiner Auswahl ist ein Fehler aufgetreten.'
+ : 'Bei der Verarbeitung deiner Auswahl ist ein Fehler aufgetreten.';
await replyUserError(selectInteraction, {
type: ErrorTypes.CONFIGURATION,
@@ -145,7 +148,7 @@ time: 60000
collector.on('end', async (collected, reason) => {
if (reason === 'time') {
- const disabledRow = new ActionRowBuilder().addComponents(
+ const disabledRow = new ActionRowBuilder().addComponents(
selectMenu.setDisabled(true)
);
@@ -155,41 +158,41 @@ time: 60000
}
});
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
throw error;
}
logger.error('Unexpected error in config_setup:', error);
- throw new TitanBotError(
- `Config setup failed: ${error.message}`,
+ throw new NexoBotError(
+ `Config setup failed: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'Failed to configure Join to Create system.'
+ 'Join-to-Create konnte nicht konfiguriert werden.'
);
}
}
-};
+} satisfies SubcommandModule;
-async function handleNameTemplateChange(interaction, triggerChannel, currentConfig, client) {
+async function handleNameTemplateChange(interaction: StringSelectMenuInteraction, triggerChannel: VoiceChannel, currentConfig: JoinToCreateConfig, client: NexoClient) {
const embed = new EmbedBuilder()
- .setTitle('Channel Name Template Configuration')
- .setDescription('Please enter the new channel name template.')
+ .setTitle('Kanalnamen-Vorlage konfigurieren')
+ .setDescription('Bitte gib die neue Kanalnamen-Vorlage ein.')
.addFields(
{
- name: 'Available Variables',
- value: '• `{username}` - User\'s username\n• `{display_name}` - User\'s display name\n• `{user_tag}` - User\'s tag (User#1234)\n• `{guild_name}` - Server name',
+ name: 'Verfügbare Variablen',
+ value: '• `{username}` - Nutzername\n• `{display_name}` - Anzeigename\n• `{user_tag}` - Tag des Nutzers (User#1234)\n• `{guild_name}` - Servername',
inline: false
},
{
- name: 'Current Template',
+ name: 'Aktuelle Vorlage',
value: `\`${currentConfig.channelOptions?.[triggerChannel.id]?.nameTemplate || currentConfig.channelNameTemplate}\``,
inline: false
}
)
.setColor(getColor('info'))
- .setFooter({ text: 'Type your new template in the chat below' });
+ .setFooter({ text: 'Schreib deine neue Vorlage unten in den Chat' });
await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral });
- const collector = interaction.channel.createMessageCollector({
+ const collector = (interaction.channel as GuildTextBasedChannel).createMessageCollector({
filter: (m) => m.author.id === interaction.user.id,
time: 600_000,
max: 1
@@ -202,7 +205,7 @@ time: 600_000,
if (!newTemplate || newTemplate.length > 100) {
await replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'Template must be between 1 and 100 characters.'
+ message: 'Die Vorlage muss zwischen 1 und 100 Zeichen lang sein.'
});
return;
}
@@ -213,26 +216,26 @@ time: 600_000,
nameTemplate: newTemplate
};
- await updateJoinToCreateConfig(client, interaction.guild.id, {
+ await updateJoinToCreateConfig(client, interaction.guild!.id, {
channelOptions: channelOptions
});
await interaction.followUp({
- embeds: [successEmbed('Template Updated', `Channel name template changed to \`${newTemplate}\``)],
+ embeds: [successEmbed('Vorlage aktualisiert', `Kanalnamen-Vorlage geändert zu \`${newTemplate}\``)],
flags: MessageFlags.Ephemeral,
});
await message.delete().catch(() => {});
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
logger.debug(`Template validation error: ${error.message}`);
} else {
logger.error('Template update error:', error);
}
- const errorMessage = error instanceof TitanBotError
- ? error.userMessage || 'Could not update the channel name template.'
- : 'Could not update the channel name template.';
+ const errorMessage = error instanceof NexoBotError
+ ? error.userMessage || 'Die Kanalnamen-Vorlage konnte nicht aktualisiert werden.'
+ : 'Die Kanalnamen-Vorlage konnte nicht aktualisiert werden.';
await replyUserError(interaction, {
type: ErrorTypes.CONFIGURATION,
@@ -245,29 +248,29 @@ time: 600_000,
if (reason === 'time') {
replyUserError(interaction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No response received. Template update cancelled.'
+ message: 'Keine Antwort erhalten. Aktualisierung der Vorlage abgebrochen.'
}).catch(() => {});
}
});
}
-async function handleUserLimitChange(interaction, triggerChannel, currentConfig, client) {
+async function handleUserLimitChange(interaction: StringSelectMenuInteraction, triggerChannel: VoiceChannel, currentConfig: JoinToCreateConfig, client: NexoClient) {
const embed = new EmbedBuilder()
- .setTitle('User Limit Configuration')
- .setDescription('Please enter the new user limit (0-99, where 0 = no limit).')
+ .setTitle('Nutzerlimit konfigurieren')
+ .setDescription('Bitte gib das neue Nutzerlimit ein (0-99, 0 = kein Limit).')
.addFields(
{
- name: 'Current Limit',
- value: `${currentConfig.channelOptions?.[triggerChannel.id]?.userLimit || currentConfig.userLimit === 0 ? 'No limit' : currentConfig.userLimit + ' users'}`,
+ name: 'Aktuelles Limit',
+ value: `${currentConfig.channelOptions?.[triggerChannel.id]?.userLimit || currentConfig.userLimit === 0 ? 'Kein Limit' : currentConfig.userLimit + ' Nutzer'}`,
inline: false
}
)
.setColor(getColor('info'))
- .setFooter({ text: 'Type the new limit in the chat below' });
+ .setFooter({ text: 'Schreib das neue Limit unten in den Chat' });
await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral });
- const collector = interaction.channel.createMessageCollector({
+ const collector = (interaction.channel as GuildTextBasedChannel).createMessageCollector({
filter: (m) => m.author.id === interaction.user.id && /^\d+$/.test(m.content.trim()),
time: 600_000,
max: 1
@@ -280,7 +283,7 @@ async function handleUserLimitChange(interaction, triggerChannel, currentConfig,
if (newLimit < 0 || newLimit > 99) {
await replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'User limit must be between 0 and 99.'
+ message: 'Das Nutzerlimit muss zwischen 0 und 99 liegen.'
});
return;
}
@@ -291,26 +294,26 @@ async function handleUserLimitChange(interaction, triggerChannel, currentConfig,
userLimit: newLimit
};
- await updateJoinToCreateConfig(client, interaction.guild.id, {
+ await updateJoinToCreateConfig(client, interaction.guild!.id, {
channelOptions: channelOptions
});
await interaction.followUp({
- embeds: [successEmbed('Limit Updated', `User limit changed to ${newLimit === 0 ? 'No limit' : newLimit + ' users'}`)],
+ embeds: [successEmbed('Limit aktualisiert', `Nutzerlimit geändert zu ${newLimit === 0 ? 'Kein Limit' : newLimit + ' Nutzer'}`)],
flags: MessageFlags.Ephemeral,
});
await message.delete().catch(() => {});
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
logger.debug(`User limit validation error: ${error.message}`);
} else {
logger.error('User limit update error:', error);
}
- const errorMessage = error instanceof TitanBotError
- ? error.userMessage || 'Could not update the user limit.'
- : 'Could not update the user limit.';
+ const errorMessage = error instanceof NexoBotError
+ ? error.userMessage || 'Das Nutzerlimit konnte nicht aktualisiert werden.'
+ : 'Das Nutzerlimit konnte nicht aktualisiert werden.';
await replyUserError(interaction, {
type: ErrorTypes.CONFIGURATION,
@@ -323,34 +326,34 @@ async function handleUserLimitChange(interaction, triggerChannel, currentConfig,
if (reason === 'time') {
replyUserError(interaction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No valid response received. Update cancelled.'
+ message: 'Keine gültige Antwort erhalten. Aktualisierung abgebrochen.'
}).catch(() => {});
}
});
}
-async function handleBitrateChange(interaction, triggerChannel, currentConfig, client) {
+async function handleBitrateChange(interaction: StringSelectMenuInteraction, triggerChannel: VoiceChannel, currentConfig: JoinToCreateConfig, client: NexoClient) {
const embed = new EmbedBuilder()
- .setTitle('Bitrate Configuration')
- .setDescription('Please enter the new bitrate in kbps (8-384).')
+ .setTitle('Bitrate konfigurieren')
+ .setDescription('Bitte gib die neue Bitrate in kbps ein (8-384).')
.addFields(
{
- name: 'Current Bitrate',
+ name: 'Aktuelle Bitrate',
value: `${(currentConfig.channelOptions?.[triggerChannel.id]?.bitrate || currentConfig.bitrate) / 1000} kbps`,
inline: false
},
{
- name: 'Common Values',
- value: '• 64 kbps - Normal quality\n• 96 kbps - Good quality\n• 128 kbps - High quality\n• 256 kbps - Very high quality',
+ name: 'Übliche Werte',
+ value: '• 64 kbps - Normale Qualität\n• 96 kbps - Gute Qualität\n• 128 kbps - Hohe Qualität\n• 256 kbps - Sehr hohe Qualität',
inline: false
}
)
.setColor(getColor('info'))
- .setFooter({ text: 'Type the new bitrate in the chat below' });
+ .setFooter({ text: 'Schreib die neue Bitrate unten in den Chat' });
await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral });
- const collector = interaction.channel.createMessageCollector({
+ const collector = (interaction.channel as GuildTextBasedChannel).createMessageCollector({
filter: (m) => m.author.id === interaction.user.id && /^\d+$/.test(m.content.trim()),
time: 600_000,
max: 1
@@ -363,7 +366,7 @@ async function handleBitrateChange(interaction, triggerChannel, currentConfig, c
if (newBitrate < 8 || newBitrate > 384) {
await replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'Bitrate must be between 8 and 384 kbps.'
+ message: 'Die Bitrate muss zwischen 8 und 384 kbps liegen.'
});
return;
}
@@ -374,26 +377,26 @@ async function handleBitrateChange(interaction, triggerChannel, currentConfig, c
bitrate: newBitrate * 1000
};
- await updateJoinToCreateConfig(client, interaction.guild.id, {
+ await updateJoinToCreateConfig(client, interaction.guild!.id, {
channelOptions: channelOptions
});
await interaction.followUp({
- embeds: [successEmbed('Bitrate Updated', `Bitrate changed to ${newBitrate} kbps`)],
+ embeds: [successEmbed('Bitrate aktualisiert', `Bitrate geändert zu ${newBitrate} kbps`)],
flags: MessageFlags.Ephemeral,
});
await message.delete().catch(() => {});
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
logger.debug(`Bitrate validation error: ${error.message}`);
} else {
logger.error('Bitrate update error:', error);
}
- const errorMessage = error instanceof TitanBotError
- ? error.userMessage || 'Could not update the bitrate.'
- : 'Could not update the bitrate.';
+ const errorMessage = error instanceof NexoBotError
+ ? error.userMessage || 'Die Bitrate konnte nicht aktualisiert werden.'
+ : 'Die Bitrate konnte nicht aktualisiert werden.';
await replyUserError(interaction, {
type: ErrorTypes.CONFIGURATION,
@@ -406,27 +409,27 @@ async function handleBitrateChange(interaction, triggerChannel, currentConfig, c
if (reason === 'time') {
replyUserError(interaction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No valid response received. Update cancelled.'
+ message: 'Keine gültige Antwort erhalten. Aktualisierung abgebrochen.'
}).catch(() => {});
}
});
}
-async function handleRemoveTrigger(interaction, triggerChannel, currentConfig, client) {
+async function handleRemoveTrigger(interaction: StringSelectMenuInteraction, triggerChannel: VoiceChannel, currentConfig: JoinToCreateConfig, client: NexoClient) {
const embed = new EmbedBuilder()
- .setTitle('Remove Trigger Channel')
- .setDescription(`Are you sure you want to remove ${triggerChannel} from the Join to Create system?`)
+ .setTitle('Trigger-Kanal entfernen')
+ .setDescription(`Willst du ${triggerChannel} wirklich aus dem Join-to-Create-System entfernen?`)
.setColor('#ff6600')
- .setFooter({ text: 'This action cannot be undone' });
+ .setFooter({ text: 'Das kann nicht rückgängig gemacht werden' });
- const row = new ActionRowBuilder().addComponents(
+ const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`confirm_remove_${triggerChannel.id}`)
- .setLabel('Remove Channel')
+ .setLabel('Kanal entfernen')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`cancel_remove_${triggerChannel.id}`)
- .setLabel('Cancel')
+ .setLabel('Abbrechen')
.setStyle(ButtonStyle.Secondary)
);
@@ -436,7 +439,7 @@ async function handleRemoveTrigger(interaction, triggerChannel, currentConfig, c
flags: MessageFlags.Ephemeral
});
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = (interaction.channel as GuildTextBasedChannel).createMessageComponentCollector({
componentType: ComponentType.Button,
filter: (i) => i.user.id === interaction.user.id &&
(i.customId === `confirm_remove_${triggerChannel.id}` || i.customId === `cancel_remove_${triggerChannel.id}`),
@@ -449,29 +452,29 @@ async function handleRemoveTrigger(interaction, triggerChannel, currentConfig, c
if (buttonInteraction.customId === `confirm_remove_${triggerChannel.id}`) {
try {
- const success = await removeJoinToCreateTrigger(client, interaction.guild.id, triggerChannel.id);
+ const success = await removeJoinToCreateTrigger(client, interaction.guild!.id, triggerChannel.id);
if (success) {
await buttonInteraction.followUp({
- embeds: [successEmbed('Channel Removed', `${triggerChannel} has been removed from the Join to Create system.`)],
+ embeds: [successEmbed('Kanal entfernt', `${triggerChannel} wurde aus dem Join-to-Create-System entfernt.`)],
flags: MessageFlags.Ephemeral,
});
} else {
await replyUserError(buttonInteraction, {
type: ErrorTypes.CONFIGURATION,
- message: 'Could not remove the trigger channel.'
+ message: 'Der Trigger-Kanal konnte nicht entfernt werden.'
});
}
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
logger.debug(`Trigger removal validation error: ${error.message}`);
} else {
logger.error('Remove trigger error:', error);
}
- const errorMessage = error instanceof TitanBotError
- ? error.userMessage || 'An error occurred while removing the trigger channel.'
- : 'An error occurred while removing the trigger channel.';
+ const errorMessage = error instanceof NexoBotError
+ ? error.userMessage || 'Beim Entfernen des Trigger-Kanals ist ein Fehler aufgetreten.'
+ : 'Beim Entfernen des Trigger-Kanals ist ein Fehler aufgetreten.';
await replyUserError(buttonInteraction, {
type: ErrorTypes.CONFIGURATION,
@@ -480,7 +483,7 @@ async function handleRemoveTrigger(interaction, triggerChannel, currentConfig, c
}
} else {
await buttonInteraction.followUp({
- embeds: [successEmbed('Cancelled', 'Channel removal has been cancelled.')],
+ embeds: [successEmbed('Abgebrochen', 'Das Entfernen des Kanals wurde abgebrochen.')],
flags: MessageFlags.Ephemeral,
});
}
@@ -490,33 +493,33 @@ async function handleRemoveTrigger(interaction, triggerChannel, currentConfig, c
if (reason === 'time') {
replyUserError(interaction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No response received. Removal cancelled.'
+ message: 'Keine Antwort erhalten. Entfernen abgebrochen.'
}).catch(() => {});
}
});
}
-async function handleViewSettings(interaction, triggerChannel, currentConfig, client) {
- const channelConfig = currentConfig.channelOptions?.[triggerChannel.id] || {};
+async function handleViewSettings(interaction: StringSelectMenuInteraction, triggerChannel: VoiceChannel, currentConfig: JoinToCreateConfig, client: NexoClient) {
+ const channelConfig: Partial = currentConfig.channelOptions?.[triggerChannel.id] || {};
const embed = new EmbedBuilder()
- .setTitle('Current Settings')
- .setDescription(`Configuration for ${triggerChannel}`)
+ .setTitle('Aktuelle Einstellungen')
+ .setDescription(`Konfiguration für ${triggerChannel}`)
.setColor(getColor('info'))
.addFields(
{
- name: 'Trigger Channel',
+ name: 'Trigger-Kanal',
value: `${triggerChannel} (${triggerChannel.id})`,
inline: false
},
{
- name: 'Channel Name Template',
+ name: 'Kanalnamen-Vorlage',
value: `\`${channelConfig.nameTemplate || currentConfig.channelNameTemplate}\``,
inline: false
},
{
- name: 'User Limit',
- value: `${channelConfig.userLimit || currentConfig.userLimit === 0 ? 'No limit' : (channelConfig.userLimit || currentConfig.userLimit) + ' users'}`,
+ name: 'Nutzerlimit',
+ value: `${channelConfig.userLimit || currentConfig.userLimit === 0 ? 'Kein Limit' : (channelConfig.userLimit || currentConfig.userLimit) + ' Nutzer'}`,
inline: true
},
{
@@ -525,17 +528,17 @@ async function handleViewSettings(interaction, triggerChannel, currentConfig, cl
inline: true
},
{
- name: 'Category',
- value: currentConfig.categoryId ? `<#${currentConfig.categoryId}>` : 'Not set',
+ name: 'Kategorie',
+ value: currentConfig.categoryId ? `<#${currentConfig.categoryId}>` : 'Nicht festgelegt',
inline: true
},
{
- name: 'System Status',
- value: currentConfig.enabled ? '✅ Enabled' : '❌ Disabled',
+ name: 'Systemstatus',
+ value: currentConfig.enabled ? '✅ Aktiviert' : '❌ Deaktiviert',
inline: true
},
{
- name: 'Active Temporary Channels',
+ name: 'Aktive temporäre Kanäle',
value: Object.keys(currentConfig.temporaryChannels || {}).length.toString(),
inline: true
}
diff --git a/src/commands/JoinToCreate/modules/setup.js b/src/commands/JoinToCreate/modules/setup.ts
similarity index 62%
rename from src/commands/JoinToCreate/modules/setup.js
rename to src/commands/JoinToCreate/modules/setup.ts
index 00ebd31053..d83b46ba59 100644
--- a/src/commands/JoinToCreate/modules/setup.js
+++ b/src/commands/JoinToCreate/modules/setup.ts
@@ -1,49 +1,53 @@
import { ChannelType, MessageFlags, PermissionFlagsBits } from 'discord.js';
import { successEmbed, errorEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
import { addJoinToCreateTrigger, getJoinToCreateConfig } from '../../../utils/database.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { CategoryChannel } from 'discord.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
- const category = interaction.options.getChannel('category');
- const nameTemplate = interaction.options.getString('channel_name') || "{username}'s Room";
+ const category = interaction.options.getChannel('category') as CategoryChannel | null;
+ const nameTemplate = interaction.options.getString('channel_name') || "Raum von {username}";
const userLimit = interaction.options.getInteger('user_limit') || 0;
const bitrate = interaction.options.getInteger('bitrate') || 64;
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
try {
- const triggerChannel = await interaction.guild.channels.create({
- name: 'Join to Create',
+ const triggerChannel = await interaction.guild!.channels.create({
+ name: 'Join-to-Create',
type: ChannelType.GuildVoice,
parent: category?.id,
userLimit: userLimit,
bitrate: bitrate * 1000,
permissionOverwrites: [
{
- id: interaction.guild.id,
+ id: interaction.guild!.id,
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.Connect],
},
],
});
- await addJoinToCreateTrigger(client, guildId, triggerChannel.id, {
+ // categoryId is not part of JoinToCreateChannelOptions (ignored by addJoinToCreateTrigger) but kept as in the JS.
+ const triggerOptions = {
nameTemplate: nameTemplate,
userLimit: userLimit,
bitrate: bitrate * 1000,
categoryId: category?.id
- });
+ };
+ await addJoinToCreateTrigger(client, guildId, triggerChannel.id, triggerOptions);
const embed = successEmbed(
- '✅ Join to Create Setup Complete',
- `Created trigger channel: ${triggerChannel}\n\n` +
- `**Settings:**\n` +
- `• Temporary Channel Name Template: \`${nameTemplate}\`\n` +
- `• User Limit: ${userLimit === 0 ? 'No limit' : userLimit + ' users'}\n` +
+ '✅ Join-to-Create eingerichtet',
+ `Trigger-Kanal erstellt: ${triggerChannel}\n\n` +
+ `**Einstellungen:**\n` +
+ `• Namensvorlage für temporäre Kanäle: \`${nameTemplate}\`\n` +
+ `• Nutzerlimit: ${userLimit === 0 ? 'Kein Limit' : userLimit + ' Nutzer'}\n` +
`• Bitrate: ${bitrate} kbps\n` +
- `${category ?`• Category: ${category.name}`: '• Category: None (root level)'}\n\n` +
- `When users join this channel, a temporary voice channel will be created for them.`
+ `${category ?`• Kategorie: ${category.name}`: '• Kategorie: Keine (oberste Ebene)'}\n\n` +
+ `Wenn Nutzer diesem Kanal beitreten, wird für sie ein temporärer Sprachkanal erstellt.`
);
try {
@@ -64,15 +68,15 @@ export default {
}
}
} catch (error) {
- if (error instanceof TitanBotError) {
+ if (error instanceof NexoBotError) {
throw error;
}
logger.error('Error in JoinToCreate setup:', error);
- throw new TitanBotError(
- `Setup failed: ${error.message}`,
+ throw new NexoBotError(
+ `Setup failed: ${(error as Error).message}`,
ErrorTypes.DISCORD_API,
- 'Failed to set up Join to Create system.'
+ 'Join-to-Create konnte nicht eingerichtet werden.'
);
}
}
-};
\ No newline at end of file
+} satisfies SubcommandModule;
\ No newline at end of file
diff --git a/src/commands/Leveling/level.ts b/src/commands/Leveling/level.ts
new file mode 100644
index 0000000000..e6542c5530
--- /dev/null
+++ b/src/commands/Leveling/level.ts
@@ -0,0 +1,19 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import levelCommand from './modules/level.js';
+import leveladdCommand from './modules/leveladd.js';
+import levelremoveCommand from './modules/levelremove.js';
+import levelsetCommand from './modules/levelset.js';
+
+export default createRouterCommand({
+ name: 'level',
+ description: 'Leveling konfigurieren und Level von Mitgliedern verwalten',
+ category: 'Leveling',
+ defaultMemberPermissions: PermissionFlagsBits.ManageGuild,
+ routes: [
+ { legacyName: 'level', module: levelCommand, spread: true },
+ { legacyName: 'leveladd', module: leveladdCommand },
+ { legacyName: 'levelremove', module: levelremoveCommand },
+ { legacyName: 'levelset', module: levelsetCommand },
+ ],
+});
diff --git a/src/commands/Leveling/leaderboard.js b/src/commands/Leveling/modules/leaderboard.ts
similarity index 62%
rename from src/commands/Leveling/leaderboard.js
rename to src/commands/Leveling/modules/leaderboard.ts
index 52169ae6cb..c1d5a38e79 100644
--- a/src/commands/Leveling/leaderboard.js
+++ b/src/commands/Leveling/modules/leaderboard.ts
@@ -1,53 +1,54 @@
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from 'discord.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { getLeaderboard, getLevelingConfig, getXpForLevel } from '../../services/leveling/leveling.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { getLeaderboard, getLevelingConfig, getXpForLevel } from '../../../services/leveling/leveling.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('leaderboard')
- .setDescription("Shows the server's level leaderboard")
+ .setDescription('Zeigt die Level-Bestenliste des Servers')
.setDMPermission(false),
category: 'Leveling',
async execute(interaction, config, client) {
await InteractionHelper.safeDefer(interaction);
- const levelingConfig = await getLevelingConfig(client, interaction.guildId);
+ const levelingConfig = await getLevelingConfig(client, interaction.guildId!);
if (!levelingConfig?.enabled) {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
new EmbedBuilder()
.setColor('#f1c40f')
- .setDescription('The leveling system is currently disabled on this server.')
+ .setDescription('Das Leveling-System ist auf diesem Server derzeit deaktiviert.')
],
flags: MessageFlags.Ephemeral
});
return;
}
- const leaderboard = await getLeaderboard(client, interaction.guildId, 10);
+ const leaderboard = await getLeaderboard(client, interaction.guildId!, 10);
if (leaderboard.length === 0) {
- throw new TitanBotError(
+ throw new NexoBotError(
'No leaderboard data found',
ErrorTypes.DATABASE,
- 'No level data found yet. Start chatting to gain XP!'
+ 'Noch keine Level-Daten vorhanden. Schreib Nachrichten, um XP zu sammeln!'
);
}
const embed = new EmbedBuilder()
- .setTitle('Level Leaderboard')
+ .setTitle('Level-Bestenliste')
.setColor('#2ecc71')
- .setDescription("Top 10 most active members in this server:")
+ .setDescription('Die 10 aktivsten Mitglieder auf diesem Server:')
.setTimestamp();
const leaderboardText = await Promise.all(
leaderboard.map(async (user, index) => {
try {
- const member = await interaction.guild.members.fetch(user.userId).catch(() => null);
+ const member = await interaction.guild!.members.fetch(user.userId).catch(() => null);
const userMention = member?.user.toString() || `<@${user.userId}>`;
const xpForNextLevel = getXpForLevel(user.level + 1);
@@ -57,19 +58,19 @@ export default {
else if (index === 2) rankPrefix = '🥉';
else rankPrefix = `**${index + 1}.**`;
- return `${rankPrefix} ${userMention} - Level ${user.level} (${user.xp}/${xpForNextLevel} XP)`;
+ return `${rankPrefix} ${userMention} – Level ${user.level} (${user.xp}/${xpForNextLevel} XP)`;
} catch {
- return `**${index + 1}.** Error loading user ${user.userId}`;
+ return `**${index + 1}.** Fehler beim Laden von Nutzer ${user.userId}`;
}
})
);
embed.addFields({
- name: 'Rankings',
+ name: 'Rangliste',
value: leaderboardText.join('\n')
});
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
logger.debug(`Leaderboard displayed for guild ${interaction.guildId}`);
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Leveling/level.js b/src/commands/Leveling/modules/level.ts
similarity index 61%
rename from src/commands/Leveling/level.js
rename to src/commands/Leveling/modules/level.ts
index ecfb5620e3..10283ce523 100644
--- a/src/commands/Leveling/level.js
+++ b/src/commands/Leveling/modules/level.ts
@@ -1,34 +1,36 @@
-import { getColor } from '../../config/bot.js';
+import { getColor } from '../../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, MessageFlags } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { getLevelingConfig, saveLevelingConfig } from '../../services/leveling/leveling.js';
-import { botHasPermission } from '../../utils/permissionGuard.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { logger } from '../../utils/logger.js';
-import levelDashboard from './modules/level_dashboard.js';
+import type { TextChannel } from 'discord.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { getLevelingConfig, saveLevelingConfig } from '../../../services/leveling/leveling.js';
+import { botHasPermission } from '../../../utils/permissionGuard.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { logger } from '../../../utils/logger.js';
+import levelDashboard from './level_dashboard.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('level')
- .setDescription('Manage the leveling system')
+ .setDescription('Das Leveling-System verwalten')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.setDMPermission(false)
.addSubcommand((subcommand) =>
subcommand
.setName('setup')
- .setDescription('Set up the leveling system — this also enables it')
+ .setDescription('Das Leveling-System einrichten – dabei wird es auch aktiviert')
.addChannelOption((option) =>
option
.setName('channel')
- .setDescription('Channel to send level-up notifications in')
+ .setDescription('Kanal für Level-up-Benachrichtigungen')
.addChannelTypes(ChannelType.GuildText)
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('xp_min')
- .setDescription('Minimum XP awarded per message (default: 15)')
+ .setDescription('Minimale XP pro Nachricht (Standard: 15)')
.setMinValue(1)
.setMaxValue(500)
.setRequired(false),
@@ -36,7 +38,7 @@ export default {
.addIntegerOption((option) =>
option
.setName('xp_max')
- .setDescription('Maximum XP awarded per message (default: 25)')
+ .setDescription('Maximale XP pro Nachricht (Standard: 25)')
.setMinValue(1)
.setMaxValue(500)
.setRequired(false),
@@ -45,7 +47,7 @@ export default {
option
.setName('message')
.setDescription(
- 'Level-up message. Use {user} and {level} as placeholders (default provided)',
+ 'Level-up-Nachricht. Platzhalter: {user} und {level} (Standard vorhanden)',
)
.setMaxLength(500)
.setRequired(false),
@@ -53,7 +55,7 @@ export default {
.addIntegerOption((option) =>
option
.setName('xp_cooldown')
- .setDescription('Seconds between XP grants per user (default: 60)')
+ .setDescription('Sekunden zwischen XP-Vergaben pro Nutzer (Standard: 60)')
.setMinValue(0)
.setMaxValue(3600)
.setRequired(false),
@@ -62,7 +64,7 @@ export default {
.addSubcommand((subcommand) =>
subcommand
.setName('dashboard')
- .setDescription('Open the interactive leveling configuration dashboard'),
+ .setDescription('Das interaktive Leveling-Dashboard öffnen'),
),
category: 'Leveling',
@@ -73,7 +75,7 @@ export default {
if (!deferred) return;
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the **Manage Server** permission to use this command.' });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um diesen Command zu nutzen.' });
}
const subcommand = interaction.options.getSubcommand();
@@ -83,30 +85,30 @@ export default {
}
if (subcommand === 'setup') {
- const channel = interaction.options.getChannel('channel');
+ const channel = interaction.options.getChannel('channel') as TextChannel;
const xpMin = interaction.options.getInteger('xp_min') ?? 15;
const xpMax = interaction.options.getInteger('xp_max') ?? 25;
const message =
interaction.options.getString('message') ??
- '{user} has leveled up to level {level}!';
+ '{user} ist auf Level {level} aufgestiegen!';
const xpCooldown = interaction.options.getInteger('xp_cooldown') ?? 60;
if (xpMin > xpMax) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: `Minimum XP (**${xpMin}**) cannot be greater than maximum XP (**${xpMax}**).` });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: `Die minimalen XP (**${xpMin}**) dürfen nicht größer sein als die maximalen XP (**${xpMax}**).` });
}
if (!botHasPermission(channel, ['SendMessages', 'EmbedLinks'])) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Bot missing permissions in the specified channel',
ErrorTypes.PERMISSION,
- `I need **SendMessages** and **EmbedLinks** permissions in ${channel} to send level-up notifications.`,
+ `Ich brauche die Berechtigungen **Nachrichten senden** und **Links einbetten** in ${channel}, um Level-up-Benachrichtigungen zu senden.`,
);
}
- const existingConfig = await getLevelingConfig(client, interaction.guildId);
+ const existingConfig = await getLevelingConfig(client, interaction.guildId!);
if (existingConfig.configured) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `The leveling system is already set up on this server (level-up notifications go to <#${existingConfig.levelUpChannel}>).\n\nUse \`/level dashboard\` to adjust any settings.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Das Leveling-System ist auf diesem Server bereits eingerichtet (Level-up-Benachrichtigungen gehen an <#${existingConfig.levelUpChannel}>).\n\nNutze \`/level dashboard\`, um die Einstellungen anzupassen.` });
}
const newConfig = {
@@ -120,7 +122,7 @@ export default {
announceLevelUp: true,
};
- await saveLevelingConfig(client, interaction.guildId, newConfig);
+ await saveLevelingConfig(client, interaction.guildId!, newConfig);
logger.info(`Leveling system set up in guild ${interaction.guildId}`, {
channelId: channel.id,
@@ -133,18 +135,18 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
createEmbed({
- title: 'Leveling System Set Up',
+ title: 'Leveling-System eingerichtet',
description:
- `The leveling system is now **enabled** and ready to go.\n\n` +
- `**Level-up Channel:** ${channel}\n` +
- `**XP per Message:** ${xpMin} – ${xpMax}\n` +
- `**XP Cooldown:** ${xpCooldown}s\n` +
- `**Level-up Message:** \`${message}\`\n\n` +
- `Use \`/level dashboard\` to adjust any of these settings at any time.`,
+ `Das Leveling-System ist jetzt **aktiviert** und einsatzbereit.\n\n` +
+ `**Level-up-Kanal:** ${channel}\n` +
+ `**XP pro Nachricht:** ${xpMin} – ${xpMax}\n` +
+ `**XP-Cooldown:** ${xpCooldown}s\n` +
+ `**Level-up-Nachricht:** \`${message}\`\n\n` +
+ `Mit \`/level dashboard\` kannst du diese Einstellungen jederzeit anpassen.`,
color: 'success',
}),
],
});
}
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Leveling/modules/level_dashboard.js b/src/commands/Leveling/modules/level_dashboard.ts
similarity index 64%
rename from src/commands/Leveling/modules/level_dashboard.js
rename to src/commands/Leveling/modules/level_dashboard.ts
index 3d8ee34b61..41b5ec4c6a 100644
--- a/src/commands/Leveling/modules/level_dashboard.js
+++ b/src/commands/Leveling/modules/level_dashboard.ts
@@ -16,108 +16,111 @@ import {
ComponentType,
EmbedBuilder,
} from 'discord.js';
+import type { ChatInputCommandInteraction, Guild, SelectMenuModalData, StringSelectMenuInteraction } from 'discord.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { successEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
import { getLevelingConfig, saveLevelingConfig } from '../../../services/leveling/leveling.js';
import { botHasPermission } from '../../../utils/permissionGuard.js';
import { startDashboardSession } from '../../../utils/dashboardSession.js';
+import type { GuildLevelingConfig } from '../../../services/leveling/leveling.js';
+import type { NexoClient, SubcommandModule } from '../../../types/index.js';
-function buildDashboardEmbed(cfg, guild) {
- const channel = cfg.levelUpChannel ? `<#${cfg.levelUpChannel}>` : '`Not set`';
+function buildDashboardEmbed(cfg: GuildLevelingConfig, guild: Guild) {
+ const channel = cfg.levelUpChannel ? `<#${cfg.levelUpChannel}>` : '`Nicht gesetzt`';
const xpMin = cfg.xpRange?.min ?? cfg.xpPerMessage?.min ?? 15;
const xpMax = cfg.xpRange?.max ?? cfg.xpPerMessage?.max ?? 25;
const cooldown = cfg.xpCooldown ?? 60;
- const rawMsg = cfg.levelUpMessage || '{user} has leveled up to level {level}!';
+ const rawMsg = cfg.levelUpMessage || '{user} ist auf Level {level} aufgestiegen!';
const msgPreview = `\`${rawMsg.length > 60 ? rawMsg.substring(0, 60) + '…' : rawMsg}\``;
const rewards = cfg.roleRewards ?? {};
const rewardEntries = Object.entries(rewards).sort(([a], [b]) => Number(a) - Number(b));
const rewardsValue = rewardEntries.length > 0
? rewardEntries.map(([lvl, roleId]) => `Level **${lvl}** → <@&${roleId}>`).join('\n')
- : '`None configured`';
+ : '`Keine eingerichtet`';
const ignoredChannels = cfg.ignoredChannels ?? [];
const ignoredRoles = cfg.ignoredRoles ?? [];
- const ignoredChValue = ignoredChannels.length > 0 ? ignoredChannels.map(id => `<#${id}>`).join(',') : '`None`';
- const ignoredRoValue = ignoredRoles.length > 0 ? ignoredRoles.map(id => `<@&${id}>`).join(',') : '`None`';
+ const ignoredChValue = ignoredChannels.length > 0 ? ignoredChannels.map(id => `<#${id}>`).join(',') : '`Keine`';
+ const ignoredRoValue = ignoredRoles.length > 0 ? ignoredRoles.map(id => `<@&${id}>`).join(',') : '`Keine`';
return new EmbedBuilder()
- .setTitle('⚡ Leveling System Dashboard')
- .setDescription(`Manage leveling settings for **${guild.name}**.\nSelect an option below to modify a setting.`)
+ .setTitle('⚡ Leveling-Dashboard')
+ .setDescription(`Verwalte die Leveling-Einstellungen für **${guild.name}**.\nWähle unten eine Option, um eine Einstellung zu ändern.`)
.setColor(getColor('info'))
.addFields(
- { name: 'Level-up Channel', value: channel, inline: true },
- { name: 'System Status', value: cfg.enabled ? '**Enabled**' : '**Disabled**', inline: true },
- { name: 'Announcements', value: cfg.announceLevelUp !== false ? '**Enabled**' : '**Disabled**', inline: true },
- { name: 'XP per Message', value: `\`${xpMin} – ${xpMax}\``, inline: true },
- { name: 'XP Cooldown', value: `\`${cooldown}s\``, inline: true },
+ { name: 'Level-up-Kanal', value: channel, inline: true },
+ { name: 'Systemstatus', value: cfg.enabled ? '**Aktiviert**' : '**Deaktiviert**', inline: true },
+ { name: 'Ankündigungen', value: cfg.announceLevelUp !== false ? '**Aktiviert**' : '**Deaktiviert**', inline: true },
+ { name: 'XP pro Nachricht', value: `\`${xpMin} – ${xpMax}\``, inline: true },
+ { name: 'XP-Cooldown', value: `\`${cooldown}s\``, inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
- { name: 'Level-up Message', value: msgPreview, inline: false },
- { name: 'Role Rewards', value: rewardsValue, inline: false },
- { name: 'Ignored Channels', value: ignoredChValue, inline: true },
- { name: 'Ignored Roles', value: ignoredRoValue, inline: true },
+ { name: 'Level-up-Nachricht', value: msgPreview, inline: false },
+ { name: 'Rollen-Belohnungen', value: rewardsValue, inline: false },
+ { name: 'Ignorierte Kanäle', value: ignoredChValue, inline: true },
+ { name: 'Ignorierte Rollen', value: ignoredRoValue, inline: true },
)
- .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität' })
.setTimestamp();
}
-function buildSelectMenu(guildId) {
+function buildSelectMenu(guildId: string) {
return new StringSelectMenuBuilder()
.setCustomId(`level_cfg_${guildId}`)
- .setPlaceholder('Select a setting to configure...')
+ .setPlaceholder('Wähle eine Einstellung...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Change Level-up Channel')
- .setDescription('Set the channel where level-up notifications are sent')
+ .setLabel('Level-up-Kanal ändern')
+ .setDescription('Lege fest, in welchem Kanal Level-up-Benachrichtigungen gesendet werden')
.setValue('channel')
.setEmoji('📢'),
new StringSelectMenuOptionBuilder()
- .setLabel('Edit Level-up Message')
- .setDescription('Customise the message shown when a user levels up')
+ .setLabel('Level-up-Nachricht bearbeiten')
+ .setDescription('Passe die Nachricht an, die beim Level-up eines Nutzers erscheint')
.setValue('message')
.setEmoji('💬'),
new StringSelectMenuOptionBuilder()
- .setLabel('Set XP Range')
- .setDescription('Set the minimum and maximum XP rewarded per message')
+ .setLabel('XP-Bereich festlegen')
+ .setDescription('Lege die minimalen und maximalen XP pro Nachricht fest')
.setValue('xp_range')
.setEmoji('🎲'),
new StringSelectMenuOptionBuilder()
- .setLabel('Set XP Cooldown')
- .setDescription('Seconds between XP grants for the same user')
+ .setLabel('XP-Cooldown festlegen')
+ .setDescription('Sekunden zwischen XP-Vergaben für denselben Nutzer')
.setValue('xp_cooldown')
.setEmoji('⏱️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Add Role Reward')
- .setDescription('Award a role when a user reaches a specific level')
+ .setLabel('Rollen-Belohnung hinzufügen')
+ .setDescription('Vergib eine Rolle, wenn ein Nutzer ein bestimmtes Level erreicht')
.setValue('role_reward_add')
.setEmoji('🏆'),
new StringSelectMenuOptionBuilder()
- .setLabel('Remove Role Reward')
- .setDescription('Remove a role reward from a specific level')
+ .setLabel('Rollen-Belohnung entfernen')
+ .setDescription('Entferne die Rollen-Belohnung eines bestimmten Levels')
.setValue('role_reward_remove')
.setEmoji('\ud83d\uddd1\ufe0f'),
new StringSelectMenuOptionBuilder()
- .setLabel('Ignored Channels')
- .setDescription('Toggle channels where XP will not be awarded')
+ .setLabel('Ignorierte Kanäle')
+ .setDescription('Kanäle umschalten, in denen keine XP vergeben werden')
.setValue('ignore_channels')
.setEmoji('\ud83d\udeab'),
new StringSelectMenuOptionBuilder()
- .setLabel('Ignored Roles')
- .setDescription('Toggle roles that will not receive XP')
+ .setLabel('Ignorierte Rollen')
+ .setDescription('Rollen umschalten, die keine XP erhalten')
.setValue('ignore_roles')
.setEmoji('\ud83d\udeab'),
);
}
-function buildButtonRow(cfg, guildId, disabled = false) {
+function buildButtonRow(cfg: GuildLevelingConfig, guildId: string, disabled = false) {
const announceOn = cfg.announceLevelUp !== false;
const systemOn = cfg.enabled !== false;
- return new ActionRowBuilder().addComponents(
+ return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`level_cfg_toggle_announce_${guildId}`)
- .setLabel('Announcements')
+ .setLabel('Ankündigungen')
.setStyle(announceOn ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji('📣')
.setDisabled(disabled),
@@ -130,13 +133,13 @@ function buildButtonRow(cfg, guildId, disabled = false) {
);
}
-async function refreshDashboard(rootInteraction, cfg, guildId) {
+async function refreshDashboard(rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string) {
const selectMenu = buildSelectMenu(guildId);
await InteractionHelper.safeEditReply(rootInteraction, {
- embeds: [buildDashboardEmbed(cfg, rootInteraction.guild)],
+ embeds: [buildDashboardEmbed(cfg, rootInteraction.guild!)],
components: [
buildButtonRow(cfg, guildId),
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
}).catch(() => {});
}
@@ -145,23 +148,23 @@ export default {
prefixOnly: false,
async execute(interaction, config, client) {
try {
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
const cfg = await getLevelingConfig(client, guildId);
if (!cfg.configured) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Leveling system not configured',
ErrorTypes.CONFIGURATION,
- 'The leveling system has not been set up yet. Run `/level setup` first to configure it.',
+ 'Das Leveling-System ist noch nicht eingerichtet. Führe zuerst `/level setup` aus.',
);
}
await startDashboardSession({
interaction,
- embeds: [buildDashboardEmbed(cfg, interaction.guild)],
+ embeds: [buildDashboardEmbed(cfg, interaction.guild!)],
components: [
buildButtonRow(cfg, guildId),
- new ActionRowBuilder().addComponents(buildSelectMenu(guildId)),
+ new ActionRowBuilder().addComponents(buildSelectMenu(guildId)),
],
selectMenuId: `level_cfg_${guildId}`,
buttonMatcher: (customId) =>
@@ -206,8 +209,8 @@ export default {
await btnInteraction.followUp({
embeds: [
successEmbed(
- '✅ Announcements Updated',
- `Level-up announcements are now **${cfg.announceLevelUp ? 'enabled' : 'disabled'}**.`,
+ '✅ Ankündigungen aktualisiert',
+ `Level-up-Ankündigungen sind jetzt **${cfg.announceLevelUp ? 'aktiviert' : 'deaktiviert'}**.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -219,8 +222,8 @@ export default {
await btnInteraction.followUp({
embeds: [
successEmbed(
- '✅ System Updated',
- `The leveling system is now **${cfg.enabled ? 'enabled' : 'disabled'}**.${!cfg.enabled ? '\nUsers will not earn XP until the system is re-enabled.' : ''}`,
+ '✅ System aktualisiert',
+ `Das Leveling-System ist jetzt **${cfg.enabled ? 'aktiviert' : 'deaktiviert'}**.${!cfg.enabled ? '\nNutzer erhalten keine XP, bis das System wieder aktiviert wird.' : ''}`,
),
],
flags: MessageFlags.Ephemeral,
@@ -231,37 +234,37 @@ export default {
},
});
} catch (error) {
- if (error instanceof TitanBotError) throw error;
+ if (error instanceof NexoBotError) throw error;
logger.error('Unexpected error in level_dashboard:', error);
- throw new TitanBotError(
- `Level dashboard failed: ${error.message}`,
+ throw new NexoBotError(
+ `Level dashboard failed: ${error instanceof Error ? error.message : String(error)}`,
ErrorTypes.UNKNOWN,
- 'Failed to open the leveling dashboard.',
+ 'Das Leveling-Dashboard konnte nicht geöffnet werden.',
);
}
},
-};
+} satisfies SubcommandModule & { prefixOnly: boolean };
-async function handleRoleRewardAdd(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleRoleRewardAdd(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId(`level_cfg_role_reward_add_${guildId}`)
- .setTitle('🏆 Add Role Reward');
+ .setTitle('🏆 Rollen-Belohnung hinzufügen');
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('reward_role')
- .setPlaceholder('Select a role to award...')
+ .setPlaceholder('Wähle eine Rolle als Belohnung...')
.setMinValues(1)
.setMaxValues(1)
.setRequired(true);
const roleLabel = new LabelBuilder()
- .setLabel('Role to Award')
- .setDescription('This role will be given when the user reaches the level')
+ .setLabel('Belohnungsrolle')
+ .setDescription('Diese Rolle erhält der Nutzer, wenn er das Level erreicht')
.setRoleSelectMenuComponent(roleSelect);
const levelInput = new TextInputBuilder()
.setCustomId('reward_level')
- .setLabel('Level required (1–500)')
+ .setLabel('Benötigtes Level (1–500)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('10')
.setMaxLength(3)
@@ -269,7 +272,7 @@ async function handleRoleRewardAdd(selectInteraction, rootInteraction, cfg, guil
.setRequired(true);
modal.addLabelComponents(roleLabel);
- modal.addComponents(new ActionRowBuilder().addComponents(levelInput));
+ modal.addComponents(new ActionRowBuilder().addComponents(levelInput));
await selectInteraction.showModal(modal);
@@ -286,25 +289,25 @@ async function handleRoleRewardAdd(selectInteraction, rootInteraction, cfg, guil
const level = parseInt(rawLevel, 10);
if (isNaN(level) || level < 1 || level > 500) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Level must be a whole number between **1** and **500**.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Das Level muss eine ganze Zahl zwischen **1** und **500** sein.' });
return;
}
- const roleId = submitted.fields.getField('reward_role').values[0];
+ const roleId = (submitted.fields.getField('reward_role') as SelectMenuModalData).values[0];
cfg.roleRewards = cfg.roleRewards ?? {};
cfg.roleRewards[level] = roleId;
await saveLevelingConfig(client, guildId, cfg);
await submitted.reply({
- embeds: [successEmbed('Role Reward Added', `<@&${roleId}> will now be awarded at level **${level}**.`)],
+ embeds: [successEmbed('Rollen-Belohnung hinzugefügt', `<@&${roleId}> wird jetzt ab Level **${level}** vergeben.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleRoleRewardRemove(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleRoleRewardRemove(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string, client: NexoClient) {
const rewards = cfg.roleRewards ?? {};
const entries = Object.entries(rewards).sort(([a], [b]) => Number(a) - Number(b));
@@ -312,25 +315,25 @@ async function handleRoleRewardRemove(selectInteraction, rootInteraction, cfg, g
await selectInteraction.deferUpdate();
await replyUserError(selectInteraction, {
type: ErrorTypes.USER_INPUT,
- message: 'There are no role rewards configured to remove.',
+ message: 'Es sind keine Rollen-Belohnungen eingerichtet, die entfernt werden können.',
});
return;
}
const modal = new ModalBuilder()
.setCustomId(`level_cfg_role_reward_remove_${guildId}`)
- .setTitle('🗑️ Remove Role Reward');
+ .setTitle('🗑️ Rollen-Belohnung entfernen');
const infoInput = new TextInputBuilder()
.setCustomId('current_rewards')
- .setLabel('Current rewards (read-only)')
+ .setLabel('Aktuelle Belohnungen (nur lesen)')
.setStyle(TextInputStyle.Paragraph)
.setValue(entries.map(([lvl, roleId]) => `Level ${lvl}: <@&${roleId}>`).join('\n'))
.setRequired(false);
const levelInput = new TextInputBuilder()
.setCustomId('remove_level')
- .setLabel('Level to remove reward from')
+ .setLabel('Level, dessen Belohnung entfernt wird')
.setStyle(TextInputStyle.Short)
.setValue(entries[0][0])
.setMaxLength(3)
@@ -338,8 +341,8 @@ async function handleRoleRewardRemove(selectInteraction, rootInteraction, cfg, g
.setRequired(true);
modal.addComponents(
- new ActionRowBuilder().addComponents(infoInput),
- new ActionRowBuilder().addComponents(levelInput),
+ new ActionRowBuilder().addComponents(infoInput),
+ new ActionRowBuilder().addComponents(levelInput),
);
await selectInteraction.showModal(modal);
@@ -357,7 +360,7 @@ async function handleRoleRewardRemove(selectInteraction, rootInteraction, cfg, g
const level = parseInt(rawLevel, 10);
if (isNaN(level) || !cfg.roleRewards?.[level]) {
- await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: `No role reward is configured for level **${rawLevel}**.` });
+ await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: `Für Level **${rawLevel}** ist keine Rollen-Belohnung eingerichtet.` });
return;
}
@@ -365,29 +368,29 @@ async function handleRoleRewardRemove(selectInteraction, rootInteraction, cfg, g
await saveLevelingConfig(client, guildId, cfg);
await submitted.reply({
- embeds: [successEmbed('Role Reward Removed', `The role reward for level **${level}** has been removed.`)],
+ embeds: [successEmbed('Rollen-Belohnung entfernt', `Die Rollen-Belohnung für Level **${level}** wurde entfernt.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleChannel(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId(`level_cfg_channel_modal_${guildId}`)
- .setTitle('\ud83d\udce2 Change Level-up Channel');
+ .setTitle('\ud83d\udce2 Level-up-Kanal ändern');
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('levelup_channel')
- .setPlaceholder('Select a text channel...')
+ .setPlaceholder('Wähle einen Textkanal...')
.setMinValues(1)
.setMaxValues(1)
.addChannelTypes(ChannelType.GuildText)
.setRequired(true);
const channelLabel = new LabelBuilder()
- .setLabel('Level-up Channel')
- .setDescription('Channel where level-up notifications will be sent')
+ .setLabel('Level-up-Kanal')
+ .setDescription('Kanal, in dem Level-up-Benachrichtigungen gesendet werden')
.setChannelSelectMenuComponent(channelSelect);
modal.addLabelComponents(channelLabel);
@@ -403,11 +406,11 @@ async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, c
if (!submitted) return;
- const channelId = submitted.fields.getField('levelup_channel').values[0];
- const channel = selectInteraction.guild.channels.cache.get(channelId);
+ const channelId = (submitted.fields.getField('levelup_channel') as SelectMenuModalData).values[0];
+ const channel = selectInteraction.guild!.channels.cache.get(channelId);
if (channel && !botHasPermission(channel, ['SendMessages', 'EmbedLinks'])) {
- await replyUserError(submitted, { type: ErrorTypes.PERMISSION, message: `I need **SendMessages** and **EmbedLinks** permissions in ${channel} to send level-up notifications.` });
+ await replyUserError(submitted, { type: ErrorTypes.PERMISSION, message: `Ich brauche die Berechtigungen **Nachrichten senden** und **Links einbetten** in ${channel}, um Level-up-Benachrichtigungen zu senden.` });
return;
}
@@ -415,29 +418,29 @@ async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, c
await saveLevelingConfig(client, guildId, cfg);
await submitted.reply({
- embeds: [successEmbed('\u2705 Channel Updated', `Level-up notifications will now be sent in ${channel ??`<#${channelId}>`}.`)],
+ embeds: [successEmbed('\u2705 Kanal aktualisiert', `Level-up-Benachrichtigungen werden jetzt in ${channel ??`<#${channelId}>`} gesendet.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleIgnoreChannels(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleIgnoreChannels(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId(`level_cfg_ignore_channels_${guildId}`)
- .setTitle('\ud83d\udeab Ignored Channels');
+ .setTitle('\ud83d\udeab Ignorierte Kanäle');
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('ignore_channel')
- .setPlaceholder('Select channels to toggle...')
+ .setPlaceholder('Wähle Kanäle zum Umschalten...')
.setMinValues(1)
.setMaxValues(10)
.addChannelTypes(ChannelType.GuildText)
.setRequired(true);
const channelLabel = new LabelBuilder()
- .setLabel('Toggle Ignored Channels')
- .setDescription('Selected channels will be toggled — XP will not be awarded in them')
+ .setLabel('Ignorierte Kanäle umschalten')
+ .setDescription('Ausgewählte Kanäle werden umgeschaltet – in ignorierten Kanälen gibt es keine XP')
.setChannelSelectMenuComponent(channelSelect);
modal.addLabelComponents(channelLabel);
@@ -453,7 +456,7 @@ async function handleIgnoreChannels(selectInteraction, rootInteraction, cfg, gui
if (!submitted) return;
- const selectedIds = submitted.fields.getField('ignore_channel').values;
+ const selectedIds = (submitted.fields.getField('ignore_channel') as SelectMenuModalData).values;
const ignoreSet = new Set(cfg.ignoredChannels ?? []);
for (const id of selectedIds) {
@@ -469,31 +472,31 @@ async function handleIgnoreChannels(selectInteraction, rootInteraction, cfg, gui
const list = cfg.ignoredChannels.length > 0
? cfg.ignoredChannels.map(id => `<#${id}>`).join(',')
- : '`None`';
+ : '`Keine`';
await submitted.reply({
- embeds: [successEmbed('\u2705 Ignored Channels Updated', `XP will not be awarded in: ${list}`)],
+ embeds: [successEmbed('\u2705 Ignorierte Kanäle aktualisiert', `In diesen Kanälen gibt es keine XP: ${list}`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleIgnoreRoles(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleIgnoreRoles(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId(`level_cfg_ignore_roles_${guildId}`)
- .setTitle('\ud83d\udeab Ignored Roles');
+ .setTitle('\ud83d\udeab Ignorierte Rollen');
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('ignore_role')
- .setPlaceholder('Select roles to toggle...')
+ .setPlaceholder('Wähle Rollen zum Umschalten...')
.setMinValues(1)
.setMaxValues(10)
.setRequired(true);
const roleLabel = new LabelBuilder()
- .setLabel('Toggle Ignored Roles')
- .setDescription('Selected roles will be toggled — members with them will not earn XP')
+ .setLabel('Ignorierte Rollen umschalten')
+ .setDescription('Ausgewählte Rollen werden umgeschaltet – Mitglieder mit ignorierten Rollen erhalten keine XP')
.setRoleSelectMenuComponent(roleSelect);
modal.addLabelComponents(roleLabel);
@@ -509,7 +512,7 @@ async function handleIgnoreRoles(selectInteraction, rootInteraction, cfg, guildI
if (!submitted) return;
- const selectedIds = submitted.fields.getField('ignore_role').values;
+ const selectedIds = (submitted.fields.getField('ignore_role') as SelectMenuModalData).values;
const ignoreSet = new Set(cfg.ignoredRoles ?? []);
for (const id of selectedIds) {
@@ -525,31 +528,31 @@ async function handleIgnoreRoles(selectInteraction, rootInteraction, cfg, guildI
const list = cfg.ignoredRoles.length > 0
? cfg.ignoredRoles.map(id => `<@&${id}>`).join(',')
- : '`None`';
+ : '`Keine`';
await submitted.reply({
- embeds: [successEmbed('\u2705 Ignored Roles Updated', `These roles will not earn XP: ${list}`)],
+ embeds: [successEmbed('\u2705 Ignorierte Rollen aktualisiert', `Diese Rollen erhalten keine XP: ${list}`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleMessage(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleMessage(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('level_cfg_message')
- .setTitle('💬 Edit Level-up Message')
+ .setTitle('💬 Level-up-Nachricht bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('message_input')
- .setLabel('Message ({user} and {level} are available)')
+ .setLabel('Nachricht ({user} und {level} möglich)')
.setStyle(TextInputStyle.Paragraph)
- .setValue(cfg.levelUpMessage || '{user} has leveled up to level {level}!')
+ .setValue(cfg.levelUpMessage || '{user} ist auf Level {level} aufgestiegen!')
.setMaxLength(500)
.setMinLength(1)
.setRequired(true)
- .setPlaceholder('{user} has leveled up to level {level}!'),
+ .setPlaceholder('{user} ist auf Level {level} aufgestiegen!'),
),
);
@@ -576,13 +579,13 @@ async function handleMessage(selectInteraction, rootInteraction, cfg, guildId, c
cfg.levelUpMessage = newMessage;
await saveLevelingConfig(client, guildId, cfg);
- const preview = newMessage.replace('{user}', '@User').replace('{level}', '5');
+ const preview = newMessage.replace('{user}', '@Nutzer').replace('{level}', '5');
await submitted.reply({
embeds: [
successEmbed(
- '✅ Message Updated',
- `Level-up message saved.\n**Preview:** ${preview}`,
+ '✅ Nachricht aktualisiert',
+ `Level-up-Nachricht gespeichert.\n**Vorschau:** ${preview}`,
),
],
flags: MessageFlags.Ephemeral,
@@ -591,18 +594,18 @@ async function handleMessage(selectInteraction, rootInteraction, cfg, guildId, c
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleXpRange(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleXpRange(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string, client: NexoClient) {
const currentMin = cfg.xpRange?.min ?? cfg.xpPerMessage?.min ?? 15;
const currentMax = cfg.xpRange?.max ?? cfg.xpPerMessage?.max ?? 25;
const modal = new ModalBuilder()
.setCustomId('level_cfg_xp_range')
- .setTitle('Set XP Range per Message')
+ .setTitle('XP-Bereich pro Nachricht festlegen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('xp_min_input')
- .setLabel('Minimum XP (1–500)')
+ .setLabel('Minimale XP (1–500)')
.setStyle(TextInputStyle.Short)
.setValue(String(currentMin))
.setMaxLength(3)
@@ -610,10 +613,10 @@ async function handleXpRange(selectInteraction, rootInteraction, cfg, guildId, c
.setRequired(true)
.setPlaceholder('15'),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('xp_max_input')
- .setLabel('Maximum XP (1–500)')
+ .setLabel('Maximale XP (1–500)')
.setStyle(TextInputStyle.Short)
.setValue(String(currentMax))
.setMaxLength(3)
@@ -641,12 +644,12 @@ async function handleXpRange(selectInteraction, rootInteraction, cfg, guildId, c
const newMax = parseInt(rawMax, 10);
if (isNaN(newMin) || isNaN(newMax) || newMin < 1 || newMax < 1 || newMin > 500 || newMax > 500) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Both XP values must be whole numbers between **1** and **500**.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Beide XP-Werte müssen ganze Zahlen zwischen **1** und **500** sein.' });
return;
}
if (newMin > newMax) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Minimum XP cannot be greater than maximum XP.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Die minimalen XP dürfen nicht größer sein als die maximalen XP.' });
return;
}
@@ -656,8 +659,8 @@ async function handleXpRange(selectInteraction, rootInteraction, cfg, guildId, c
await submitted.reply({
embeds: [
successEmbed(
- '✅ XP Range Updated',
- `Users will now earn between **${newMin}** and **${newMax}** XP per message.`,
+ '✅ XP-Bereich aktualisiert',
+ `Nutzer erhalten jetzt zwischen **${newMin}** und **${newMax}** XP pro Nachricht.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -666,15 +669,15 @@ async function handleXpRange(selectInteraction, rootInteraction, cfg, guildId, c
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleXpCooldown(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleXpCooldown(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: GuildLevelingConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('level_cfg_cooldown')
- .setTitle('⏱️ Set XP Cooldown')
+ .setTitle('⏱️ XP-Cooldown festlegen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('cooldown_input')
- .setLabel('Cooldown in seconds (0–3600)')
+ .setLabel('Cooldown in Sekunden (0–3600)')
.setStyle(TextInputStyle.Short)
.setValue(String(cfg.xpCooldown ?? 60))
.setMaxLength(4)
@@ -700,7 +703,7 @@ async function handleXpCooldown(selectInteraction, rootInteraction, cfg, guildId
const newCooldown = parseInt(raw, 10);
if (isNaN(newCooldown) || newCooldown < 0 || newCooldown > 3600) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Cooldown must be a whole number between **0** and **3600** seconds.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Der Cooldown muss eine ganze Zahl zwischen **0** und **3600** Sekunden sein.' });
return;
}
@@ -710,8 +713,8 @@ async function handleXpCooldown(selectInteraction, rootInteraction, cfg, guildId
await submitted.reply({
embeds: [
successEmbed(
- '✅ Cooldown Updated',
- `XP cooldown set to **${newCooldown} second${newCooldown !== 1 ? 's' : ''}**.${newCooldown === 0 ? '\n> ⚠️ A cooldown of 0 means XP is granted on every message.' : ''}`,
+ '✅ Cooldown aktualisiert',
+ `XP-Cooldown auf **${newCooldown} Sekunde${newCooldown !== 1 ? 'n' : ''}** gesetzt.${newCooldown === 0 ? '\n> ⚠️ Bei einem Cooldown von 0 gibt es für jede Nachricht XP.' : ''}`,
),
],
flags: MessageFlags.Ephemeral,
diff --git a/src/commands/Leveling/leveladd.js b/src/commands/Leveling/modules/leveladd.ts
similarity index 55%
rename from src/commands/Leveling/leveladd.js
rename to src/commands/Leveling/modules/leveladd.ts
index 9d439696b7..9b9c190a4f 100644
--- a/src/commands/Leveling/leveladd.js
+++ b/src/commands/Leveling/modules/leveladd.ts
@@ -1,25 +1,26 @@
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { checkUserPermissions } from '../../utils/permissionGuard.js';
-import { addLevels, getLevelingConfig } from '../../services/leveling/leveling.js';
-import { createEmbed } from '../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { checkUserPermissions } from '../../../utils/permissionGuard.js';
+import { addLevels, getLevelingConfig } from '../../../services/leveling/leveling.js';
+import { createEmbed } from '../../../utils/embeds.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('leveladd')
- .setDescription('Add levels to a user')
+ .setDescription('Einem Nutzer Level hinzufügen')
.addUserOption((option) =>
option
.setName('user')
- .setDescription('The user to add levels to')
+ .setDescription('Der Nutzer, der Level erhalten soll')
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName('levels')
- .setDescription('Number of levels to add')
+ .setDescription('Anzahl der Level, die hinzugefügt werden')
.setRequired(true)
.setMinValue(1)
)
@@ -33,42 +34,42 @@ export default {
const hasPermission = await checkUserPermissions(
interaction,
PermissionFlagsBits.ManageGuild,
- 'You need ManageGuild permission to use this command.'
+ 'Du brauchst die Berechtigung „Server verwalten“, um diesen Command zu nutzen.'
);
if (!hasPermission) return;
- const levelingConfig = await getLevelingConfig(client, interaction.guildId);
+ const levelingConfig = await getLevelingConfig(client, interaction.guildId!);
if (!levelingConfig?.enabled) {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
new EmbedBuilder()
.setColor('#f1c40f')
- .setDescription('The leveling system is currently disabled on this server.')
+ .setDescription('Das Leveling-System ist auf diesem Server derzeit deaktiviert.')
],
flags: MessageFlags.Ephemeral
});
return;
}
- const targetUser = interaction.options.getUser('user');
- const levelsToAdd = interaction.options.getInteger('levels');
+ const targetUser = interaction.options.getUser('user')!;
+ const levelsToAdd = interaction.options.getInteger('levels')!;
- const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null);
+ const member = await interaction.guild!.members.fetch(targetUser.id).catch(() => null);
if (!member) {
- throw new TitanBotError(
+ throw new NexoBotError(
`User ${targetUser.id} not found in this guild`,
ErrorTypes.USER_INPUT,
- 'The specified user is not in this server.'
+ 'Der angegebene Nutzer ist nicht auf diesem Server.'
);
}
- const userData = await addLevels(client, interaction.guildId, targetUser.id, levelsToAdd);
+ const userData = await addLevels(client, interaction.guildId!, targetUser.id, levelsToAdd);
await InteractionHelper.safeEditReply(interaction, {
embeds: [
createEmbed({
- title: 'Levels Added',
- description: `Successfully added ${levelsToAdd} levels to ${targetUser.tag}.\n**New Level:** ${userData.level}`,
+ title: 'Level hinzugefügt',
+ description: `${targetUser.tag} hat ${levelsToAdd} Level erhalten.\n**Neues Level:** ${userData.level}`,
color: 'success'
})
]
@@ -78,4 +79,4 @@ export default {
`[ADMIN] User ${interaction.user.tag} added ${levelsToAdd} levels to ${targetUser.tag} in guild ${interaction.guildId}`
);
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Leveling/levelremove.js b/src/commands/Leveling/modules/levelremove.ts
similarity index 57%
rename from src/commands/Leveling/levelremove.js
rename to src/commands/Leveling/modules/levelremove.ts
index f781d33df8..dc1204b623 100644
--- a/src/commands/Leveling/levelremove.js
+++ b/src/commands/Leveling/modules/levelremove.ts
@@ -1,25 +1,26 @@
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { checkUserPermissions } from '../../utils/permissionGuard.js';
-import { removeLevels, getUserLevelData, getLevelingConfig } from '../../services/leveling/leveling.js';
-import { createEmbed } from '../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { checkUserPermissions } from '../../../utils/permissionGuard.js';
+import { removeLevels, getUserLevelData, getLevelingConfig } from '../../../services/leveling/leveling.js';
+import { createEmbed } from '../../../utils/embeds.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('levelremove')
- .setDescription('Remove levels from a user')
+ .setDescription('Einem Nutzer Level abziehen')
.addUserOption((option) =>
option
.setName('user')
- .setDescription('The user to remove levels from')
+ .setDescription('Der Nutzer, dem Level abgezogen werden')
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName('levels')
- .setDescription('Number of levels to remove')
+ .setDescription('Anzahl der Level, die abgezogen werden')
.setRequired(true)
.setMinValue(1)
)
@@ -33,51 +34,51 @@ export default {
const hasPermission = await checkUserPermissions(
interaction,
PermissionFlagsBits.ManageGuild,
- 'You need ManageGuild permission to use this command.'
+ 'Du brauchst die Berechtigung „Server verwalten“, um diesen Command zu nutzen.'
);
if (!hasPermission) return;
- const levelingConfig = await getLevelingConfig(client, interaction.guildId);
+ const levelingConfig = await getLevelingConfig(client, interaction.guildId!);
if (!levelingConfig?.enabled) {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
new EmbedBuilder()
.setColor('#f1c40f')
- .setDescription('The leveling system is currently disabled on this server.')
+ .setDescription('Das Leveling-System ist auf diesem Server derzeit deaktiviert.')
],
flags: MessageFlags.Ephemeral
});
return;
}
- const targetUser = interaction.options.getUser('user');
- const levelsToRemove = interaction.options.getInteger('levels');
+ const targetUser = interaction.options.getUser('user')!;
+ const levelsToRemove = interaction.options.getInteger('levels')!;
- const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null);
+ const member = await interaction.guild!.members.fetch(targetUser.id).catch(() => null);
if (!member) {
- throw new TitanBotError(
+ throw new NexoBotError(
`User ${targetUser.id} not found in this guild`,
ErrorTypes.USER_INPUT,
- 'The specified user is not in this server.'
+ 'Der angegebene Nutzer ist nicht auf diesem Server.'
);
}
- const userData = await getUserLevelData(client, interaction.guildId, targetUser.id);
+ const userData = await getUserLevelData(client, interaction.guildId!, targetUser.id);
if (userData.level === 0) {
- throw new TitanBotError(
+ throw new NexoBotError(
`User ${targetUser.id} is already at minimum level`,
ErrorTypes.VALIDATION,
- `${targetUser.tag} is already at level 0 and cannot have levels removed.`
+ `${targetUser.tag} ist bereits auf Level 0, es können keine Level abgezogen werden.`
);
}
- const updatedData = await removeLevels(client, interaction.guildId, targetUser.id, levelsToRemove);
+ const updatedData = await removeLevels(client, interaction.guildId!, targetUser.id, levelsToRemove);
await InteractionHelper.safeEditReply(interaction, {
embeds: [
createEmbed({
- title: 'Levels Removed',
- description: `Successfully removed ${levelsToRemove} levels from ${targetUser.tag}.\n**New Level:** ${updatedData.level}`,
+ title: 'Level abgezogen',
+ description: `${targetUser.tag} wurden ${levelsToRemove} Level abgezogen.\n**Neues Level:** ${updatedData.level}`,
color: 'success'
})
]
@@ -87,4 +88,4 @@ export default {
`[ADMIN] User ${interaction.user.tag} removed ${levelsToRemove} levels from ${targetUser.tag} in guild ${interaction.guildId}`
);
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Leveling/levelset.js b/src/commands/Leveling/modules/levelset.ts
similarity index 53%
rename from src/commands/Leveling/levelset.js
rename to src/commands/Leveling/modules/levelset.ts
index 4ce8589ac0..108454a5c9 100644
--- a/src/commands/Leveling/levelset.js
+++ b/src/commands/Leveling/modules/levelset.ts
@@ -1,25 +1,26 @@
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { checkUserPermissions } from '../../utils/permissionGuard.js';
-import { setUserLevel, getLevelingConfig } from '../../services/leveling/leveling.js';
-import { createEmbed } from '../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { checkUserPermissions } from '../../../utils/permissionGuard.js';
+import { setUserLevel, getLevelingConfig } from '../../../services/leveling/leveling.js';
+import { createEmbed } from '../../../utils/embeds.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('levelset')
- .setDescription("Set a user's level to a specific value")
+ .setDescription('Das Level eines Nutzers auf einen bestimmten Wert setzen')
.addUserOption((option) =>
option
.setName('user')
- .setDescription('The user to set the level for')
+ .setDescription('Der Nutzer, dessen Level gesetzt wird')
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName('level')
- .setDescription('The level to set')
+ .setDescription('Das Level, das gesetzt wird')
.setRequired(true)
.setMinValue(0)
)
@@ -33,42 +34,42 @@ export default {
const hasPermission = await checkUserPermissions(
interaction,
PermissionFlagsBits.ManageGuild,
- 'You need ManageGuild permission to use this command.'
+ 'Du brauchst die Berechtigung „Server verwalten“, um diesen Command zu nutzen.'
);
if (!hasPermission) return;
- const levelingConfig = await getLevelingConfig(client, interaction.guildId);
+ const levelingConfig = await getLevelingConfig(client, interaction.guildId!);
if (!levelingConfig?.enabled) {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
new EmbedBuilder()
.setColor('#f1c40f')
- .setDescription('The leveling system is currently disabled on this server.')
+ .setDescription('Das Leveling-System ist auf diesem Server derzeit deaktiviert.')
],
flags: MessageFlags.Ephemeral
});
return;
}
- const targetUser = interaction.options.getUser('user');
- const newLevel = interaction.options.getInteger('level');
+ const targetUser = interaction.options.getUser('user')!;
+ const newLevel = interaction.options.getInteger('level')!;
- const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null);
+ const member = await interaction.guild!.members.fetch(targetUser.id).catch(() => null);
if (!member) {
- throw new TitanBotError(
+ throw new NexoBotError(
`User ${targetUser.id} not found in this guild`,
ErrorTypes.USER_INPUT,
- 'The specified user is not in this server.'
+ 'Der angegebene Nutzer ist nicht auf diesem Server.'
);
}
- const userData = await setUserLevel(client, interaction.guildId, targetUser.id, newLevel);
+ const userData = await setUserLevel(client, interaction.guildId!, targetUser.id, newLevel);
await InteractionHelper.safeEditReply(interaction, {
embeds: [
createEmbed({
- title: 'Level Set',
- description: `Successfully set ${targetUser.tag}'s level to **${newLevel}**.\n**Total XP:** ${userData.totalXp}`,
+ title: 'Level gesetzt',
+ description: `Das Level von ${targetUser.tag} ist jetzt **${newLevel}**.\n**Gesamt-XP:** ${userData.totalXp}`,
color: 'success'
})
]
@@ -78,4 +79,4 @@ export default {
`[ADMIN] User ${interaction.user.tag} set ${targetUser.tag}'s level to ${newLevel} in guild ${interaction.guildId}`
);
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Leveling/rank.js b/src/commands/Leveling/modules/rank.ts
similarity index 69%
rename from src/commands/Leveling/rank.js
rename to src/commands/Leveling/modules/rank.ts
index fea894105c..db563cd17b 100644
--- a/src/commands/Leveling/rank.js
+++ b/src/commands/Leveling/modules/rank.ts
@@ -1,17 +1,18 @@
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from 'discord.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { getUserLevelData, getLevelingConfig, getXpForLevel } from '../../services/leveling/leveling.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { getUserLevelData, getLevelingConfig, getXpForLevel } from '../../../services/leveling/leveling.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('rank')
- .setDescription("Check your or another user's rank and level")
+ .setDescription('Zeigt deinen Rang und dein Level oder die eines anderen Nutzers')
.addUserOption((option) =>
option
.setName('user')
- .setDescription('The user to check the rank of')
+ .setDescription('Der Nutzer, dessen Rang du sehen willst')
.setRequired(false)
)
.setDMPermission(false),
@@ -20,13 +21,13 @@ export default {
async execute(interaction, config, client) {
await InteractionHelper.safeDefer(interaction);
- const levelingConfig = await getLevelingConfig(client, interaction.guildId);
+ const levelingConfig = await getLevelingConfig(client, interaction.guildId!);
if (!levelingConfig?.enabled) {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
new EmbedBuilder()
.setColor('#f1c40f')
- .setDescription('The leveling system is currently disabled on this server.')
+ .setDescription('Das Leveling-System ist auf diesem Server derzeit deaktiviert.')
],
flags: MessageFlags.Ephemeral
});
@@ -34,19 +35,19 @@ export default {
}
const targetUser = interaction.options.getUser('user') || interaction.user;
- const member = await interaction.guild.members
+ const member = await interaction.guild!.members
.fetch(targetUser.id)
.catch(() => null);
if (!member) {
- throw new TitanBotError(
+ throw new NexoBotError(
`User ${targetUser.id} not found in guild`,
ErrorTypes.USER_INPUT,
- 'Could not find the specified user in this server.'
+ 'Der angegebene Nutzer wurde auf diesem Server nicht gefunden.'
);
}
- const userData = await getUserLevelData(client, interaction.guildId, targetUser.id);
+ const userData = await getUserLevelData(client, interaction.guildId!, targetUser.id);
const safeUserData = {
level: userData?.level ?? 0,
@@ -59,8 +60,8 @@ export default {
const progressBar = createProgressBar(progress, 20);
const embed = new EmbedBuilder()
- .setTitle(`${member.displayName}'s Rank`)
- .setThumbnail(member.displayAvatarURL({ dynamic: true }))
+ .setTitle(`Rang von ${member.displayName}`)
+ .setThumbnail(member.displayAvatarURL())
.addFields(
{
name: 'Level',
@@ -73,12 +74,12 @@ export default {
inline: true
},
{
- name: 'Total XP',
+ name: 'Gesamt-XP',
value: safeUserData.totalXp.toString(),
inline: true
},
{
- name: `Progress to Level ${safeUserData.level + 1}`,
+ name: `Fortschritt bis Level ${safeUserData.level + 1}`,
value: `${progressBar} ${progress}%`
}
)
@@ -88,9 +89,9 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
logger.debug(`Rank checked for user ${targetUser.id} in guild ${interaction.guildId}`);
}
-};
+} satisfies Command;
-function createProgressBar(percentage, length = 10) {
+function createProgressBar(percentage: number, length = 10): string {
if (percentage < 0 || percentage > 100) {
percentage = Math.max(0, Math.min(100, percentage));
}
diff --git a/src/commands/Leveling/rank.ts b/src/commands/Leveling/rank.ts
new file mode 100644
index 0000000000..a711526025
--- /dev/null
+++ b/src/commands/Leveling/rank.ts
@@ -0,0 +1,13 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import rankCommand from './modules/rank.js';
+import leaderboardCommand from './modules/leaderboard.js';
+
+export default createRouterCommand({
+ name: 'rank',
+ description: 'Ränge und die Level-Bestenliste anzeigen',
+ category: 'Leveling',
+ routes: [
+ { legacyName: 'rank', module: rankCommand },
+ { legacyName: 'leaderboard', module: leaderboardCommand },
+ ],
+});
diff --git a/src/commands/Logging/logging.js b/src/commands/Logging/logging.ts
similarity index 67%
rename from src/commands/Logging/logging.js
rename to src/commands/Logging/logging.ts
index 0d94386314..4201658ce9 100644
--- a/src/commands/Logging/logging.js
+++ b/src/commands/Logging/logging.ts
@@ -1,3 +1,4 @@
+import type { Command } from '../../types/index.js';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType } from 'discord.js';
import { logger } from '../../utils/logger.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
@@ -9,40 +10,40 @@ import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName('logging')
- .setDescription('Manage server logging — channels, filters, and event categories.')
+ .setDescription('Server-Logging verwalten – Kanäle, Filter und Event-Kategorien.')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.setDMPermission(false)
.addSubcommand((subcommand) =>
subcommand
.setName('dashboard')
- .setDescription('Open the logging dashboard — set channels, filters, and toggle categories.'),
+ .setDescription('Logging-Dashboard öffnen – Kanäle, Filter und Kategorien einstellen.'),
)
.addSubcommand((subcommand) =>
subcommand
.setName('channel')
- .setDescription('Quick-set a log channel without opening the dashboard.')
+ .setDescription('Log-Kanal schnell setzen, ohne das Dashboard zu öffnen.')
.addStringOption((option) =>
option
.setName('destination')
- .setDescription('Which log destination to configure.')
+ .setDescription('Welches Log-Ziel du konfigurieren willst.')
.setRequired(true)
.addChoices(
- { name: 'Audit (moderation, messages, members…)', value: 'audit' },
- { name: 'Applications', value: 'applications' },
- { name: 'Reports', value: 'reports' },
+ { name: 'Audit (Moderation, Nachrichten, Mitglieder…)', value: 'audit' },
+ { name: 'Bewerbungen', value: 'applications' },
+ { name: 'Meldungen', value: 'reports' },
),
)
.addChannelOption((option) =>
option
.setName('channel')
- .setDescription('The text channel for logs.')
+ .setDescription('Der Textkanal für Logs.')
.addChannelTypes(ChannelType.GuildText)
.setRequired(false),
)
.addBooleanOption((option) =>
option
.setName('disable')
- .setDescription('Set to True to clear this log channel.')
+ .setDescription('Auf True setzen, um diesen Log-Kanal zu entfernen.')
.setRequired(false),
),
),
@@ -59,10 +60,10 @@ export default {
return await channel.execute(interaction, config, client);
}
- await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'This subcommand is not recognised.' });
+ await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Dieser Subcommand ist unbekannt.' });
} catch (error) {
logger.error('logging command error:', error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An unexpected error occurred.' }).catch(() => {});
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Ein unerwarteter Fehler ist aufgetreten.' }).catch(() => {});
}
},
-};
+} satisfies Command;
diff --git a/src/commands/Logging/modules/logging_channel.js b/src/commands/Logging/modules/logging_channel.ts
similarity index 51%
rename from src/commands/Logging/modules/logging_channel.js
rename to src/commands/Logging/modules/logging_channel.ts
index f92e6a6d3b..01c916ca29 100644
--- a/src/commands/Logging/modules/logging_channel.js
+++ b/src/commands/Logging/modules/logging_channel.ts
@@ -1,60 +1,61 @@
-import { PermissionsBitField, ChannelType } from 'discord.js';
+import type { Command, SubcommandModule } from '../../../types/index.js';
+import { PermissionsBitField, ChannelType, type GuildMember, type TextChannel } from 'discord.js';
import { setLogChannel } from '../../../services/loggingService.js';
import { successEmbed } from '../../../utils/embeds.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { logger } from '../../../utils/logger.js';
import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
-const DESTINATION_LABELS = {
- audit: 'Audit Log',
- applications: 'Applications',
- reports: 'Reports',
+const DESTINATION_LABELS: Record = {
+ audit: 'Audit-Log',
+ applications: 'Bewerbungen',
+ reports: 'Meldungen',
};
export default {
prefixOnly: false,
async execute(interaction, config, client) {
try {
- if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Server** permissions to configure logging channels.' });
+ if (!(interaction.member as GuildMember).permissions.has(PermissionsBitField.Flags.ManageGuild)) {
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um Log-Kanäle einzustellen.' });
}
await InteractionHelper.safeDefer(interaction, { ephemeral: true });
- const destination = interaction.options.getString('destination');
+ const destination = interaction.options.getString('destination')!;
const channel = interaction.options.getChannel('channel');
const disable = interaction.options.getBoolean('disable') ?? false;
if (disable) {
- await setLogChannel(client, interaction.guildId, destination, null);
+ await setLogChannel(client, interaction.guildId!, destination, null);
return InteractionHelper.safeEditReply(interaction, {
embeds: [successEmbed(
- 'Channel Cleared',
- `The **${DESTINATION_LABELS[destination]}** channel has been removed.`,
+ 'Kanal entfernt',
+ `Der Kanal für **${DESTINATION_LABELS[destination]}** wurde entfernt.`,
)],
});
}
if (!channel || channel.type !== ChannelType.GuildText) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please provide a valid text channel.' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte gib einen gültigen Textkanal an.' });
}
- const botPerms = channel.permissionsFor(interaction.guild.members.me);
+ const botPerms = (channel as TextChannel).permissionsFor(interaction.guild!.members.me!);
if (!botPerms?.has(['ViewChannel', 'SendMessages', 'EmbedLinks'])) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: `I need **View Channel**, **Send Messages**, and **Embed Links** in ${channel}.` });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: `Ich brauche **Kanal ansehen**, **Nachrichten senden** und **Links einbetten** in ${channel}.` });
}
- await setLogChannel(client, interaction.guildId, destination, channel.id);
+ await setLogChannel(client, interaction.guildId!, destination, channel.id);
return InteractionHelper.safeEditReply(interaction, {
embeds: [successEmbed(
- 'Channel Updated',
- `**${DESTINATION_LABELS[destination]}** logs will be sent to ${channel}.\nUse \`/logging dashboard\` to toggle event categories.`,
+ 'Kanal aktualisiert',
+ `Logs für **${DESTINATION_LABELS[destination]}** werden an ${channel} gesendet.\nNutze \`/logging dashboard\`, um Event-Kategorien umzuschalten.`,
)],
});
} catch (error) {
logger.error('logging_channel error:', error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Failed to update the log channel.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Der Log-Kanal konnte nicht aktualisiert werden.' });
}
},
-};
+} satisfies SubcommandModule & Pick;
diff --git a/src/commands/Logging/modules/logging_dashboard.js b/src/commands/Logging/modules/logging_dashboard.ts
similarity index 53%
rename from src/commands/Logging/modules/logging_dashboard.js
rename to src/commands/Logging/modules/logging_dashboard.ts
index f317698ccf..f49c27d01a 100644
--- a/src/commands/Logging/modules/logging_dashboard.js
+++ b/src/commands/Logging/modules/logging_dashboard.ts
@@ -1,4 +1,5 @@
-import { EmbedBuilder, MessageFlags, PermissionsBitField } from 'discord.js';
+import type { Command, NexoClient, SubcommandModule } from '../../../types/index.js';
+import { EmbedBuilder, MessageFlags, PermissionsBitField, type BaseInteraction, type Guild, type GuildMember, type MessageComponentInteraction, type ModalSubmitInteraction } from 'discord.js';
import { getColor } from '../../../config/bot.js';
import { getGuildConfig } from '../../../services/config/guildConfig.js';
import { getLoggingStatus } from '../../../services/loggingService.js';
@@ -9,12 +10,17 @@ import {
DASHBOARD_CATEGORIES,
DASHBOARD_CATEGORY_LABELS,
EVENT_TYPES_BY_CATEGORY,
+ type EnabledEventsMap,
} from '../../../utils/logging/loggingUi.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { logger } from '../../../utils/logger.js';
import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
-export function getCategoryStatus(enabledEvents, category, auditEnabled) {
+
+/** Interactions whose source message is the dashboard (buttons, selects, modals opened from it). */
+type DashboardMessageInteraction = MessageComponentInteraction | ModalSubmitInteraction;
+
+export function getCategoryStatus(enabledEvents: EnabledEventsMap | null | undefined, category: string, auditEnabled: boolean): boolean {
if (!auditEnabled) return false;
const events = enabledEvents || {};
if (events[`${category}.*`] === false) return false;
@@ -23,82 +29,82 @@ export function getCategoryStatus(enabledEvents, category, auditEnabled) {
return categoryEvents.every((eventType) => events[eventType] !== false);
}
-async function formatChannelMention(guild, id) {
- if (!id) return '`Not configured`';
+async function formatChannelMention(guild: Guild, id: string | null | undefined): Promise {
+ if (!id) return '`Nicht konfiguriert`';
const channel = guild.channels.cache.get(id) ?? await guild.channels.fetch(id).catch(() => null);
- return channel ? channel.toString() : `⚠️ Missing (${id})`;
+ return channel ? channel.toString() : `⚠️ Fehlt (${id})`;
}
-function countEnabledCategories(enabledEvents, auditEnabled) {
+function countEnabledCategories(enabledEvents: EnabledEventsMap | null | undefined, auditEnabled: boolean) {
const enabled = DASHBOARD_CATEGORIES.filter((key) =>
getCategoryStatus(enabledEvents, key, auditEnabled),
).length;
return { enabled, total: DASHBOARD_CATEGORIES.length };
}
-export async function buildLoggingDashboardView(interaction, client) {
- const guildConfig = await getGuildConfig(client, interaction.guildId);
- const loggingStatus = await getLoggingStatus(client, interaction.guildId);
+export async function buildLoggingDashboardView(interaction: BaseInteraction, client: NexoClient) {
+ const guildConfig = await getGuildConfig(client, interaction.guildId!);
+ const loggingStatus = await getLoggingStatus(client, interaction.guildId!);
const auditEnabled = Boolean(loggingStatus.enabled);
const channels = loggingStatus.channels || {};
- const auditChannel = await formatChannelMention(interaction.guild, channels.audit);
- const applicationsChannel = await formatChannelMention(interaction.guild, channels.applications);
- const reportsChannel = await formatChannelMention(interaction.guild, channels.reports);
- const lifecycleChannel = await formatChannelMention(interaction.guild, guildConfig.ticketLogsChannelId);
- const transcriptChannel = await formatChannelMention(interaction.guild, guildConfig.ticketTranscriptChannelId);
+ const auditChannel = await formatChannelMention(interaction.guild!, channels.audit);
+ const applicationsChannel = await formatChannelMention(interaction.guild!, channels.applications);
+ const reportsChannel = await formatChannelMention(interaction.guild!, channels.reports);
+ const lifecycleChannel = await formatChannelMention(interaction.guild!, guildConfig.ticketLogsChannelId);
+ const transcriptChannel = await formatChannelMention(interaction.guild!, guildConfig.ticketTranscriptChannelId);
const ignore = loggingStatus.ignore || { users: [], channels: [] };
const { enabled: enabledCount, total } = countEnabledCategories(loggingStatus.enabledEvents, auditEnabled);
const embed = new EmbedBuilder()
- .setTitle('📝 Logging Dashboard')
- .setDescription(`Manage server logging for **${interaction.guild.name}**. Use the menu below to configure channels, categories, and filters.`)
+ .setTitle('📝 Logging-Dashboard')
+ .setDescription(`Verwalte das Server-Logging für **${interaction.guild!.name}**. Nutze das Menü unten, um Kanäle, Kategorien und Filter einzustellen.`)
.setColor(auditEnabled ? getColor('success') : getColor('warning'))
.addFields(
{
- name: 'Logging Status',
- value: auditEnabled ? '✅ Enabled' : '❌ Disabled',
+ name: 'Logging-Status',
+ value: auditEnabled ? '✅ Aktiviert' : '❌ Deaktiviert',
inline: true,
},
{
- name: 'Event Categories',
- value: auditEnabled ? `${enabledCount}/${total} enabled` : '`Logging disabled`',
+ name: 'Event-Kategorien',
+ value: auditEnabled ? `${enabledCount}/${total} aktiviert` : '`Logging deaktiviert`',
inline: true,
},
{
- name: 'Ignore Filters',
- value: `${ignore.users?.length || 0} users · ${ignore.channels?.length || 0} channels`,
+ name: 'Ignorierfilter',
+ value: `${ignore.users?.length || 0} Nutzer · ${ignore.channels?.length || 0} Kanäle`,
inline: true,
},
{
- name: 'Log Channels',
+ name: 'Log-Kanäle',
value: [
`**Audit:** ${auditChannel}`,
- `**Applications:** ${applicationsChannel}`,
- `**Reports:** ${reportsChannel}`,
+ `**Bewerbungen:** ${applicationsChannel}`,
+ `**Meldungen:** ${reportsChannel}`,
].join('\n'),
inline: false,
},
{
- name: 'Ticket Channels (read-only)',
+ name: 'Ticket-Kanäle (nur lesen)',
value: [
- `**Ticket Logs:** ${lifecycleChannel}`,
- `**Transcripts:** ${transcriptChannel}`,
+ `**Ticket-Logs:** ${lifecycleChannel}`,
+ `**Transkripte:** ${transcriptChannel}`,
].join('\n'),
inline: false,
},
)
- .setFooter({ text: 'Ticket channels: configure via /ticket dashboard' })
+ .setFooter({ text: 'Ticket-Kanäle: über /ticket dashboard einstellen' })
.setTimestamp();
const components = createLoggingDashboardComponents(loggingStatus.enabledEvents, auditEnabled);
return { embed, components };
}
-export async function buildLoggingCategoriesView(interaction, client) {
- const loggingStatus = await getLoggingStatus(client, interaction.guildId);
+export async function buildLoggingCategoriesView(interaction: BaseInteraction, client: NexoClient) {
+ const loggingStatus = await getLoggingStatus(client, interaction.guildId!);
const auditEnabled = Boolean(loggingStatus.enabled);
const categoryLines = DASHBOARD_CATEGORIES.map((key) => {
@@ -108,57 +114,57 @@ export async function buildLoggingCategoriesView(interaction, client) {
}).join('\n');
const embed = new EmbedBuilder()
- .setTitle('📋 Event Categories')
+ .setTitle('📋 Event-Kategorien')
.setDescription(
auditEnabled
- ? 'Toggle which types of events are logged to your audit channel.'
- : '⚠️ Logging is disabled. Enable it from the main dashboard to send logs.',
+ ? 'Leg fest, welche Event-Arten in deinem Audit-Kanal geloggt werden.'
+ : '⚠️ Logging ist deaktiviert. Aktivier es im Haupt-Dashboard, damit Logs gesendet werden.',
)
.setColor(getColor('info'))
- .addFields({ name: 'Category Status', value: categoryLines, inline: false })
- .setFooter({ text: 'Green = logging on · Red = logging off' })
+ .addFields({ name: 'Kategorie-Status', value: categoryLines, inline: false })
+ .setFooter({ text: 'Grün = Logging an · Rot = Logging aus' })
.setTimestamp();
const components = createLoggingCategoryViewComponents(loggingStatus.enabledEvents, auditEnabled);
return { embed, components };
}
-export async function buildLoggingFilterView(interaction, client) {
- const loggingStatus = await getLoggingStatus(client, interaction.guildId);
+export async function buildLoggingFilterView(interaction: BaseInteraction, client: NexoClient) {
+ const loggingStatus = await getLoggingStatus(client, interaction.guildId!);
const ignore = loggingStatus.ignore || { users: [], channels: [] };
const userLines = (ignore.users || []).length
- ? ignore.users.map((id) => `• User \`${id}\``).join('\n')
- : '*No ignored users*';
+ ? ignore.users.map((id) => `• Nutzer \`${id}\``).join('\n')
+ : '*Keine ignorierten Nutzer*';
const channelLines = (ignore.channels || []).length
- ? ignore.channels.map((id) => `• Channel \`${id}\``).join('\n')
- : '*No ignored channels*';
+ ? ignore.channels.map((id) => `• Kanal \`${id}\``).join('\n')
+ : '*Keine ignorierten Kanäle*';
const embed = new EmbedBuilder()
- .setTitle('🔇 Log Ignore Filters')
- .setDescription('Users and channels on this list will be skipped when sending audit logs.')
+ .setTitle('🔇 Log-Ignorierfilter')
+ .setDescription('Nutzer und Kanäle auf dieser Liste werden beim Senden von Audit-Logs übersprungen.')
.setColor(getColor('info'))
.addFields(
- { name: 'Ignored Users', value: userLines.slice(0, 1024), inline: false },
- { name: 'Ignored Channels', value: channelLines.slice(0, 1024), inline: false },
+ { name: 'Ignorierte Nutzer', value: userLines.slice(0, 1024), inline: false },
+ { name: 'Ignorierte Kanäle', value: channelLines.slice(0, 1024), inline: false },
)
- .setFooter({ text: 'Use the buttons below to add or remove filters' })
+ .setFooter({ text: 'Nutze die Buttons unten, um Filter hinzuzufügen oder zu entfernen' })
.setTimestamp();
const components = createLoggingFilterComponents();
return { embed, components };
}
-export function isCategoriesView(interaction) {
- return interaction.message?.embeds?.[0]?.title === '📋 Event Categories';
+export function isCategoriesView(interaction: DashboardMessageInteraction): boolean {
+ return interaction.message?.embeds?.[0]?.title === '📋 Event-Kategorien';
}
-export function isFilterView(interaction) {
- return interaction.message?.embeds?.[0]?.title === '🔇 Log Ignore Filters';
+export function isFilterView(interaction: DashboardMessageInteraction): boolean {
+ return interaction.message?.embeds?.[0]?.title === '🔇 Log-Ignorierfilter';
}
-export async function refreshDashboardMessage(interaction, client) {
+export async function refreshDashboardMessage(interaction: DashboardMessageInteraction, client: NexoClient): Promise {
let view;
if (isCategoriesView(interaction)) {
view = await buildLoggingCategoriesView(interaction, client);
@@ -168,7 +174,7 @@ export async function refreshDashboardMessage(interaction, client) {
view = await buildLoggingDashboardView(interaction, client);
}
- await interaction.message.edit({
+ await interaction.message!.edit({
embeds: [view.embed],
components: view.components,
content: null,
@@ -179,8 +185,8 @@ export default {
prefixOnly: false,
async execute(interaction, config, client) {
try {
- if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Server** permissions to view the logging dashboard.' });
+ if (!(interaction.member as GuildMember).permissions.has(PermissionsBitField.Flags.ManageGuild)) {
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um das Logging-Dashboard zu sehen.' });
}
await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
@@ -188,7 +194,7 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed], components });
} catch (error) {
logger.error('logging_dashboard error:', error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Failed to load the logging dashboard.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Das Logging-Dashboard konnte nicht geladen werden.' });
}
},
-};
+} satisfies SubcommandModule & Pick;
diff --git a/src/commands/Moderation/ban.ts b/src/commands/Moderation/ban.ts
new file mode 100644
index 0000000000..f75359192f
--- /dev/null
+++ b/src/commands/Moderation/ban.ts
@@ -0,0 +1,17 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import banCommand from './modules/ban.js';
+import unbanCommand from './modules/unban.js';
+import massbanCommand from './modules/massban.js';
+
+export default createRouterCommand({
+ name: 'ban',
+ description: 'Mitglieder bannen, entbannen und massenhaft bannen',
+ category: 'Moderation',
+ defaultMemberPermissions: PermissionFlagsBits.BanMembers,
+ routes: [
+ { legacyName: 'ban', module: banCommand },
+ { legacyName: 'unban', module: unbanCommand },
+ { legacyName: 'massban', module: massbanCommand },
+ ],
+});
diff --git a/src/commands/Moderation/cases.js b/src/commands/Moderation/cases.ts
similarity index 76%
rename from src/commands/Moderation/cases.js
rename to src/commands/Moderation/cases.ts
index 5999cd834d..03c816c515 100644
--- a/src/commands/Moderation/cases.js
+++ b/src/commands/Moderation/cases.ts
@@ -1,3 +1,4 @@
+import type { Command } from '../../types/index.js';
import { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } from 'discord.js';
import { createEmbed, successEmbed } from '../../utils/embeds.js';
import { getModerationCases } from '../../utils/moderation.js';
@@ -7,27 +8,27 @@ import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName('cases')
- .setDescription('View moderation cases and audit logs')
+ .setDescription('Moderationsfälle und Audit-Logs ansehen')
.setDefaultMemberPermissions(PermissionFlagsBits.ViewAuditLog)
.setDMPermission(false)
.addStringOption(option =>
option.setName('filter')
- .setDescription('Filter cases by type or user')
+ .setDescription('Fälle nach Typ oder Nutzer filtern')
.addChoices(
- { name: 'All Cases', value: 'all' },
- { name: 'Bans', value: 'Member Banned' },
+ { name: 'Alle Fälle', value: 'all' },
+ { name: 'Banns', value: 'Member Banned' },
{ name: 'Kicks', value: 'Member Kicked' },
{ name: 'Timeouts', value: 'Member Timed Out' },
- { name: 'Warnings', value: 'User Warned' }
+ { name: 'Verwarnungen', value: 'User Warned' }
)
)
.addUserOption(option =>
option.setName('user')
- .setDescription('Filter cases by specific user')
+ .setDescription('Fälle nach einem bestimmten Nutzer filtern')
)
.addIntegerOption(option =>
option.setName('limit')
- .setDescription('Number of cases to show (default: 10)')
+ .setDescription('Anzahl der angezeigten Fälle (Standard: 10)')
.setMinValue(1)
.setMaxValue(50)
),
@@ -56,7 +57,7 @@ export default {
userId: targetUser?.id
};
- const cases = await getModerationCases(interaction.guild.id, filters);
+ const cases = await getModerationCases(interaction.guild!.id, filters);
if (cases.length === 0) {
throw new Error(targetUser
@@ -69,14 +70,14 @@ export default {
const totalPages = Math.ceil(cases.length / CASES_PER_PAGE);
let currentPage = 1;
- const createCasesEmbed = (page) => {
+ const createCasesEmbed = (page: number) => {
const startIndex = (page - 1) * CASES_PER_PAGE;
const endIndex = startIndex + CASES_PER_PAGE;
const pageCases = cases.slice(startIndex, endIndex);
const embed = createEmbed({
- title: 'Moderation Cases',
- description: `Showing moderation cases for **${interaction.guild.name}**\n\n**Page ${page} of ${totalPages}**`
+ title: 'Moderationsfälle',
+ description: `Moderationsfälle für **${interaction.guild!.name}**\n\n**Seite ${page} von ${totalPages}**`
});
pageCases.forEach(case_ => {
@@ -84,37 +85,37 @@ export default {
const time = new Date(case_.createdAt).toLocaleTimeString();
embed.addFields({
- name: `Case #${case_.caseId} - ${case_.action}`,
- value: `**Target:** ${case_.target}\n**Moderator:** ${case_.executor}\n**Date:** ${date} at ${time}\n**Reason:** ${case_.reason || 'No reason provided'}`,
+ name: `Fall #${case_.caseId} - ${case_.action}`,
+ value: `**Ziel:** ${case_.target}\n**Moderator:** ${case_.executor}\n**Datum:** ${date} um ${time}\n**Grund:** ${case_.reason || 'Kein Grund angegeben'}`,
inline: false
});
});
embed.setFooter({
- text: `Total cases: ${cases.length} | Filter: ${filterType}${targetUser ?` | User: ${targetUser.tag}`: ''}`
+ text: `Fälle gesamt: ${cases.length} | Filter: ${filterType}${targetUser ?` | Nutzer: ${targetUser.tag}`: ''}`
});
return embed;
};
- const createNavigationRow = (page) => {
- const row = new ActionRowBuilder();
+ const createNavigationRow = (page: number) => {
+ const row = new ActionRowBuilder();
const prevButton = new ButtonBuilder()
.setCustomId('prev_page')
- .setLabel('⬅️ Previous')
+ .setLabel('⬅️ Vorherige')
.setStyle(ButtonStyle.Secondary)
.setDisabled(page === 1);
const pageInfoButton = new ButtonBuilder()
.setCustomId('page_info')
- .setLabel(`Page ${page}/${totalPages}`)
+ .setLabel(`Seite ${page}/${totalPages}`)
.setStyle(ButtonStyle.Primary)
.setDisabled(true);
const nextButton = new ButtonBuilder()
.setCustomId('next_page')
- .setLabel('Next ➡️')
+ .setLabel('Weiter ➡️')
.setStyle(ButtonStyle.Secondary)
.setDisabled(page === totalPages);
@@ -137,7 +138,7 @@ time: 120000
if (buttonInteraction.user.id !== interaction.user.id) {
await buttonInteraction.followUp({
- content: 'You cannot use these buttons. Run `/cases` to get your own case view.',
+ content: 'Du kannst diese Buttons nicht nutzen. Nutze `/cases`, um deine eigene Fallansicht zu öffnen.',
flags: MessageFlags.Ephemeral
});
return;
@@ -171,7 +172,7 @@ time: 120000
} catch (error) {
logger.error('Error in cases command:', error);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while retrieving moderation cases. Please try again later.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Laden der Moderationsfälle ist ein Fehler aufgetreten. Bitte versuch es später noch einmal.' });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Moderation/channel.ts b/src/commands/Moderation/channel.ts
new file mode 100644
index 0000000000..e9c0bb57f7
--- /dev/null
+++ b/src/commands/Moderation/channel.ts
@@ -0,0 +1,15 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import lockCommand from './modules/lock.js';
+import unlockCommand from './modules/unlock.js';
+
+export default createRouterCommand({
+ name: 'channel',
+ description: 'Kanäle sperren und entsperren',
+ category: 'Moderation',
+ defaultMemberPermissions: PermissionFlagsBits.ManageChannels,
+ routes: [
+ { legacyName: 'lock', module: lockCommand },
+ { legacyName: 'unlock', module: unlockCommand },
+ ],
+});
diff --git a/src/commands/Moderation/kick.ts b/src/commands/Moderation/kick.ts
new file mode 100644
index 0000000000..945b1f1cf1
--- /dev/null
+++ b/src/commands/Moderation/kick.ts
@@ -0,0 +1,15 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import kickCommand from './modules/kick.js';
+import masskickCommand from './modules/masskick.js';
+
+export default createRouterCommand({
+ name: 'kick',
+ description: 'Mitglieder kicken',
+ category: 'Moderation',
+ defaultMemberPermissions: PermissionFlagsBits.KickMembers,
+ routes: [
+ { legacyName: 'kick', module: kickCommand },
+ { legacyName: 'masskick', module: masskickCommand },
+ ],
+});
diff --git a/src/commands/Moderation/mod.ts b/src/commands/Moderation/mod.ts
new file mode 100644
index 0000000000..6df4a5e5f6
--- /dev/null
+++ b/src/commands/Moderation/mod.ts
@@ -0,0 +1,21 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import timeoutCommand from './modules/timeout.js';
+import untimeoutCommand from './modules/untimeout.js';
+import warnCommand from './modules/warn.js';
+import warningsCommand from './modules/warnings.js';
+import dmCommand from './modules/dm.js';
+
+export default createRouterCommand({
+ name: 'mod',
+ description: 'Timeouts, Verwarnungen und Moderator-DMs',
+ category: 'Moderation',
+ defaultMemberPermissions: PermissionFlagsBits.ModerateMembers,
+ routes: [
+ { legacyName: 'timeout', module: timeoutCommand },
+ { legacyName: 'untimeout', module: untimeoutCommand },
+ { legacyName: 'warn', module: warnCommand },
+ { legacyName: 'warnings', module: warningsCommand },
+ { legacyName: 'dm', module: dmCommand },
+ ],
+});
diff --git a/src/commands/Moderation/ban.js b/src/commands/Moderation/modules/ban.ts
similarity index 51%
rename from src/commands/Moderation/ban.js
rename to src/commands/Moderation/modules/ban.ts
index 86dd778858..7f8f8b118d 100644
--- a/src/commands/Moderation/ban.js
+++ b/src/commands/Moderation/modules/ban.ts
@@ -1,67 +1,68 @@
-import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { ModerationService } from '../../services/moderation/moderationService.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, type GuildMember } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { ModerationService } from '../../../services/moderation/moderationService.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName("ban")
- .setDescription("Ban a user from the server")
+ .setDescription("Einen Nutzer vom Server bannen")
.addUserOption((option) =>
option
.setName("target")
- .setDescription("The user to ban")
+ .setDescription("Der Nutzer, der gebannt werden soll")
.setRequired(true),
)
.addStringOption((option) =>
- option.setName("reason").setDescription("Reason for the ban"),
+ option.setName("reason").setDescription("Grund für den Bann"),
)
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
category: "moderation",
async execute(interaction, config, client) {
const user = interaction.options.getUser("target");
- const reason = interaction.options.getString("reason") || "No reason provided";
+ const reason = interaction.options.getString("reason") || "Kein Grund angegeben";
if (!user) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Missing target user',
ErrorTypes.USER_INPUT,
- 'You must specify a user to ban.',
+ 'Du musst einen Nutzer zum Bannen angeben.',
{ subtype: 'invalid_user' },
);
}
if (user.id === interaction.user.id) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Cannot ban self',
ErrorTypes.VALIDATION,
- 'You cannot ban yourself.',
+ 'Du kannst dich nicht selbst bannen.',
);
}
if (user.id === client.user.id) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Cannot ban bot',
ErrorTypes.VALIDATION,
- 'You cannot ban the bot.',
+ 'Du kannst den Bot nicht bannen.',
);
}
const result = await ModerationService.banUser({
- guild: interaction.guild,
+ guild: interaction.guild!,
user,
- moderator: interaction.member,
+ moderator: interaction.member as GuildMember,
reason,
});
await InteractionHelper.universalReply(interaction, {
embeds: [
successEmbed(
- `🚫 **Banned** ${user.tag}`,
- `**Reason:** ${reason}\n**Case ID:** #${result.caseId}`,
+ `🚫 **Gebannt:** ${user.tag}`,
+ `**Grund:** ${reason}\n**Fall-ID:** #${result.caseId}`,
),
],
});
},
-};
+} satisfies Command;
diff --git a/src/commands/Moderation/dm.js b/src/commands/Moderation/modules/dm.ts
similarity index 64%
rename from src/commands/Moderation/dm.js
rename to src/commands/Moderation/modules/dm.ts
index da9714f18a..989dbc24d9 100644
--- a/src/commands/Moderation/dm.js
+++ b/src/commands/Moderation/modules/dm.ts
@@ -1,31 +1,32 @@
+import type { Command } from '../../../types/index.js';
import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logEvent } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { sanitizeMarkdown } from '../../utils/validation.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logEvent } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { sanitizeMarkdown } from '../../../utils/validation.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { replyUserError, ErrorTypes, type ErrorLike } from '../../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName("dm")
- .setDescription("Send a direct message to a user (Staff only)")
+ .setDescription("Einem Nutzer eine Direktnachricht senden (nur Team)")
.addUserOption(option =>
option
.setName("user")
- .setDescription("The user to send a DM to")
+ .setDescription("Der Nutzer, der die DM bekommen soll")
.setRequired(true)
)
.addStringOption(option =>
option
.setName("message")
- .setDescription("The message to send")
+ .setDescription("Die Nachricht, die gesendet werden soll")
.setRequired(true)
)
.addBooleanOption(option =>
option
.setName("anonymous")
- .setDescription("Send the message anonymously (default: false)")
+ .setDescription("Nachricht anonym senden (Standard: false)")
.setRequired(false)
)
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
@@ -43,18 +44,18 @@ export default {
return;
}
- const targetUser = interaction.options.getUser("user");
- const message = interaction.options.getString("message");
+ const targetUser = interaction.options.getUser("user")!;
+ const message = interaction.options.getString("message")!;
const anonymous = interaction.options.getBoolean("anonymous") || false;
try {
if (message.length > 2000) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Messages must be under 2000 characters.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Nachrichten müssen kürzer als 2000 Zeichen sein.' });
}
if (targetUser.bot) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You cannot send DMs to bot accounts.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du kannst Bot-Accounts keine DMs senden.' });
}
const sanitized = sanitizeMarkdown(message);
@@ -64,10 +65,10 @@ export default {
await dmChannel.send({
embeds: [
successEmbed(
- anonymous ? "Message from the Staff Team" : `Message from ${interaction.user.tag}`,
+ anonymous ? "Nachricht vom Team" : `Nachricht von ${interaction.user.tag}`,
sanitized
).setFooter({
- text: `You cannot reply to this message. | Logger ID: ${interaction.id}`
+ text: `Du kannst auf diese Nachricht nicht antworten. | Logger-ID: ${interaction.id}`
})
]
});
@@ -79,7 +80,7 @@ export default {
action: "DM Sent",
target: `${targetUser.tag} (${targetUser.id})`,
executor: `${interaction.user.tag} (${interaction.user.id})`,
- reason: `Anonymous: ${anonymous ? 'Yes' : 'No'}`,
+ reason: `Anonym: ${anonymous ? 'Ja' : 'Nein'}`,
metadata: {
userId: targetUser.id,
moderatorId: interaction.user.id,
@@ -92,19 +93,19 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- "DM Sent",
- `Successfully sent a message to ${targetUser.tag}`
+ "DM gesendet",
+ `Nachricht an ${targetUser.tag} erfolgreich gesendet`
),
],
});
} catch (error) {
logger.error('DM command error:', error);
-if (error.code === 50007) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Could not send a DM to ${targetUser.tag}. They may have DMs disabled.` });
+if ((error as ErrorLike).code === 50007) {
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Konnte ${targetUser.tag} keine DM senden. Eventuell sind DMs deaktiviert.` });
}
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Failed to send DM: ${error.message}` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `DM konnte nicht gesendet werden: ${(error as ErrorLike).message}` });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Moderation/kick.js b/src/commands/Moderation/modules/kick.ts
similarity index 52%
rename from src/commands/Moderation/kick.js
rename to src/commands/Moderation/modules/kick.ts
index 03fa9f677c..e4c3fe2480 100644
--- a/src/commands/Moderation/kick.js
+++ b/src/commands/Moderation/modules/kick.ts
@@ -1,21 +1,22 @@
-import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { ModerationService } from '../../services/moderation/moderationService.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, type GuildMember } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { ModerationService } from '../../../services/moderation/moderationService.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName("kick")
- .setDescription("Kick a user from the server")
+ .setDescription("Einen Nutzer vom Server kicken")
.addUserOption((option) =>
option
.setName("target")
- .setDescription("The user to kick")
+ .setDescription("Der Nutzer, der gekickt werden soll")
.setRequired(true),
)
.addStringOption((option) =>
- option.setName("reason").setDescription("Reason for the kick"),
+ option.setName("reason").setDescription("Grund für den Kick"),
)
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
category: "moderation",
@@ -23,56 +24,56 @@ export default {
async execute(interaction, config, client) {
const targetUser = interaction.options.getUser("target");
const member = interaction.options.getMember("target");
- const reason = interaction.options.getString("reason") || "No reason provided";
+ const reason = interaction.options.getString("reason") || "Kein Grund angegeben";
if (!targetUser) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Missing target user',
ErrorTypes.USER_INPUT,
- 'You must specify a user to kick.',
+ 'Du musst einen Nutzer zum Kicken angeben.',
{ subtype: 'invalid_user' },
);
}
if (targetUser.id === interaction.user.id) {
- throw new TitanBotError(
+ throw new NexoBotError(
"Cannot kick self",
ErrorTypes.VALIDATION,
- "You cannot kick yourself.",
+ "Du kannst dich nicht selbst kicken.",
);
}
if (targetUser.id === client.user.id) {
- throw new TitanBotError(
+ throw new NexoBotError(
"Cannot kick bot",
ErrorTypes.VALIDATION,
- "You cannot kick the bot.",
+ "Du kannst den Bot nicht kicken.",
);
}
if (!member) {
- throw new TitanBotError(
+ throw new NexoBotError(
"Target not found",
ErrorTypes.USER_INPUT,
- "The target user is not currently in this server.",
+ "Der Nutzer ist gerade nicht auf diesem Server.",
{ subtype: 'user_not_found' },
);
}
const result = await ModerationService.kickUser({
- guild: interaction.guild,
- member,
- moderator: interaction.member,
+ guild: interaction.guild!,
+ member: member as GuildMember,
+ moderator: interaction.member as GuildMember,
reason,
});
await InteractionHelper.universalReply(interaction, {
embeds: [
successEmbed(
- `👢 **Kicked** ${targetUser.tag}`,
- `**Reason:** ${reason}\n**Case ID:** #${result.caseId}`,
+ `👢 **Gekickt:** ${targetUser.tag}`,
+ `**Grund:** ${reason}\n**Fall-ID:** #${result.caseId}`,
),
],
});
},
-};
+} satisfies Command;
diff --git a/src/commands/Moderation/lock.js b/src/commands/Moderation/modules/lock.ts
similarity index 60%
rename from src/commands/Moderation/lock.js
rename to src/commands/Moderation/modules/lock.ts
index 75b251973f..1ca232fc8d 100644
--- a/src/commands/Moderation/lock.js
+++ b/src/commands/Moderation/modules/lock.ts
@@ -1,16 +1,17 @@
-import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logEvent } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { getColor } from '../../config/bot.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, type TextChannel } from 'discord.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logEvent } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { getColor } from '../../../config/bot.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName("lock")
.setDescription(
- "Locks the current channel (prevents @everyone from sending messages).",
+ "Sperrt den aktuellen Kanal (@everyone kann keine Nachrichten mehr senden).",
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels),
category: "moderation",
@@ -26,24 +27,24 @@ export default {
return;
}
- const channel = interaction.channel;
- const everyoneRole = interaction.guild.roles.everyone;
+ const channel = interaction.channel as TextChannel;
+ const everyoneRole = interaction.guild!.roles.everyone;
try {
const currentPermissions = channel.permissionsFor(everyoneRole);
if (currentPermissions.has(PermissionFlagsBits.SendMessages) === false) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `${channel} is already locked.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `${channel} ist bereits gesperrt.` });
}
await channel.permissionOverwrites.edit(
everyoneRole,
{ SendMessages: false },
-{ type: 0, reason: `Channel locked by ${interaction.user.tag}` },
+{ type: 0, reason: `Kanal gesperrt von ${interaction.user.tag}` },
);
await logEvent({
client,
- guild: interaction.guild,
+ guild: interaction.guild!,
event: {
action: "Channel Locked",
target: channel.toString(),
@@ -59,14 +60,14 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- `🔒 **Channel Locked**`,
- `${channel} is now locked down. No one can speak here now.`,
+ `🔒 **Kanal gesperrt**`,
+ `${channel} ist jetzt gesperrt. Hier kann niemand mehr schreiben.`,
),
],
});
} catch (error) {
logger.error('Lock command error:', error);
- await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'An unexpected error occurred while trying to lock the channel. Check my permissions (I need \'Manage Channels\').' });
+ await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Beim Sperren des Kanals ist ein unerwarteter Fehler aufgetreten. Prüf meine Berechtigungen (ich brauche „Kanäle verwalten“).' });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Moderation/massban.js b/src/commands/Moderation/modules/massban.ts
similarity index 66%
rename from src/commands/Moderation/massban.js
rename to src/commands/Moderation/modules/massban.ts
index 9f6f8637a4..86d267bd4d 100644
--- a/src/commands/Moderation/massban.js
+++ b/src/commands/Moderation/modules/massban.ts
@@ -1,30 +1,37 @@
-import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logModerationAction } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { ModerationService } from '../../services/moderation/moderationService.js';
-import { TitanBotError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, type GuildMember } from 'discord.js';
+import { createEmbed, successEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logModerationAction } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { ModerationService } from '../../../services/moderation/moderationService.js';
+import { NexoBotError, replyUserError, ErrorTypes, type ErrorLike } from '../../../utils/errorHandler.js';
+
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+interface MassActionResults {
+ successful: { user: string; userId: string }[];
+ failed: { userId: string; reason: string }[];
+ skipped: { user: string; userId: string; reason: string }[];
+}
+
export default {
data: new SlashCommandBuilder()
.setName("massban")
- .setDescription("Ban multiple users from the server at once")
+ .setDescription("Mehrere Nutzer auf einmal vom Server bannen")
.addStringOption(option =>
option
.setName("users")
- .setDescription("User IDs or mentions to ban (separated by spaces or commas)")
+ .setDescription("Nutzer-IDs oder Erwähnungen zum Bannen (durch Leerzeichen oder Kommas getrennt)")
.setRequired(true)
)
.addStringOption(option =>
option.setName("reason")
- .setDescription("Reason for the mass ban")
+ .setDescription("Grund für den Massenbann")
.setRequired(false)
)
.addIntegerOption(option =>
option
.setName("delete_days")
- .setDescription("Number of days of messages to delete (0-7)")
+ .setDescription("Wie viele Tage an Nachrichten gelöscht werden sollen (0-7)")
.setMinValue(0)
.setMaxValue(7)
.setRequired(false)
@@ -44,8 +51,8 @@ export default {
return;
}
- const usersInput = interaction.options.getString("users");
- const reason = interaction.options.getString("reason") || "Mass ban - No reason provided";
+ const usersInput = interaction.options.getString("users")!;
+ const reason = interaction.options.getString("reason") || "Massenbann – kein Grund angegeben";
const deleteDays = interaction.options.getInteger("delete_days") || 0;
try {
@@ -56,18 +63,18 @@ export default {
.slice(0, 20);
if (userIds.length === 0) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please provide valid user IDs or mentions. Maximum 20 users at once.' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte gib gültige Nutzer-IDs oder Erwähnungen an. Maximal 20 Nutzer auf einmal.' });
}
if (userIds.includes(interaction.user.id)) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You cannot include yourself in a mass ban.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du kannst dich nicht selbst in einen Massenbann aufnehmen.' });
}
if (userIds.includes(client.user.id)) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You cannot include the bot in a mass ban.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du kannst den Bot nicht in einen Massenbann aufnehmen.' });
}
- const results = {
+ const results: MassActionResults = {
successful: [],
failed: [],
skipped: []
@@ -78,19 +85,19 @@ export default {
const user = await client.users.fetch(userId).catch(() => null);
if (!user) {
- results.failed.push({ userId, reason: "User not found" });
+ results.failed.push({ userId, reason: "Nutzer nicht gefunden" });
continue;
}
- const member = await interaction.guild.members.fetch(userId).catch(() => null);
+ const member = await interaction.guild!.members.fetch(userId).catch(() => null);
if (member) {
- const modCheck = ModerationService.validateHierarchy(interaction.member, member, 'ban');
+ const modCheck = ModerationService.validateHierarchy(interaction.member as GuildMember, member, 'ban');
if (!modCheck.valid) {
results.skipped.push({
user: user.tag,
userId,
- reason: ModerationService.buildHierarchySkipReason(interaction.member, member, 'ban'),
+ reason: ModerationService.buildHierarchySkipReason(interaction.member as GuildMember, member, 'ban'),
});
continue;
}
@@ -100,13 +107,13 @@ export default {
results.skipped.push({
user: user.tag,
userId,
- reason: ModerationService.buildHierarchySkipReason(interaction.member, member, 'ban', 'bot'),
+ reason: ModerationService.buildHierarchySkipReason(interaction.member as GuildMember, member, 'ban', 'bot'),
});
continue;
}
}
- await interaction.guild.members.ban(userId, {
+ await interaction.guild!.members.ban(userId, {
reason: reason,
deleteMessageSeconds: deleteDays * 24 * 60 * 60
});
@@ -118,12 +125,12 @@ export default {
await logModerationAction({
client,
- guild: interaction.guild,
+ guild: interaction.guild!,
event: {
action: "Member Banned",
target: `${user.tag} (${user.id})`,
executor: `${interaction.user.tag} (${interaction.user.id})`,
- reason: `${reason} (Mass Ban)`,
+ reason: `${reason} (Massenbann)`,
metadata: {
userId: user.id,
moderatorId: interaction.user.id,
@@ -135,9 +142,9 @@ export default {
} catch (error) {
logger.error(`Failed to ban user ${userId}:`, error);
- const reason = error instanceof TitanBotError
+ const reason = error instanceof NexoBotError
? (error.userMessage || error.message)
- : (error.message || "Unknown error");
+ : ((error as ErrorLike).message || "Unbekannter Fehler");
results.failed.push({
userId,
reason,
@@ -145,10 +152,10 @@ export default {
}
}
- let description = `**Mass Ban Results:**\n\n`;
+ let description = `**Ergebnisse des Massenbanns:**\n\n`;
if (results.successful.length > 0) {
- description += `✅ **Successfully Banned (${results.successful.length}):**\n`;
+ description += `✅ **Erfolgreich gebannt (${results.successful.length}):**\n`;
results.successful.forEach(result => {
description += `• ${result.user} (${result.userId})\n`;
});
@@ -156,7 +163,7 @@ export default {
}
if (results.skipped.length > 0) {
- description += `⚠️ **Skipped (${results.skipped.length}):**\n`;
+ description += `⚠️ **Übersprungen (${results.skipped.length}):**\n`;
results.skipped.forEach(result => {
description += `• ${result.user} - ${result.reason}\n`;
});
@@ -164,7 +171,7 @@ export default {
}
if (results.failed.length > 0) {
- description += `❌ **Failed (${results.failed.length}):**\n`;
+ description += `❌ **Fehlgeschlagen (${results.failed.length}):**\n`;
results.failed.forEach(result => {
description += `• ${result.userId} - ${result.reason}\n`;
});
@@ -175,7 +182,7 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
embed(
- `🔨 Mass Ban Completed`,
+ `🔨 Massenbann abgeschlossen`,
description
)
]
@@ -183,7 +190,7 @@ export default {
} catch (error) {
logger.error("Error in massban command:", error);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while processing the mass ban. Please try again later.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Massenbann ist ein Fehler aufgetreten. Bitte versuch es später noch einmal.' });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Moderation/masskick.js b/src/commands/Moderation/modules/masskick.ts
similarity index 65%
rename from src/commands/Moderation/masskick.js
rename to src/commands/Moderation/modules/masskick.ts
index 26e7e21a48..57704922a1 100644
--- a/src/commands/Moderation/masskick.js
+++ b/src/commands/Moderation/modules/masskick.ts
@@ -1,24 +1,31 @@
-import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logModerationAction } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { ModerationService } from '../../services/moderation/moderationService.js';
-import { TitanBotError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, type GuildMember } from 'discord.js';
+import { createEmbed, successEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logModerationAction } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { ModerationService } from '../../../services/moderation/moderationService.js';
+import { NexoBotError, replyUserError, ErrorTypes, type ErrorLike } from '../../../utils/errorHandler.js';
+
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+interface MassActionResults {
+ successful: { user: string; userId: string }[];
+ failed: { userId: string; reason: string }[];
+ skipped: { user: string; userId: string; reason: string }[];
+}
+
export default {
data: new SlashCommandBuilder()
.setName("masskick")
- .setDescription("Kick multiple users from the server at once")
+ .setDescription("Mehrere Nutzer auf einmal vom Server kicken")
.addStringOption(option =>
option
.setName("users")
- .setDescription("User IDs or mentions to kick (separated by spaces or commas)")
+ .setDescription("Nutzer-IDs oder Erwähnungen zum Kicken (durch Leerzeichen oder Kommas getrennt)")
.setRequired(true)
)
.addStringOption(option =>
option.setName("reason")
- .setDescription("Reason for the mass kick")
+ .setDescription("Grund für den Massenkick")
.setRequired(false)
)
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
@@ -36,8 +43,8 @@ export default {
return;
}
- const usersInput = interaction.options.getString("users");
- const reason = interaction.options.getString("reason") || "Mass kick - No reason provided";
+ const usersInput = interaction.options.getString("users")!;
+ const reason = interaction.options.getString("reason") || "Massenkick – kein Grund angegeben";
try {
const userIds = usersInput
@@ -47,18 +54,18 @@ export default {
.slice(0, 20);
if (userIds.length === 0) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please provide valid user IDs or mentions. Maximum 20 users at once.' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte gib gültige Nutzer-IDs oder Erwähnungen an. Maximal 20 Nutzer auf einmal.' });
}
if (userIds.includes(interaction.user.id)) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You cannot include yourself in a mass kick.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du kannst dich nicht selbst in einen Massenkick aufnehmen.' });
}
if (userIds.includes(client.user.id)) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You cannot include the bot in a mass kick.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du kannst den Bot nicht in einen Massenkick aufnehmen.' });
}
- const results = {
+ const results: MassActionResults = {
successful: [],
failed: [],
skipped: []
@@ -66,19 +73,19 @@ export default {
for (const userId of userIds) {
try {
- const member = await interaction.guild.members.fetch(userId).catch(() => null);
+ const member = await interaction.guild!.members.fetch(userId).catch(() => null);
if (!member) {
- results.failed.push({ userId, reason: "User not in server" });
+ results.failed.push({ userId, reason: "Nutzer ist nicht auf dem Server" });
continue;
}
- const modCheck = ModerationService.validateHierarchy(interaction.member, member, 'kick');
+ const modCheck = ModerationService.validateHierarchy(interaction.member as GuildMember, member, 'kick');
if (!modCheck.valid) {
results.skipped.push({
user: member.user.tag,
userId,
- reason: ModerationService.buildHierarchySkipReason(interaction.member, member, 'kick'),
+ reason: ModerationService.buildHierarchySkipReason(interaction.member as GuildMember, member, 'kick'),
});
continue;
}
@@ -88,7 +95,7 @@ export default {
results.skipped.push({
user: member.user.tag,
userId,
- reason: ModerationService.buildHierarchySkipReason(interaction.member, member, 'kick', 'bot'),
+ reason: ModerationService.buildHierarchySkipReason(interaction.member as GuildMember, member, 'kick', 'bot'),
});
continue;
}
@@ -97,7 +104,7 @@ export default {
results.skipped.push({
user: member.user.tag,
userId,
- reason: 'Target has Admin or a managed role, or bot lacks Kick Members',
+ reason: 'Ziel hat Admin oder eine verwaltete Rolle, oder dem Bot fehlt „Mitglieder kicken“',
});
continue;
}
@@ -111,12 +118,12 @@ export default {
await logModerationAction({
client,
- guild: interaction.guild,
+ guild: interaction.guild!,
event: {
action: "Member Kicked",
target: `${member.user.tag} (${member.user.id})`,
executor: `${interaction.user.tag} (${interaction.user.id})`,
- reason: `${reason} (Mass Kick)`,
+ reason: `${reason} (Massenkick)`,
metadata: {
userId: member.user.id,
moderatorId: interaction.user.id,
@@ -127,9 +134,9 @@ export default {
} catch (error) {
logger.error(`Failed to kick user ${userId}:`, error);
- const reason = error instanceof TitanBotError
+ const reason = error instanceof NexoBotError
? (error.userMessage || error.message)
- : (error.message || "Unknown error");
+ : ((error as ErrorLike).message || "Unbekannter Fehler");
results.failed.push({
userId,
reason,
@@ -137,10 +144,10 @@ export default {
}
}
- let description = `**Mass Kick Results:**\n\n`;
+ let description = `**Ergebnisse des Massenkicks:**\n\n`;
if (results.successful.length > 0) {
- description += `✅ **Successfully Kicked (${results.successful.length}):**\n`;
+ description += `✅ **Erfolgreich gekickt (${results.successful.length}):**\n`;
results.successful.forEach(result => {
description += `• ${result.user} (${result.userId})\n`;
});
@@ -148,7 +155,7 @@ export default {
}
if (results.skipped.length > 0) {
- description += `⚠️ **Skipped (${results.skipped.length}):**\n`;
+ description += `⚠️ **Übersprungen (${results.skipped.length}):**\n`;
results.skipped.forEach(result => {
description += `• ${result.user} - ${result.reason}\n`;
});
@@ -156,7 +163,7 @@ export default {
}
if (results.failed.length > 0) {
- description += `❌ **Failed (${results.failed.length}):**\n`;
+ description += `❌ **Fehlgeschlagen (${results.failed.length}):**\n`;
results.failed.forEach(result => {
description += `• ${result.userId} - ${result.reason}\n`;
});
@@ -167,7 +174,7 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
embed(
- `👢 Mass Kick Completed`,
+ `👢 Massenkick abgeschlossen`,
description
)
]
@@ -175,7 +182,7 @@ export default {
} catch (error) {
logger.error("Error in masskick command:", error);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while processing the mass kick. Please try again later.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Massenkick ist ein Fehler aufgetreten. Bitte versuch es später noch einmal.' });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Moderation/purge.js b/src/commands/Moderation/modules/purge.ts
similarity index 60%
rename from src/commands/Moderation/purge.js
rename to src/commands/Moderation/modules/purge.ts
index 7b279f2d4f..b36c30cc7d 100644
--- a/src/commands/Moderation/purge.js
+++ b/src/commands/Moderation/modules/purge.ts
@@ -1,19 +1,20 @@
-import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed } from '../../utils/embeds.js';
-import { logEvent } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { getColor } from '../../config/bot.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, MessageFlags, type GuildTextBasedChannel } from 'discord.js';
+import { createEmbed, successEmbed } from '../../../utils/embeds.js';
+import { logEvent } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { getColor } from '../../../config/bot.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName("purge")
- .setDescription("Delete a specific amount of messages")
+ .setDescription("Eine bestimmte Anzahl an Nachrichten löschen")
.addIntegerOption((option) =>
option
.setName("amount")
- .setDescription("Number of messages (1-100)")
+ .setDescription("Anzahl der Nachrichten (1-100)")
.setRequired(true),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
@@ -33,11 +34,11 @@ export default {
return;
}
- const amount = interaction.options.getInteger("amount");
- const channel = interaction.channel;
+ const amount = interaction.options.getInteger("amount")!;
+ const channel = interaction.channel as GuildTextBasedChannel;
if (amount < 1 || amount > 100)
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please specify a number between 1 and 100.' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte gib eine Zahl zwischen 1 und 100 an.' });
try {
const fetched = await channel.messages.fetch({ limit: amount });
@@ -49,9 +50,9 @@ export default {
guild: interaction.guild,
event: {
action: "Messages Purged",
- target: `${channel} (${deletedCount} messages)`,
+ target: `${channel} (${deletedCount} Nachrichten)`,
executor: `${interaction.user.tag} (${interaction.user.id})`,
- reason: `Deleted ${deletedCount} messages`,
+ reason: `${deletedCount} Nachrichten gelöscht`,
metadata: {
channelId: channel.id,
messageCount: deletedCount,
@@ -64,8 +65,8 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- "Messages Purged",
- `Deleted ${deletedCount} messages in ${channel}.`,
+ "Nachrichten gelöscht",
+ `${deletedCount} Nachrichten in ${channel} gelöscht.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -78,7 +79,7 @@ export default {
}, 3000);
} catch (error) {
logger.error('Purge command error:', error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An unexpected error occurred during message deletion. Note: Messages older than 14 days cannot be bulk deleted.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Löschen der Nachrichten ist ein unerwarteter Fehler aufgetreten. Hinweis: Nachrichten, die älter als 14 Tage sind, können nicht gesammelt gelöscht werden.' });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Moderation/say.js b/src/commands/Moderation/modules/say.ts
similarity index 65%
rename from src/commands/Moderation/say.js
rename to src/commands/Moderation/modules/say.ts
index 51d8dffbfa..f284040ce5 100644
--- a/src/commands/Moderation/say.js
+++ b/src/commands/Moderation/modules/say.ts
@@ -1,49 +1,54 @@
+import type { Command } from '../../../types/index.js';
import {
SlashCommandBuilder,
PermissionFlagsBits,
ChannelType,
MessageFlags,
+ type ChatInputCommandInteraction,
+ type GuildMember,
+ type NewsChannel,
+ type TextChannel,
} from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logEvent } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { sanitizeInput } from '../../utils/validation.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logEvent } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { sanitizeInput } from '../../../utils/validation.js';
-const TEXT_CHANNEL_TYPES = [
+const TEXT_CHANNEL_TYPES: (ChannelType.GuildText | ChannelType.GuildAnnouncement)[] = [
ChannelType.GuildText,
ChannelType.GuildAnnouncement,
];
-function resolveTargetChannel(interaction) {
+function resolveTargetChannel(interaction: ChatInputCommandInteraction): TextChannel | NewsChannel | null {
const selected = interaction.options.getChannel('channel');
if (selected) {
- return selected;
+ return selected as TextChannel | NewsChannel;
}
- if (!interaction.channel || !TEXT_CHANNEL_TYPES.includes(interaction.channel.type)) {
+ if (!interaction.channel || !(TEXT_CHANNEL_TYPES as ChannelType[]).includes(interaction.channel.type)) {
return null;
}
- return interaction.channel;
+ return interaction.channel as TextChannel | NewsChannel;
}
export default {
data: new SlashCommandBuilder()
.setName('say')
- .setDescription('Send a plain message as the bot')
+ .setDescription('Eine einfache Nachricht als Bot senden')
.addStringOption((option) =>
option
.setName('message')
- .setDescription('The message the bot should send')
+ .setDescription('Die Nachricht, die der Bot senden soll')
.setRequired(true)
.setMaxLength(2000),
)
.addChannelOption((option) =>
option
.setName('channel')
- .setDescription('Channel to send in (defaults to the current channel)')
+ .setDescription('Kanal für die Nachricht (Standard: aktueller Kanal)')
.addChannelTypes(...TEXT_CHANNEL_TYPES)
.setRequired(false),
)
@@ -71,7 +76,7 @@ export default {
if (!message) {
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'Message cannot be empty.',
+ message: 'Die Nachricht darf nicht leer sein.',
});
}
@@ -79,24 +84,24 @@ export default {
if (!channel) {
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'Choose a text channel or run this command in one.',
+ message: 'Wähl einen Textkanal aus oder nutze diesen Command in einem.',
});
}
- const memberPermissions = channel.permissionsFor(interaction.member);
- const botPermissions = channel.permissionsFor(interaction.guild.members.me);
+ const memberPermissions = channel.permissionsFor(interaction.member as GuildMember);
+ const botPermissions = channel.permissionsFor(interaction.guild!.members.me!);
if (!memberPermissions?.has(PermissionFlagsBits.SendMessages)) {
return replyUserError(interaction, {
type: ErrorTypes.PERMISSION,
- message: `You do not have permission to send messages in ${channel}.`,
+ message: `Du hast keine Berechtigung, in ${channel} Nachrichten zu senden.`,
});
}
if (!botPermissions?.has(PermissionFlagsBits.SendMessages)) {
return replyUserError(interaction, {
type: ErrorTypes.PERMISSION,
- message: `I do not have permission to send messages in ${channel}.`,
+ message: `Ich habe keine Berechtigung, in ${channel} Nachrichten zu senden.`,
});
}
@@ -124,11 +129,11 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- 'Message Sent',
- `Posted in ${channel}. [Jump to message](${sentMessage.url})`,
+ 'Nachricht gesendet',
+ `In ${channel} gepostet. [Zur Nachricht springen](${sentMessage.url})`,
),
],
flags: MessageFlags.Ephemeral,
});
},
-};
+} satisfies Command;
diff --git a/src/commands/Moderation/timeout.js b/src/commands/Moderation/modules/timeout.ts
similarity index 54%
rename from src/commands/Moderation/timeout.js
rename to src/commands/Moderation/modules/timeout.ts
index 936464ced6..8fc0f2b30b 100644
--- a/src/commands/Moderation/timeout.js
+++ b/src/commands/Moderation/modules/timeout.ts
@@ -1,40 +1,41 @@
-import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { ModerationService } from '../../services/moderation/moderationService.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, type GuildMember } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { ModerationService } from '../../../services/moderation/moderationService.js';
const durationChoices = [
- { name: "5 minutes", value: 5 },
- { name: "10 minutes", value: 10 },
- { name: "30 minutes", value: 30 },
- { name: "1 hour", value: 60 },
- { name: "6 hours", value: 360 },
- { name: "1 day", value: 1440 },
- { name: "1 week", value: 10080 },
+ { name: "5 Minuten", value: 5 },
+ { name: "10 Minuten", value: 10 },
+ { name: "30 Minuten", value: 30 },
+ { name: "1 Stunde", value: 60 },
+ { name: "6 Stunden", value: 360 },
+ { name: "1 Tag", value: 1440 },
+ { name: "1 Woche", value: 10080 },
];
export default {
data: new SlashCommandBuilder()
.setName("timeout")
- .setDescription("Timeout a user for a specific duration.")
+ .setDescription("Einem Nutzer für eine bestimmte Dauer einen Timeout geben.")
.addUserOption((option) =>
option
.setName("target")
- .setDescription("User to timeout")
+ .setDescription("Nutzer, der einen Timeout bekommen soll")
.setRequired(true),
)
.addIntegerOption(
(option) =>
option
.setName("duration")
- .setDescription("Duration of the timeout")
+ .setDescription("Dauer des Timeouts")
.setRequired(true)
.addChoices(...durationChoices),
)
.addStringOption((option) =>
- option.setName("reason").setDescription("Reason for the timeout"),
+ option.setName("reason").setDescription("Grund für den Timeout"),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: "moderation",
@@ -53,59 +54,59 @@ export default {
const targetUser = interaction.options.getUser("target");
const member = interaction.options.getMember("target");
const durationMinutes = interaction.options.getInteger("duration");
- const reason = interaction.options.getString("reason") || "No reason provided";
+ const reason = interaction.options.getString("reason") || "Kein Grund angegeben";
if (!targetUser) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Missing target user',
ErrorTypes.USER_INPUT,
- 'You must specify a user to timeout.',
+ 'Du musst einen Nutzer für den Timeout angeben.',
{ subtype: 'invalid_user' },
);
}
if (targetUser.id === interaction.user.id) {
- throw new TitanBotError(
+ throw new NexoBotError(
"Cannot timeout self",
ErrorTypes.VALIDATION,
- "You cannot timeout yourself.",
+ "Du kannst dir nicht selbst einen Timeout geben.",
);
}
if (targetUser.id === client.user.id) {
- throw new TitanBotError(
+ throw new NexoBotError(
"Cannot timeout bot",
ErrorTypes.VALIDATION,
- "You cannot timeout the bot.",
+ "Du kannst dem Bot keinen Timeout geben.",
);
}
if (!member) {
- throw new TitanBotError(
+ throw new NexoBotError(
"Target not found",
ErrorTypes.USER_INPUT,
- "The target user is not currently in this server.",
+ "Der Nutzer ist gerade nicht auf diesem Server.",
);
}
- const durationMs = durationMinutes * 60 * 1000;
+ const durationMs = durationMinutes! * 60 * 1000;
const result = await ModerationService.timeoutUser({
- guild: interaction.guild,
- member,
- moderator: interaction.member,
+ guild: interaction.guild!,
+ member: member as GuildMember,
+ moderator: interaction.member as GuildMember,
durationMs,
reason,
});
const durationDisplay =
durationChoices.find((c) => c.value === durationMinutes)
- ?.name || `${durationMinutes} minutes`;
+ ?.name || `${durationMinutes} Minuten`;
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- `⏳ **Timed out** ${targetUser.tag} for ${durationDisplay}.`,
- `**Reason:** ${reason}\n**Case ID:** #${result.caseId}`,
+ `⏳ **Timeout** für ${targetUser.tag} (${durationDisplay}).`,
+ `**Grund:** ${reason}\n**Fall-ID:** #${result.caseId}`,
),
],
});
},
-};
+} satisfies Command;
diff --git a/src/commands/Moderation/unban.js b/src/commands/Moderation/modules/unban.ts
similarity index 56%
rename from src/commands/Moderation/unban.js
rename to src/commands/Moderation/modules/unban.ts
index 484cdec02a..24f209a91c 100644
--- a/src/commands/Moderation/unban.js
+++ b/src/commands/Moderation/modules/unban.ts
@@ -1,23 +1,24 @@
-import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { ModerationService } from '../../services/moderation/moderationService.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, type GuildMember } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { ModerationService } from '../../../services/moderation/moderationService.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("unban")
- .setDescription("Unban a user from the server")
+ .setDescription("Einen Nutzer auf dem Server entbannen")
.addStringOption(option =>
option
.setName("target")
- .setDescription("The ID (or mention) of the user to unban")
+ .setDescription("Die ID (oder Erwähnung) des Nutzers, der entbannt werden soll")
.setRequired(true),
)
.addStringOption(option =>
option.setName("reason")
- .setDescription("Reason for the unban")
+ .setDescription("Grund für die Entbannung")
.setRequired(false),
)
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
@@ -35,12 +36,12 @@ export default {
}
const rawTarget = interaction.options.getString("target");
- const targetId = rawTarget.replace(/[<@!>]/g, '').trim();
+ const targetId = rawTarget!.replace(/[<@!>]/g, '').trim();
if (!/^\d{17,20}$/.test(targetId)) {
return replyUserError(interaction, {
type: ErrorTypes.USER_INPUT,
- message: 'Please provide a valid user ID or mention.',
+ message: 'Bitte gib eine gültige Nutzer-ID oder Erwähnung an.',
});
}
@@ -48,26 +49,26 @@ export default {
if (!targetUser) {
return replyUserError(interaction, {
type: ErrorTypes.USER_INPUT,
- message: `Could not find a user with the ID \`${targetId}\`.`,
+ message: `Es wurde kein Nutzer mit der ID \`${targetId}\` gefunden.`,
});
}
- const reason = interaction.options.getString("reason") || "No reason provided";
+ const reason = interaction.options.getString("reason") || "Kein Grund angegeben";
const result = await ModerationService.unbanUser({
- guild: interaction.guild,
+ guild: interaction.guild!,
user: targetUser,
- moderator: interaction.member,
+ moderator: interaction.member as GuildMember,
reason,
});
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- "✅ User Unbanned",
- `Successfully unbanned **${targetUser.tag}** from the server.\n\n**Reason:** ${reason}\n**Case ID:** #${result.caseId}`,
+ "✅ Nutzer entbannt",
+ `**${targetUser.tag}** wurde erfolgreich entbannt.\n\n**Grund:** ${reason}\n**Fall-ID:** #${result.caseId}`,
),
],
});
},
-};
+} satisfies Command;
diff --git a/src/commands/Moderation/unlock.js b/src/commands/Moderation/modules/unlock.ts
similarity index 63%
rename from src/commands/Moderation/unlock.js
rename to src/commands/Moderation/modules/unlock.ts
index 408c2b3b07..3709a43ce8 100644
--- a/src/commands/Moderation/unlock.js
+++ b/src/commands/Moderation/modules/unlock.ts
@@ -1,16 +1,17 @@
-import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logEvent } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { getColor } from '../../config/bot.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, type TextChannel } from 'discord.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logEvent } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { getColor } from '../../../config/bot.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
export default {
data: new SlashCommandBuilder()
.setName("unlock")
.setDescription(
- "Unlocks the current channel (allows @everyone to send messages again).",
+ "Entsperrt den aktuellen Kanal (@everyone kann wieder Nachrichten senden).",
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels),
category: "moderation",
@@ -26,8 +27,8 @@ export default {
return;
}
- const channel = interaction.channel;
- const everyoneRole = interaction.guild.roles.everyone;
+ const channel = interaction.channel as TextChannel;
+ const everyoneRole = interaction.guild!.roles.everyone;
try {
const currentPermissions = channel.permissionsFor(everyoneRole);
@@ -37,7 +38,7 @@ export default {
currentPermissions.has(PermissionFlagsBits.SendMessages) ===
null
) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `${channel} is not explicitly locked (everyone can already send messages).` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `${channel} ist nicht explizit gesperrt (alle können bereits Nachrichten senden).` });
}
await channel.permissionOverwrites.edit(
@@ -45,13 +46,13 @@ export default {
{ SendMessages: true },
{
type: 0,
- reason: `Channel unlocked by ${interaction.user.tag}`,
+ reason: `Kanal entsperrt von ${interaction.user.tag}`,
},
);
await logEvent({
client,
- guild: interaction.guild,
+ guild: interaction.guild!,
event: {
action: "Channel Unlocked",
target: channel.toString(),
@@ -66,14 +67,14 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- `🔓 **Channel Unlocked**`,
- `${channel} is now unlocked. You may speak now.`,
+ `🔓 **Kanal entsperrt**`,
+ `${channel} ist jetzt entsperrt. Ihr könnt wieder schreiben.`,
),
],
});
} catch (error) {
logger.error('Unlock command error:', error);
- await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'An unexpected error occurred while trying to unlock the channel. Check my permissions (I need \'Manage Channels\').' });
+ await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Beim Entsperren des Kanals ist ein unerwarteter Fehler aufgetreten. Prüf meine Berechtigungen (ich brauche „Kanäle verwalten“).' });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Moderation/untimeout.js b/src/commands/Moderation/modules/untimeout.ts
similarity index 55%
rename from src/commands/Moderation/untimeout.js
rename to src/commands/Moderation/modules/untimeout.ts
index 104a108f2e..447691de57 100644
--- a/src/commands/Moderation/untimeout.js
+++ b/src/commands/Moderation/modules/untimeout.ts
@@ -1,18 +1,19 @@
-import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { ModerationService } from '../../services/moderation/moderationService.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, type GuildMember } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { ModerationService } from '../../../services/moderation/moderationService.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("untimeout")
- .setDescription("Remove timeout from a user")
+ .setDescription("Den Timeout eines Nutzers aufheben")
.addUserOption((option) =>
option
.setName("target")
- .setDescription("User to untimeout")
+ .setDescription("Nutzer, dessen Timeout aufgehoben werden soll")
.setRequired(true),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
@@ -33,34 +34,34 @@ export default {
const member = interaction.options.getMember("target");
if (!targetUser) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Missing target user',
ErrorTypes.USER_INPUT,
- 'You must specify a user to untimeout.',
+ 'Du musst einen Nutzer angeben, dessen Timeout aufgehoben werden soll.',
{ subtype: 'invalid_user' },
);
}
if (!member) {
- throw new TitanBotError(
+ throw new NexoBotError(
"Target not found",
ErrorTypes.USER_INPUT,
- "The target user is not currently in this server.",
+ "Der Nutzer ist gerade nicht auf diesem Server.",
);
}
await ModerationService.removeTimeoutUser({
- guild: interaction.guild,
- member,
- moderator: interaction.member,
+ guild: interaction.guild!,
+ member: member as GuildMember,
+ moderator: interaction.member as GuildMember,
});
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- `🔓 **Removed timeout** from ${targetUser.tag}`,
+ `🔓 **Timeout aufgehoben** für ${targetUser.tag}`,
),
],
});
},
-};
+} satisfies Command;
diff --git a/src/commands/Moderation/usernotes.js b/src/commands/Moderation/modules/usernotes.ts
similarity index 56%
rename from src/commands/Moderation/usernotes.js
rename to src/commands/Moderation/modules/usernotes.ts
index 4fc581fec7..be597aae2d 100644
--- a/src/commands/Moderation/usernotes.js
+++ b/src/commands/Moderation/modules/usernotes.ts
@@ -1,41 +1,51 @@
-import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { getFromDb, setInDb, deleteFromDb, getUserNotesKey, getUserNotesListKey } from '../../utils/database.js';
-import { sanitizeInput } from '../../utils/validation.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, type ChatInputCommandInteraction, type User } from 'discord.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { getFromDb, setInDb, deleteFromDb, getUserNotesKey, getUserNotesListKey } from '../../../utils/database.js';
+import { sanitizeInput } from '../../../utils/validation.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+
+export interface UserNote {
+ id: number;
+ content: string;
+ type: string;
+ author: string;
+ authorId: string;
+ timestamp: string;
+}
export default {
data: new SlashCommandBuilder()
.setName("usernotes")
- .setDescription("Manage user notes for moderation purposes")
+ .setDescription("Nutzernotizen für die Moderation verwalten")
.addSubcommand(subcommand =>
subcommand
.setName("add")
- .setDescription("Add a note to a user")
+ .setDescription("Eine Notiz zu einem Nutzer hinzufügen")
.addUserOption(option =>
option
.setName("target")
- .setDescription("The user to add a note for")
+ .setDescription("Der Nutzer, zu dem du eine Notiz hinzufügen willst")
.setRequired(true)
)
.addStringOption(option =>
option
.setName("note")
- .setDescription("The note to add")
+ .setDescription("Die Notiz, die hinzugefügt werden soll")
.setRequired(true)
)
.addStringOption(option =>
option
.setName("type")
- .setDescription("Type of note")
+ .setDescription("Art der Notiz")
.addChoices(
- { name: "Warning", value: "warning" },
- { name: "Positive", value: "positive" },
+ { name: "Warnung", value: "warning" },
+ { name: "Positiv", value: "positive" },
{ name: "Neutral", value: "neutral" },
- { name: "Alert", value: "alert" }
+ { name: "Alarm", value: "alert" }
)
.setRequired(false)
)
@@ -43,28 +53,28 @@ export default {
.addSubcommand(subcommand =>
subcommand
.setName("view")
- .setDescription("View notes for a user")
+ .setDescription("Notizen zu einem Nutzer ansehen")
.addUserOption(option =>
option
.setName("target")
- .setDescription("The user to view notes for")
+ .setDescription("Der Nutzer, dessen Notizen du sehen willst")
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("remove")
- .setDescription("Remove a specific note from a user")
+ .setDescription("Eine bestimmte Notiz eines Nutzers entfernen")
.addUserOption(option =>
option
.setName("target")
- .setDescription("The user to remove a note from")
+ .setDescription("Der Nutzer, von dem du eine Notiz entfernen willst")
.setRequired(true)
)
.addIntegerOption(option =>
option
.setName("index")
- .setDescription("The index of the note to remove")
+ .setDescription("Die Nummer der Notiz, die entfernt werden soll")
.setRequired(true)
.setMinValue(1)
)
@@ -72,11 +82,11 @@ export default {
.addSubcommand(subcommand =>
subcommand
.setName("clear")
- .setDescription("Clear all notes for a user")
+ .setDescription("Alle Notizen eines Nutzers löschen")
.addUserOption(option =>
option
.setName("target")
- .setDescription("The user to clear notes for")
+ .setDescription("Der Nutzer, dessen Notizen gelöscht werden sollen")
.setRequired(true)
)
)
@@ -86,53 +96,53 @@ export default {
async execute(interaction, config, client) {
const subcommand = interaction.options.getSubcommand();
const targetUser = interaction.options.getUser("target");
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
if (subcommand !== "view" && subcommand !== "remove" && subcommand !== "clear" && subcommand !== "add") {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please select a valid subcommand.' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte wähl einen gültigen Subcommand.' });
}
- let notes = [];
+ let notes: UserNote[] = [];
if (targetUser) {
const notesKey = getUserNotesKey(guildId, targetUser.id);
- notes = await getFromDb(notesKey, []);
+ notes = await getFromDb(notesKey, []);
}
try {
switch (subcommand) {
case "add":
- return await handleAddNote(interaction, targetUser, notes, guildId);
+ return await handleAddNote(interaction, targetUser!, notes, guildId);
case "view":
- return await handleViewNotes(interaction, targetUser, notes);
+ return await handleViewNotes(interaction, targetUser!, notes);
case "remove":
- return await handleRemoveNote(interaction, targetUser, notes, guildId);
+ return await handleRemoveNote(interaction, targetUser!, notes, guildId);
case "clear":
- return await handleClearNotes(interaction, targetUser, notes, guildId);
+ return await handleClearNotes(interaction, targetUser!, notes, guildId);
default:
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please select a valid subcommand.' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte wähl einen gültigen Subcommand.' });
}
} catch (error) {
logger.error(`Error in usernotes command (${subcommand}):`, error);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while processing your request. Please try again later.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Bei deiner Anfrage ist ein Fehler aufgetreten. Bitte versuch es später noch einmal.' });
}
}
-};
+} satisfies Command;
-async function handleAddNote(interaction, targetUser, notes, guildId) {
- let note = interaction.options.getString("note").trim();
+async function handleAddNote(interaction: ChatInputCommandInteraction, targetUser: User, notes: UserNote[], guildId: string) {
+ let note = interaction.options.getString("note")!.trim();
const type = interaction.options.getString("type") || "neutral";
if (note.length > 1000) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Notes must be 1000 characters or less.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Notizen dürfen höchstens 1000 Zeichen lang sein.' });
}
if (note.length === 0) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Note cannot be empty.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Die Notiz darf nicht leer sein.' });
}
note = sanitizeInput(note);
- const noteData = {
+ const noteData: UserNote = {
id: Date.now(),
content: note,
type: type,
@@ -151,64 +161,64 @@ async function handleAddNote(interaction, targetUser, notes, guildId) {
return InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- `${typeInfo.emoji} Note Added`,
- `Added a **${type}** note for **${targetUser.tag}**:\n\n` +
+ `${typeInfo.emoji} Notiz hinzugefügt`,
+ `Notiz (**${typeInfo.label}**) für **${targetUser.tag}** hinzugefügt:\n\n` +
`> ${note}\n\n` +
`**Moderator:** ${interaction.user.tag}\n` +
- `**Total Notes:** ${notes.length}`
+ `**Notizen gesamt:** ${notes.length}`
)
]
});
}
-async function handleViewNotes(interaction, targetUser, notes) {
+async function handleViewNotes(interaction: ChatInputCommandInteraction, targetUser: User, notes: UserNote[]) {
if (notes.length === 0) {
return InteractionHelper.safeReply(interaction, {
embeds: [
infoEmbed(
- "📝 No Notes",
- `There are no notes for **${targetUser.tag}**.`
+ "📝 Keine Notizen",
+ `Zu **${targetUser.tag}** gibt es keine Notizen.`
),
],
});
}
- const sortedNotes = [...notes].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
+ const sortedNotes = [...notes].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
- let description = `**Notes for ${targetUser.tag} (${targetUser.id}):**\n\n`;
+ let description = `**Notizen zu ${targetUser.tag} (${targetUser.id}):**\n\n`;
sortedNotes.forEach((note, index) => {
const typeInfo = getNoteTypeInfo(note.type);
const date = new Date(note.timestamp).toLocaleDateString();
- description += `${typeInfo.emoji} **Note #${index + 1}** (${note.type}) - ${date}\n`;
+ description += `${typeInfo.emoji} **Notiz #${index + 1}** (${typeInfo.label}) - ${date}\n`;
description += `> ${note.content}\n`;
- description += `*Added by ${note.author}*\n\n`;
+ description += `*Hinzugefügt von ${note.author}*\n\n`;
});
if (description.length > 4000) {
- description = description.substring(0, 3900) + "\n... *(truncated)*";
+ description = description.substring(0, 3900) + "\n... *(gekürzt)*";
}
return InteractionHelper.safeReply(interaction, {
embeds: [
infoEmbed(
- `📝 User Notes (${notes.length})`,
+ `📝 Nutzernotizen (${notes.length})`,
description
)
]
});
}
-async function handleRemoveNote(interaction, targetUser, notes, guildId) {
- const index = interaction.options.getInteger("index") - 1;
+async function handleRemoveNote(interaction: ChatInputCommandInteraction, targetUser: User, notes: UserNote[], guildId: string) {
+ const index = interaction.options.getInteger("index")! - 1;
if (index < 0 || index >= notes.length) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: `Please provide a valid note index (1-${notes.length}).` });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: `Bitte gib eine gültige Notiznummer an (1-${notes.length}).` });
}
// The view command displays notes sorted newest-first, so resolve the index
// against the same ordering to delete the note the user actually sees.
- const sortedNotes = [...notes].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
+ const sortedNotes = [...notes].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
const removedNote = sortedNotes[index];
const originalIndex = notes.indexOf(removedNote);
notes.splice(originalIndex, 1);
@@ -221,24 +231,24 @@ async function handleRemoveNote(interaction, targetUser, notes, guildId) {
return InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- `${typeInfo.emoji} Note Removed`,
- `Removed note #${index + 1} from **${targetUser.tag}**:\n\n` +
+ `${typeInfo.emoji} Notiz entfernt`,
+ `Notiz #${index + 1} von **${targetUser.tag}** entfernt:\n\n` +
`> ${removedNote.content}\n\n` +
- `**Remaining Notes:** ${notes.length}`
+ `**Verbleibende Notizen:** ${notes.length}`
)
]
});
}
-async function handleClearNotes(interaction, targetUser, notes, guildId) {
+async function handleClearNotes(interaction: ChatInputCommandInteraction, targetUser: User, notes: UserNote[], guildId: string) {
const noteCount = notes.length;
if (noteCount === 0) {
return InteractionHelper.safeReply(interaction, {
embeds: [
infoEmbed(
- "No Notes to Clear",
- `There are no notes for **${targetUser.tag}** to clear.`
+ "Keine Notizen zum Löschen",
+ `Zu **${targetUser.tag}** gibt es keine Notizen zum Löschen.`
),
],
});
@@ -252,19 +262,19 @@ async function handleClearNotes(interaction, targetUser, notes, guildId) {
return InteractionHelper.safeReply(interaction, {
embeds: [
successEmbed(
- "🗑️ Notes Cleared",
- `Cleared **${noteCount}** notes from **${targetUser.tag}**.`
+ "🗑️ Notizen gelöscht",
+ `**${noteCount}** Notizen von **${targetUser.tag}** gelöscht.`
)
]
});
}
-function getNoteTypeInfo(type) {
- const types = {
- warning: { emoji: "⚠️", color: "#FF6B6B" },
- positive: { emoji: "✅", color: "#51CF66" },
- neutral: { emoji: "📝", color: "#74C0FC" },
- alert: { emoji: "🚨", color: "#FFD43B" }
+function getNoteTypeInfo(type: string) {
+ const types: Record = {
+ warning: { emoji: "⚠️", color: "#FF6B6B", label: "Warnung" },
+ positive: { emoji: "✅", color: "#51CF66", label: "Positiv" },
+ neutral: { emoji: "📝", color: "#74C0FC", label: "Neutral" },
+ alert: { emoji: "🚨", color: "#FFD43B", label: "Alarm" }
};
return types[type] || types.neutral;
diff --git a/src/commands/Moderation/warn.js b/src/commands/Moderation/modules/warn.ts
similarity index 65%
rename from src/commands/Moderation/warn.js
rename to src/commands/Moderation/modules/warn.ts
index 1d1b2ef1eb..7ec1f7dfae 100644
--- a/src/commands/Moderation/warn.js
+++ b/src/commands/Moderation/modules/warn.ts
@@ -1,26 +1,27 @@
-import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, MessageFlags } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logModerationAction } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { WarningService } from '../../services/moderation/warningService.js';
-import { ModerationService } from '../../services/moderation/moderationService.js';
-import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
+import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, MessageFlags, type GuildMember } from 'discord.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logModerationAction } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { WarningService } from '../../../services/moderation/warningService.js';
+import { ModerationService } from '../../../services/moderation/moderationService.js';
+import { NexoBotError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("warn")
- .setDescription("Warn a user")
+ .setDescription("Einen Nutzer verwarnen")
.addUserOption((o) =>
o
.setName("target")
.setRequired(true)
- .setDescription("User to warn"),
+ .setDescription("Nutzer, der verwarnt werden soll"),
)
.addStringOption((o) =>
o
.setName("reason")
.setRequired(true)
- .setDescription("Reason for the warning"),
+ .setDescription("Grund für die Verwarnung"),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: "moderation",
@@ -40,35 +41,35 @@ export default {
const member = interaction.options.getMember("target");
const reason = interaction.options.getString("reason");
const moderator = interaction.user;
- const guildId = interaction.guildId;
+ const guildId = interaction.guildId!;
if (!target) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Missing target user',
ErrorTypes.USER_INPUT,
- 'You must specify a user to warn.',
+ 'Du musst einen Nutzer zum Verwarnen angeben.',
{ subtype: 'invalid_user' },
);
}
if (!reason) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Missing warning reason',
ErrorTypes.VALIDATION,
- 'You must provide a reason for the warning.',
+ 'Du musst einen Grund für die Verwarnung angeben.',
{ subtype: 'missing_required' },
);
}
if (!member) {
- throw new TitanBotError(
+ throw new NexoBotError(
"Target not found",
ErrorTypes.USER_INPUT,
- "The target user is not currently in this server."
+ "Der Nutzer ist gerade nicht auf diesem Server."
);
}
- ModerationService.assertModerationHierarchy(interaction.member, member, 'warn');
+ ModerationService.assertModerationHierarchy(interaction.member as GuildMember, member as GuildMember, 'warn');
const { id, totalCount } = await WarningService.addWarning({
guildId,
@@ -80,7 +81,7 @@ export default {
await logModerationAction({
client,
- guild: interaction.guild,
+ guild: interaction.guild!,
event: {
action: "User Warned",
target: `${target.tag} (${target.id})`,
@@ -99,10 +100,10 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- `⚠️ **Warned** ${target.tag}`,
- `**Reason:** ${reason}\n**Total Warns:** ${totalCount}`,
+ `⚠️ **Verwarnt:** ${target.tag}`,
+ `**Grund:** ${reason}\n**Verwarnungen gesamt:** ${totalCount}`,
),
],
});
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Moderation/warnings.js b/src/commands/Moderation/modules/warnings.ts
similarity index 66%
rename from src/commands/Moderation/warnings.js
rename to src/commands/Moderation/modules/warnings.ts
index 59fbce081c..05fb4a88c3 100644
--- a/src/commands/Moderation/warnings.js
+++ b/src/commands/Moderation/modules/warnings.ts
@@ -1,20 +1,21 @@
-import { getColor } from '../../config/bot.js';
+import type { Command } from '../../../types/index.js';
+import { getColor } from '../../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { logEvent } from '../../utils/moderation.js';
-import { logger } from '../../utils/logger.js';
-import { WarningService } from '../../services/moderation/warningService.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { logEvent } from '../../../utils/moderation.js';
+import { logger } from '../../../utils/logger.js';
+import { WarningService } from '../../../services/moderation/warningService.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
data: new SlashCommandBuilder()
.setName("warnings")
- .setDescription("View all warnings for a user")
+ .setDescription("Alle Verwarnungen eines Nutzers ansehen")
.addUserOption((o) =>
o
.setName("target")
.setRequired(true)
- .setDescription("User to check warnings for"),
+ .setDescription("Nutzer, dessen Verwarnungen du sehen willst"),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: "moderation",
@@ -30,8 +31,8 @@ export default {
return;
}
- const target = interaction.options.getUser("target");
- const guildId = interaction.guildId;
+ const target = interaction.options.getUser("target")!;
+ const guildId = interaction.guildId!;
const validWarnings = await WarningService.getWarnings(guildId, target.id);
const totalWarns = validWarnings.length;
@@ -40,8 +41,8 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
createEmbed({
- title: `Warnings: ${target.tag}`,
- description: "This user has no recorded warnings.",
+ title: `Verwarnungen: ${target.tag}`,
+ description: "Dieser Nutzer hat keine Verwarnungen.",
}).setColor(getColor('success')),
],
});
@@ -49,16 +50,16 @@ export default {
}
const embed = createEmbed({
- title: `Warnings: ${target.tag}`,
- description: `Total Warnings: **${totalWarns}**`,
+ title: `Verwarnungen: ${target.tag}`,
+ description: `Verwarnungen gesamt: **${totalWarns}**`,
}).setColor(getColor('warning'));
const warningFields = validWarnings
.map((w, i) => {
const discordTimestamp = Math.floor(w.timestamp / 1000);
return {
- name: `[#${i + 1}] Reason: ${w.reason.substring(0, 100)}`,
- value: `**Moderator:** <@${w.moderatorId}>\n**Date:** ()`,
+ name: `[#${i + 1}] Grund: ${w.reason.substring(0, 100)}`,
+ value: `**Moderator:** <@${w.moderatorId}>\n**Datum:** ()`,
inline: false,
};
})
@@ -66,14 +67,14 @@ export default {
embed.addFields(warningFields);
- const actionRow = new ActionRowBuilder().addComponents(
+ const actionRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`warning_delete_specific:${target.id}:${interaction.user.id}`)
- .setLabel('Delete Specific Warning')
+ .setLabel('Bestimmte Verwarnung löschen')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`warning_clear_all:${target.id}:${interaction.user.id}`)
- .setLabel('Clear All Warnings')
+ .setLabel('Alle Verwarnungen löschen')
.setStyle(ButtonStyle.Danger),
);
@@ -84,7 +85,7 @@ export default {
action: "Warnings Viewed",
target: `${target.tag} (${target.id})`,
executor: `${interaction.user.tag} (${interaction.user.id})`,
- reason: `Viewed ${totalWarns} warnings`,
+ reason: `${totalWarns} Verwarnungen angesehen`,
metadata: {
userId: target.id,
moderatorId: interaction.user.id,
@@ -95,4 +96,4 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed], components: [actionRow] });
},
-};
+} satisfies Command;
diff --git a/src/commands/Moderation/msg.ts b/src/commands/Moderation/msg.ts
new file mode 100644
index 0000000000..3048155bbd
--- /dev/null
+++ b/src/commands/Moderation/msg.ts
@@ -0,0 +1,19 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import purgeCommand from './modules/purge.js';
+import sayCommand from './modules/say.js';
+import embedbuilderCommand from '../Tools/modules/embedbuilder.js';
+import usernotesCommand from './modules/usernotes.js';
+
+export default createRouterCommand({
+ name: 'msg',
+ description: 'Nachrichten löschen, senden und erstellen, Nutzernotizen',
+ category: 'Moderation',
+ defaultMemberPermissions: PermissionFlagsBits.ManageMessages,
+ routes: [
+ { legacyName: 'purge', module: purgeCommand },
+ { legacyName: 'say', module: sayCommand },
+ { legacyName: 'embedbuilder', module: embedbuilderCommand },
+ { legacyName: 'usernotes', module: usernotesCommand },
+ ],
+});
diff --git a/src/commands/Music/join.js b/src/commands/Music/modules/join.ts
similarity index 52%
rename from src/commands/Music/join.js
rename to src/commands/Music/modules/join.ts
index 10e6174103..e7909d8ddf 100644
--- a/src/commands/Music/join.js
+++ b/src/commands/Music/modules/join.ts
@@ -1,13 +1,14 @@
import { SlashCommandBuilder } from 'discord.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { joinVoiceChannel, replyMusicSuccess } from '../../services/music/musicActions.js';
-import { deferMusicCommand } from '../../services/music/prefixSupport.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { joinVoiceChannel, replyMusicSuccess } from '../../../services/music/musicActions.js';
+import { deferMusicCommand } from '../../../services/music/prefixSupport.js';
+import type { Command } from '../../../types/index.js';
export default {
category: 'Music',
data: new SlashCommandBuilder()
.setName('join')
- .setDescription('Join your voice channel without starting playback'),
+ .setDescription('Deinem Sprachkanal beitreten, ohne die Wiedergabe zu starten'),
async execute(interaction, config, client) {
const deferred = await deferMusicCommand(interaction);
@@ -18,4 +19,4 @@ export default {
const embed = await joinVoiceChannel(client, interaction);
await replyMusicSuccess(interaction, embed);
},
-};
+} satisfies Command;
diff --git a/src/commands/Music/music.js b/src/commands/Music/modules/music.ts
similarity index 63%
rename from src/commands/Music/music.js
rename to src/commands/Music/modules/music.ts
index fa8d7cfef6..95833f897b 100644
--- a/src/commands/Music/music.js
+++ b/src/commands/Music/modules/music.ts
@@ -1,5 +1,6 @@
import { SlashCommandBuilder } from 'discord.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { LoopOption } from 'riffy';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
import {
skipTrack,
stopPlayback,
@@ -15,92 +16,93 @@ import {
setTwentyFourSeven,
leaveVoiceChannel,
replyMusicSuccess,
-} from '../../services/music/musicActions.js';
-import { deferMusicCommand } from '../../services/music/prefixSupport.js';
+} from '../../../services/music/musicActions.js';
+import { deferMusicCommand } from '../../../services/music/prefixSupport.js';
+import type { Command } from '../../../types/index.js';
export default {
category: 'Music',
data: new SlashCommandBuilder()
.setName('music')
- .setDescription('Manage playback, queue, and voice session settings')
+ .setDescription('Wiedergabe, Warteschlange und Sprachsitzung verwalten')
.addSubcommand((sub) =>
- sub.setName('pause').setDescription('Pause playback'),
+ sub.setName('pause').setDescription('Wiedergabe pausieren'),
)
.addSubcommand((sub) =>
- sub.setName('resume').setDescription('Resume playback'),
+ sub.setName('resume').setDescription('Wiedergabe fortsetzen'),
)
.addSubcommand((sub) =>
- sub.setName('skip').setDescription('Skip the current track'),
+ sub.setName('skip').setDescription('Aktuellen Titel überspringen'),
)
.addSubcommand((sub) =>
- sub.setName('stop').setDescription('Stop playback and clear the queue'),
+ sub.setName('stop').setDescription('Wiedergabe stoppen und Warteschlange leeren'),
)
.addSubcommand((sub) =>
- sub.setName('shuffle').setDescription('Shuffle the queue'),
+ sub.setName('shuffle').setDescription('Warteschlange mischen'),
)
.addSubcommand((sub) =>
sub
.setName('loop')
- .setDescription('Set loop mode')
+ .setDescription('Wiederholungsmodus festlegen')
.addStringOption((opt) =>
opt
.setName('mode')
- .setDescription('Loop mode')
+ .setDescription('Wiederholungsmodus')
.setRequired(true)
.addChoices(
- { name: 'Off', value: 'none' },
- { name: 'Track', value: 'track' },
- { name: 'Queue', value: 'queue' },
+ { name: 'Aus', value: 'none' },
+ { name: 'Titel', value: 'track' },
+ { name: 'Warteschlange', value: 'queue' },
),
),
)
.addSubcommand((sub) =>
sub
.setName('volume')
- .setDescription('Set playback volume')
+ .setDescription('Lautstärke einstellen')
.addIntegerOption((opt) =>
- opt.setName('level').setDescription('Volume (0-100)').setRequired(true).setMinValue(0).setMaxValue(100),
+ opt.setName('level').setDescription('Lautstärke (0-100)').setRequired(true).setMinValue(0).setMaxValue(100),
),
)
.addSubcommand((sub) =>
sub
.setName('seek')
- .setDescription('Seek to a position in the current track')
+ .setDescription('Zu einer Position im aktuellen Titel springen')
.addIntegerOption((opt) =>
- opt.setName('seconds').setDescription('Position in seconds').setRequired(true).setMinValue(0),
+ opt.setName('seconds').setDescription('Position in Sekunden').setRequired(true).setMinValue(0),
),
)
.addSubcommand((sub) =>
sub
.setName('remove')
- .setDescription('Remove a track from the queue')
+ .setDescription('Titel aus der Warteschlange entfernen')
.addIntegerOption((opt) =>
- opt.setName('position').setDescription('Queue position').setRequired(true).setMinValue(1),
+ opt.setName('position').setDescription('Position in der Warteschlange').setRequired(true).setMinValue(1),
),
)
.addSubcommand((sub) =>
sub
.setName('move')
- .setDescription('Move a track in the queue')
+ .setDescription('Titel in der Warteschlange verschieben')
.addIntegerOption((opt) =>
- opt.setName('from').setDescription('Current position').setRequired(true).setMinValue(1),
+ opt.setName('from').setDescription('Aktuelle Position').setRequired(true).setMinValue(1),
)
.addIntegerOption((opt) =>
- opt.setName('to').setDescription('New position').setRequired(true).setMinValue(1),
+ opt.setName('to').setDescription('Neue Position').setRequired(true).setMinValue(1),
),
)
.addSubcommand((sub) =>
- sub.setName('clear').setDescription('Clear the queue'),
+ sub.setName('clear').setDescription('Warteschlange leeren'),
)
.addSubcommand((sub) =>
- sub.setName('leave').setDescription('Disconnect the bot from the voice channel'),
+ sub.setName('leave').setDescription('Bot vom Sprachkanal trennen'),
)
.addSubcommand((sub) =>
sub
.setName('247')
- .setDescription('Toggle 24/7 mode (stay in voice channel when idle)')
+ .setDescription('24/7-Modus umschalten (auch ohne Wiedergabe im Sprachkanal bleiben)')
.addBooleanOption((opt) =>
- opt.setName('enabled').setDescription('Enable or disable 24/7 mode').setRequired(true),
+ opt.setName('enabled').setDescription('24/7-Modus aktivieren oder deaktivieren').setRequired(true),
),
),
@@ -135,22 +137,22 @@ export default {
break;
}
case 'loop': {
- const embed = await setLoopMode(client, interaction, interaction.options.getString('mode'));
+ const embed = await setLoopMode(client, interaction, interaction.options.getString('mode')! as LoopOption);
await replyMusicSuccess(interaction, embed);
break;
}
case 'volume': {
- const embed = await setVolume(client, interaction, interaction.options.getInteger('level'));
+ const embed = await setVolume(client, interaction, interaction.options.getInteger('level')!);
await replyMusicSuccess(interaction, embed);
break;
}
case 'seek': {
- const embed = await seekTrack(client, interaction, interaction.options.getInteger('seconds'));
+ const embed = await seekTrack(client, interaction, interaction.options.getInteger('seconds')!);
await replyMusicSuccess(interaction, embed);
break;
}
case 'remove': {
- const embed = await removeFromQueue(client, interaction, interaction.options.getInteger('position'));
+ const embed = await removeFromQueue(client, interaction, interaction.options.getInteger('position')!);
await replyMusicSuccess(interaction, embed);
break;
}
@@ -158,8 +160,8 @@ export default {
const embed = await moveInQueue(
client,
interaction,
- interaction.options.getInteger('from'),
- interaction.options.getInteger('to'),
+ interaction.options.getInteger('from')!,
+ interaction.options.getInteger('to')!,
);
await replyMusicSuccess(interaction, embed);
break;
@@ -175,14 +177,14 @@ export default {
break;
}
case '247': {
- const embed = await setTwentyFourSeven(client, interaction, interaction.options.getBoolean('enabled'));
+ const embed = await setTwentyFourSeven(client, interaction, interaction.options.getBoolean('enabled')!);
await replyMusicSuccess(interaction, embed);
break;
}
default:
await InteractionHelper.safeEditReply(interaction, {
- content: 'Unknown music subcommand.',
+ content: 'Unbekannter Musik-Subcommand.',
});
}
},
-};
+} satisfies Command;
diff --git a/src/commands/Music/nowplaying.js b/src/commands/Music/modules/nowplaying.ts
similarity index 50%
rename from src/commands/Music/nowplaying.js
rename to src/commands/Music/modules/nowplaying.ts
index e50c685122..1a0c2d5005 100644
--- a/src/commands/Music/nowplaying.js
+++ b/src/commands/Music/modules/nowplaying.ts
@@ -1,17 +1,18 @@
import { SlashCommandBuilder } from 'discord.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { buildNowPlayingReply } from '../../services/music/musicActions.js';
-import { deferMusicCommand } from '../../services/music/prefixSupport.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { buildNowPlayingReply } from '../../../services/music/musicActions.js';
+import { deferMusicCommand } from '../../../services/music/prefixSupport.js';
+import type { Command } from '../../../types/index.js';
export default {
category: 'Music',
data: new SlashCommandBuilder()
.setName('nowplaying')
- .setDescription('Show the currently playing track'),
+ .setDescription('Aktuell laufenden Titel anzeigen'),
async execute(interaction, config, client) {
await deferMusicCommand(interaction);
- const payload = buildNowPlayingReply(client, interaction.guild.id);
+ const payload = buildNowPlayingReply(client, interaction.guild!.id);
await InteractionHelper.safeEditReply(interaction, payload);
},
-};
+} satisfies Command;
diff --git a/src/commands/Music/play.js b/src/commands/Music/modules/play.ts
similarity index 56%
rename from src/commands/Music/play.js
rename to src/commands/Music/modules/play.ts
index 1d07665382..5149ced60c 100644
--- a/src/commands/Music/play.js
+++ b/src/commands/Music/modules/play.ts
@@ -1,15 +1,16 @@
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { playQuery, replyMusicSuccess } from '../../services/music/musicActions.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { playQuery, replyMusicSuccess } from '../../../services/music/musicActions.js';
+import type { Command } from '../../../types/index.js';
export default {
slashOnly: true,
category: 'Music',
data: new SlashCommandBuilder()
.setName('play')
- .setDescription('Play a song or add it to the queue')
+ .setDescription('Song abspielen oder zur Warteschlange hinzufügen')
.addStringOption((opt) =>
- opt.setName('query').setDescription('Song name or URL').setRequired(true),
+ opt.setName('query').setDescription('Songname oder URL').setRequired(true),
),
async execute(interaction, config, client) {
@@ -18,7 +19,7 @@ export default {
return;
}
- const result = await playQuery(client, interaction, interaction.options.getString('query'));
+ const result = await playQuery(client, interaction, interaction.options.getString('query')!);
await replyMusicSuccess(interaction, result.embed);
},
-};
+} satisfies Command;
diff --git a/src/commands/Music/queue.js b/src/commands/Music/modules/queue.ts
similarity index 63%
rename from src/commands/Music/queue.js
rename to src/commands/Music/modules/queue.ts
index 66faa4eb9d..367859695d 100644
--- a/src/commands/Music/queue.js
+++ b/src/commands/Music/modules/queue.ts
@@ -1,24 +1,25 @@
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { buildQueueReply } from '../../services/music/musicActions.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { buildQueueReply } from '../../../services/music/musicActions.js';
+import type { Command } from '../../../types/index.js';
export default {
slashOnly: true,
category: 'Music',
data: new SlashCommandBuilder()
.setName('queue')
- .setDescription('Show the current music queue')
+ .setDescription('Aktuelle Musik-Warteschlange anzeigen')
.addIntegerOption((opt) =>
- opt.setName('page').setDescription('Page number').setMinValue(1),
+ opt.setName('page').setDescription('Seitenzahl').setMinValue(1),
),
async execute(interaction, config, client) {
await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
const page = (interaction.options.getInteger('page') || 1) - 1;
- const payload = buildQueueReply(client, interaction.guild.id, page);
+ const payload = buildQueueReply(client, interaction.guild!.id, page);
await InteractionHelper.safeEditReply(interaction, {
embeds: payload.embeds,
components: payload.components,
});
},
-};
+} satisfies Command;
diff --git a/src/commands/Music/music.ts b/src/commands/Music/music.ts
new file mode 100644
index 0000000000..995ed19386
--- /dev/null
+++ b/src/commands/Music/music.ts
@@ -0,0 +1,19 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import musicCommand from './modules/music.js';
+import joinCommand from './modules/join.js';
+import playCommand from './modules/play.js';
+import queueCommand from './modules/queue.js';
+import nowplayingCommand from './modules/nowplaying.js';
+
+export default createRouterCommand({
+ name: 'music',
+ description: 'Musik abspielen und steuern',
+ category: 'Music',
+ routes: [
+ { legacyName: 'music', module: musicCommand, spread: true },
+ { legacyName: 'join', module: joinCommand },
+ { legacyName: 'play', module: playCommand },
+ { legacyName: 'queue', module: queueCommand },
+ { legacyName: 'nowplaying', module: nowplayingCommand },
+ ],
+});
diff --git a/src/commands/Reaction_roles/reactroles.js b/src/commands/Reaction_roles/reactroles.ts
similarity index 66%
rename from src/commands/Reaction_roles/reactroles.js
rename to src/commands/Reaction_roles/reactroles.ts
index 692443c5e9..95cfa48139 100644
--- a/src/commands/Reaction_roles/reactroles.js
+++ b/src/commands/Reaction_roles/reactroles.ts
@@ -2,7 +2,7 @@ import { getColor } from '../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, RoleSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ButtonBuilder, ButtonStyle, MessageFlags, ComponentType, EmbedBuilder, LabelBuilder, CheckboxBuilder, TextDisplayBuilder } from 'discord.js';
import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
import { logger } from '../../utils/logger.js';
-import { createError, TitanBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
+import { createError, NexoBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import { createReactionRoleMessage, hasDangerousPermissions, getAllReactionRoleMessages, deleteReactionRoleMessage } from '../../services/reactionRoleService.js';
import { logEvent, EVENT_TYPES } from '../../services/loggingService.js';
@@ -12,12 +12,23 @@ import {
} from '../../utils/panelStatus.js';
import { startDashboardSession } from '../../utils/dashboardSession.js';
import { getReactionRoleKey } from '../../utils/database/keys.js';
+import type { AutocompleteInteraction, ButtonInteraction, ChatInputCommandInteraction, Embed, Guild, GuildBasedChannel, GuildTextBasedChannel, Message, Role, StringSelectMenuInteraction } from 'discord.js';
+import type { ReactionRoleMessage } from '../../services/reactionRoleService.js';
+import type { PanelStatus } from '../../utils/panelStatus.js';
+import type { Command, NexoClient } from '../../types/index.js';
+
+/**
+ * Panels managed by this command are select-menu panels, whose `roles` is a list of role ids
+ * (the legacy emoji -> role mapping is never edited here). The JS treated `roles` as an array throughout.
+ * Panel channels are looked up from stored ids and are assumed to be guild text channels, as the JS did.
+ */
+type SelectMenuPanel = ReactionRoleMessage & { roles: string[] };
const DASHBOARD_EPHEMERAL = MessageFlags.Ephemeral;
const SELECT_OPTION_LABEL_LIMIT = 100;
const SELECT_OPTION_DESCRIPTION_LIMIT = 100;
-function truncateText(value, maxLength) {
+function truncateText(value: unknown, maxLength: number) {
const text = String(value ?? '');
return text.length > maxLength ? text.substring(0, maxLength) : text;
}
@@ -25,68 +36,68 @@ function truncateText(value, maxLength) {
export default {
data: new SlashCommandBuilder()
.setName('reactroles')
- .setDescription('Manage reaction role assignments')
+ .setDescription('Reaktionsrollen verwalten')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand =>
subcommand
.setName('setup')
- .setDescription('Set up a new reaction role panel')
+ .setDescription('Neues Reaktionsrollen-Panel einrichten')
.addChannelOption(option =>
option.setName('channel')
- .setDescription('The channel to send the reaction role message to')
+ .setDescription('Kanal, in den das Reaktionsrollen-Panel gesendet wird')
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
.setRequired(true)
)
.addStringOption(option =>
option.setName('title')
- .setDescription('Title for the reaction role panel')
+ .setDescription('Titel des Reaktionsrollen-Panels')
.setRequired(true)
)
.addStringOption(option =>
option.setName('description')
- .setDescription('Description for the reaction role panel')
+ .setDescription('Beschreibung des Reaktionsrollen-Panels')
.setRequired(true)
)
.addRoleOption(option =>
option.setName('role1')
- .setDescription('First role to add')
+ .setDescription('Erste Rolle')
.setRequired(true)
)
.addRoleOption(option =>
option.setName('role2')
- .setDescription('Second role to add')
+ .setDescription('Zweite Rolle')
.setRequired(false)
)
.addRoleOption(option =>
option.setName('role3')
- .setDescription('Third role to add')
+ .setDescription('Dritte Rolle')
.setRequired(false)
)
.addRoleOption(option =>
option.setName('role4')
- .setDescription('Fourth role to add')
+ .setDescription('Vierte Rolle')
.setRequired(false)
)
.addRoleOption(option =>
option.setName('role5')
- .setDescription('Fifth role to add')
+ .setDescription('Fünfte Rolle')
.setRequired(false)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('dashboard')
- .setDescription('Manage and configure your reaction role panels')
+ .setDescription('Deine Reaktionsrollen-Panels verwalten und konfigurieren')
.addStringOption(option =>
option
.setName('panel')
- .setDescription('Select a reaction role panel to manage')
+ .setDescription('Reaktionsrollen-Panel zum Verwalten auswählen')
.setRequired(false)
.setAutocomplete(true)
)
),
- async execute(interaction) {
+ async execute(interaction: ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'setup') {
@@ -97,18 +108,18 @@ export default {
}
},
- async autocomplete(interaction) {
+ async autocomplete(interaction: AutocompleteInteraction) {
if (interaction.commandName !== 'reactroles') return;
if (interaction.options.getSubcommand() !== 'dashboard') return;
// Autocomplete must respond within 3s. Build choices from stored panel data and
// cached channels/messages only — no network fetches — to avoid DiscordAPIError 10062.
try {
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
const client = interaction.client;
- const guild = interaction.guild;
+ const guild = interaction.guild!;
- let panels;
+ let panels: ReactionRoleMessage[];
try {
panels = await getAllReactionRoleMessages(client, guildId);
} catch {
@@ -121,18 +132,18 @@ export default {
return;
}
- const choices = [];
+ const choices: { name: string; value: string }[] = [];
for (const panel of panels) {
if (!panel.messageId || !panel.channelId) continue;
- const channel = guild.channels.cache.get(panel.channelId);
+ const channel = guild.channels.cache.get(panel.channelId) as GuildTextBasedChannel | undefined;
if (!channel) continue;
const cachedTitle = channel.messages?.cache?.get(panel.messageId)?.embeds?.[0]?.title;
const roleCount = Array.isArray(panel.roles) ? panel.roles.length : 0;
const label = cachedTitle
? `${cachedTitle} (#${channel.name})`
- : `#${channel.name} · ${roleCount} role${roleCount === 1 ? '' : 's'}`;
+ : `#${channel.name} · ${roleCount} Rolle${roleCount === 1 ? '' : 'n'}`;
choices.push({ name: label.substring(0, 100), value: panel.messageId });
if (choices.length >= 25) break;
@@ -143,84 +154,85 @@ export default {
await interaction.respond([]).catch(() => {});
}
}
-};
+} satisfies Command;
-async function handleSetup(interaction) {
+async function handleSetup(interaction: ChatInputCommandInteraction) {
const deferSuccess = await InteractionHelper.safeDefer(interaction);
if (!deferSuccess) return;
- logger.info(`Reaction role setup initiated by ${interaction.user.tag} in guild ${interaction.guild.name}`);
+ logger.info(`Reaction role setup initiated by ${interaction.user.tag} in guild ${interaction.guild!.name}`);
- const channel = interaction.options.getChannel('channel');
- const title = interaction.options.getString('title');
- const description = interaction.options.getString('description');
+ const channel = interaction.options.getChannel('channel') as GuildBasedChannel;
+ const title = interaction.options.getString('title')!;
+ const description = interaction.options.getString('description')!;
if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) {
throw createError(
`Invalid channel type: ${channel.type}`,
ErrorTypes.VALIDATION,
- 'Please select a text or announcement channel.',
+ 'Bitte wähle einen Text- oder Ankündigungskanal.',
{ channelType: channel.type }
);
}
- if (!interaction.guild.members.me.permissions.has(PermissionFlagsBits.ManageRoles)) {
+ if (!interaction.guild!.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) {
throw createError(
'Bot missing ManageRoles permission',
ErrorTypes.PERMISSION,
- 'I need the "Manage Roles" permission to set up reaction roles.',
+ 'Ich brauche die Berechtigung „Rollen verwalten“, um Reaktionsrollen einzurichten.',
{ permission: 'ManageRoles' }
);
}
- if (!channel.permissionsFor(interaction.guild.members.me).has(PermissionFlagsBits.SendMessages)) {
+ if (!channel.permissionsFor(interaction.guild!.members.me!).has(PermissionFlagsBits.SendMessages)) {
throw createError(
`Bot cannot send messages in ${channel.name}`,
ErrorTypes.PERMISSION,
- `I don't have permission to send messages in ${channel}.`,
+ `Ich habe keine Berechtigung, Nachrichten in ${channel} zu senden.`,
{ channelId: channel.id }
);
}
- const existingPanels = await getAllReactionRoleMessages(interaction.client, interaction.guildId);
+ const existingPanels = await getAllReactionRoleMessages(interaction.client, interaction.guildId!);
if (existingPanels && existingPanels.length >= 5) {
throw createError(
'Panel limit reached',
ErrorTypes.VALIDATION,
- 'Your guild has reached the maximum of 5 reaction role panels. Delete an existing panel to create a new one.',
+ 'Dein Server hat das Maximum von 5 Reaktionsrollen-Panels erreicht. Lösche ein bestehendes Panel, um ein neues zu erstellen.',
{ maxPanels: 5, currentPanels: existingPanels.length }
);
}
- const roles = [];
- const roleValidationErrors = [];
- const seenRoleIds = new Set();
+ const roles: Role[] = [];
+ const roleValidationErrors: string[] = [];
+ const seenRoleIds = new Set();
for (let i = 1; i <= 5; i++) {
- const role = interaction.options.getRole(`role${i}`);
+ // Guild-only command: resolved roles are cached Role objects.
+ const role = interaction.options.getRole(`role${i}`) as Role | null;
if (role) {
if (seenRoleIds.has(role.id)) {
- roleValidationErrors.push(`**${role.name}** - This role was selected more than once`);
+ roleValidationErrors.push(`**${role.name}** - Diese Rolle wurde mehrfach ausgewählt`);
continue;
}
- if (role.position >= interaction.guild.members.me.roles.highest.position) {
- roleValidationErrors.push(`**${role.name}** - My bot's role is positioned lower than this role in your server's role hierarchy and cannot assign it`);
+ if (role.position >= interaction.guild!.members.me!.roles.highest.position) {
+ roleValidationErrors.push(`**${role.name}** - Meine Rolle steht in der Rollenhierarchie unter dieser Rolle, daher kann ich sie nicht vergeben`);
continue;
}
if (hasDangerousPermissions(role)) {
- roleValidationErrors.push(`**${role.name}** - This role has dangerous permissions (Administrator, Manage Server, etc.)`);
+ roleValidationErrors.push(`**${role.name}** - Diese Rolle hat gefährliche Berechtigungen (Administrator, Server verwalten usw.)`);
continue;
}
if (role.managed) {
- roleValidationErrors.push(`**${role.name}** - This is a managed role (integration/bot role)`);
+ roleValidationErrors.push(`**${role.name}** - Das ist eine verwaltete Rolle (Integrations-/Bot-Rolle)`);
continue;
}
- if (role.id === interaction.guild.id) {
- roleValidationErrors.push(`**${role.name}** - Cannot use the @everyone role`);
+ if (role.id === interaction.guild!.id) {
+ roleValidationErrors.push(`**${role.name}** - Die @everyone-Rolle kann nicht verwendet werden`);
continue;
}
@@ -230,7 +242,7 @@ async function handleSetup(interaction) {
}
if (roleValidationErrors.length > 0) {
- const errorMsg = `The following roles cannot be added:\n${roleValidationErrors.join('\n')}`;
+ const errorMsg = `Folgende Rollen können nicht hinzugefügt werden:\n${roleValidationErrors.join('\n')}`;
if (roles.length === 0) {
throw createError(
@@ -242,7 +254,7 @@ async function handleSetup(interaction) {
}
await interaction.followUp({
- embeds: [warningEmbed('Role Validation Warning', errorMsg)],
+ embeds: [warningEmbed('Warnung zur Rollenprüfung', errorMsg)],
flags: MessageFlags.Ephemeral
});
}
@@ -251,21 +263,21 @@ async function handleSetup(interaction) {
throw createError(
'No roles provided',
ErrorTypes.VALIDATION,
- 'You must provide at least one valid role.',
+ 'Du musst mindestens eine gültige Rolle angeben.',
{}
);
}
- const row = new ActionRowBuilder().addComponents(
+ const row = new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId('reaction_roles')
- .setPlaceholder('Select your roles')
+ .setPlaceholder('Wähle deine Rollen')
.setMinValues(0)
.setMaxValues(roles.length)
.addOptions(
roles.map(role => ({
label: truncateText(role.name, SELECT_OPTION_LABEL_LIMIT),
- description: truncateText(`Add/remove the ${role.name} role`, SELECT_OPTION_DESCRIPTION_LIMIT),
+ description: truncateText(`Rolle ${role.name} hinzufügen/entfernen`, SELECT_OPTION_DESCRIPTION_LIMIT),
value: role.id,
emoji: '🎭'
}))
@@ -277,10 +289,10 @@ async function handleSetup(interaction) {
.setDescription(description)
.setColor(getColor('info'))
.addFields({
- name: 'Available Roles',
+ name: 'Verfügbare Rollen',
value: roles.map(role => `• ${role}`).join('\n')
})
- .setFooter({ text: 'Select roles from the dropdown menu below' });
+ .setFooter({ text: 'Wähle deine Rollen im Dropdown-Menü unten' });
const message = await channel.send({
embeds: [panelEmbed],
@@ -291,7 +303,7 @@ async function handleSetup(interaction) {
try {
await createReactionRoleMessage(
interaction.client,
- interaction.guildId,
+ interaction.guildId!,
channel.id,
message.id,
roleIds
@@ -308,35 +320,35 @@ async function handleSetup(interaction) {
try {
await logEvent({
client: interaction.client,
- guildId: interaction.guildId,
+ guildId: interaction.guildId!,
eventType: EVENT_TYPES.REACTION_ROLE_CREATE,
data: {
- description: `Reaction role panel created by ${interaction.user.tag}`,
+ description: `Reaktionsrollen-Panel erstellt von ${interaction.user.tag}`,
userId: interaction.user.id,
channelId: channel.id,
fields: [
{
- name: 'Title',
+ name: 'Titel',
value: title,
inline: false
},
{
- name: 'Channel',
+ name: 'Kanal',
value: channel.toString(),
inline: true
},
{
- name: 'Roles',
- value: `${roles.length} roles`,
+ name: 'Rollen',
+ value: `${roles.length} Rollen`,
inline: true
},
{
- name: 'Role List',
+ name: 'Rollenliste',
value: roles.map(r => r.toString()).join(','),
inline: false
},
{
- name: 'Message Link',
+ name: 'Nachrichtenlink',
value: message.url,
inline: false
}
@@ -348,13 +360,13 @@ async function handleSetup(interaction) {
}
await InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed('Success', `✅ Reaction role panel created in ${channel}!\n\n${message.url}`)]
+ embeds: [successEmbed('Erfolg', `✅ Reaktionsrollen-Panel in ${channel} erstellt!\n\n${message.url}`)]
});
}
-async function fetchPanelDiscordMessage(guild, panelData) {
+async function fetchPanelDiscordMessage(guild: Guild, panelData: SelectMenuPanel) {
try {
- const channel = guild.channels.cache.get(panelData.channelId);
+ const channel = guild.channels.cache.get(panelData.channelId) as GuildTextBasedChannel | undefined;
if (!channel) return null;
return await channel.messages.fetch(panelData.messageId).catch(() => null);
} catch {
@@ -362,41 +374,41 @@ async function fetchPanelDiscordMessage(guild, panelData) {
}
}
-async function rebuildLivePanelMessage(guild, panelData) {
+async function rebuildLivePanelMessage(guild: Guild, panelData: SelectMenuPanel) {
try {
- const channel = guild.channels.cache.get(panelData.channelId);
+ const channel = guild.channels.cache.get(panelData.channelId) as GuildTextBasedChannel | undefined;
if (!channel) return;
const msg = await channel.messages.fetch(panelData.messageId).catch(() => null);
if (!msg || !msg.embeds[0]) return;
const roleObjects = panelData.roles
.map(id => guild.roles.cache.get(id))
- .filter(Boolean);
+ .filter((role): role is Role => Boolean(role));
if (roleObjects.length === 0) return;
const currentEmbed = msg.embeds[0];
const updatedEmbed = EmbedBuilder.from(currentEmbed);
const fields = currentEmbed.fields.map(f => ({ name: f.name, value: f.value, inline: f.inline }));
- const roleFieldIdx = fields.findIndex(f => f.name === 'Available Roles');
+ const roleFieldIdx = fields.findIndex(f => (f.name === 'Verfügbare Rollen' || f.name === 'Available Roles'));
const newRoleValue = roleObjects.map(r => `• ${r}`).join('\n');
if (roleFieldIdx !== -1) {
- fields[roleFieldIdx] = { name: 'Available Roles', value: newRoleValue, inline: false };
+ fields[roleFieldIdx] = { name: 'Verfügbare Rollen', value: newRoleValue, inline: false };
} else {
- fields.push({ name: 'Available Roles', value: newRoleValue, inline: false });
+ fields.push({ name: 'Verfügbare Rollen', value: newRoleValue, inline: false });
}
updatedEmbed.setFields(fields);
- const selectRow = new ActionRowBuilder().addComponents(
+ const selectRow = new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId('reaction_roles')
- .setPlaceholder('Select your roles')
+ .setPlaceholder('Wähle deine Rollen')
.setMinValues(0)
.setMaxValues(roleObjects.length)
.addOptions(
roleObjects.map(r => ({
label: r.name.substring(0, 100),
- description: `Add/remove the ${r.name} role`.substring(0, 100),
+ description: `Rolle ${r.name} hinzufügen/entfernen`.substring(0, 100),
value: r.id,
emoji: '🎭',
})),
@@ -405,11 +417,19 @@ async function rebuildLivePanelMessage(guild, panelData) {
await msg.edit({ embeds: [updatedEmbed], components: [selectRow] });
} catch (error) {
- logger.warn('Could not rebuild live reaction role panel:', error.message);
+ logger.warn('Could not rebuild live reaction role panel:', (error as Error).message);
}
}
-async function showPanelDashboard(interaction, panelData, discordMsg, guildId, guild, client, panelStatus = null) {
+async function showPanelDashboard(
+ interaction: ChatInputCommandInteraction,
+ panelData: SelectMenuPanel,
+ discordMsg: Message | null | undefined,
+ guildId: string,
+ guild: Guild,
+ client: NexoClient,
+ panelStatus: PanelStatus | null = null,
+) {
if (!panelStatus && client) {
panelStatus = await getReactionRolePanelStatus(client, guild, panelData);
if (panelStatus.recoveredId) {
@@ -422,39 +442,45 @@ async function showPanelDashboard(interaction, panelData, discordMsg, guildId, g
await InteractionHelper.safeEditReply(interaction, { ...payload, flags: DASHBOARD_EPHEMERAL });
}
-function buildReactionRoleDashboardPayload(panelData, discordMsg, guildId, guild, panelStatus = null) {
- const channel = guild.channels.cache.get(panelData.channelId);
- const title = discordMsg?.embeds?.[0]?.title ?? 'Untitled Panel';
+function buildReactionRoleDashboardPayload(
+ panelData: SelectMenuPanel,
+ discordMsg: Message | null | undefined,
+ guildId: string,
+ guild: Guild,
+ panelStatus: PanelStatus | null = null,
+) {
+ const channel = guild.channels.cache.get(panelData.channelId) as GuildTextBasedChannel | undefined;
+ const title = discordMsg?.embeds?.[0]?.title ?? 'Panel ohne Titel';
const roleList =
panelData.roles.length > 0
? panelData.roles.map(id => `<@&${id}>`).join(',')
- : '`None`';
+ : '`Keine`';
const showRepost = panelStatus?.exists === false && panelStatus?.reason === 'panel_deleted';
const embed = new EmbedBuilder()
- .setTitle('Reaction Roles Dashboard')
+ .setTitle('Reaktionsrollen-Dashboard')
.setDescription(
- `**Title:** ${title}\n\nSelect an option below to modify a setting.${discordMsg ? `\n[Click Here to View Panel](${discordMsg.url})` : ''}`,
+ `**Titel:** ${title}\n\nWähle unten eine Option, um eine Einstellung zu ändern.${discordMsg ? `\n[Hier klicken, um das Panel anzusehen](${discordMsg.url})` : ''}`,
)
.setColor(getColor('info'))
.addFields(
- { name: 'Panel Status', value: formatPanelStatusField(panelStatus), inline: false },
- { name: 'Channel', value: channel ? `<#${channel.id}>` : '`Not found`', inline: true },
- { name: 'Roles', value: `\`${panelData.roles.length} / 25\``, inline: true },
+ { name: 'Panel-Status', value: formatPanelStatusField(panelStatus), inline: false },
+ { name: 'Kanal', value: channel ? `<#${channel.id}>` : '`Nicht gefunden`', inline: true },
+ { name: 'Rollen', value: `\`${panelData.roles.length} / 25\``, inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
- { name: 'Role List', value: roleList, inline: false },
+ { name: 'Rollenliste', value: roleList, inline: false },
)
- .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität' })
.setTimestamp();
- const buttons = [];
+ const buttons: ButtonBuilder[] = [];
if (showRepost) {
buttons.push(
new ButtonBuilder()
.setCustomId(`rr_repost_${guildId}`)
- .setLabel('Repost Panel')
+ .setLabel('Panel erneut posten')
.setStyle(ButtonStyle.Primary)
.setEmoji('📌'),
);
@@ -463,30 +489,30 @@ function buildReactionRoleDashboardPayload(panelData, discordMsg, guildId, guild
buttons.push(
new ButtonBuilder()
.setCustomId(`rr_edit_text_${guildId}`)
- .setLabel('Edit Panel Text')
+ .setLabel('Panel-Text bearbeiten')
.setStyle(ButtonStyle.Primary)
.setEmoji('✏️'),
new ButtonBuilder()
.setCustomId(`rr_delete_${guildId}`)
- .setLabel('Delete Panel')
+ .setLabel('Panel löschen')
.setStyle(ButtonStyle.Danger)
.setEmoji('🗑️'),
);
const optionsSelect = new StringSelectMenuBuilder()
.setCustomId(`rr_opts_${guildId}`)
- .setPlaceholder('Select an action...')
+ .setPlaceholder('Wähle eine Aktion...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Add Role')
- .setDescription('Add a role to this panel (up to 25 total)')
+ .setLabel('Rolle hinzufügen')
+ .setDescription('Rolle zu diesem Panel hinzufügen (max. 25)')
.setValue('add_role')
.setEmoji('➕'),
...(panelData.roles.length > 0
? [
new StringSelectMenuOptionBuilder()
- .setLabel('Remove Role')
- .setDescription('Remove a role from this panel')
+ .setLabel('Rolle entfernen')
+ .setDescription('Rolle aus diesem Panel entfernen')
.setValue('remove_role')
.setEmoji('➖'),
]
@@ -496,13 +522,13 @@ function buildReactionRoleDashboardPayload(panelData, discordMsg, guildId, guild
return {
embeds: [embed],
components: [
- new ActionRowBuilder().addComponents(buttons),
- new ActionRowBuilder().addComponents(optionsSelect),
+ new ActionRowBuilder().addComponents(buttons),
+ new ActionRowBuilder().addComponents(optionsSelect),
],
};
}
-async function migrateReactionRoleMessageId(client, guildId, panelData, newMessageId) {
+async function migrateReactionRoleMessageId(client: NexoClient, guildId: string, panelData: ReactionRoleMessage, newMessageId: string | undefined) {
if (!newMessageId || panelData.messageId === newMessageId) return;
const oldKey = getReactionRoleKey(guildId, panelData.messageId);
panelData.messageId = newMessageId;
@@ -510,47 +536,47 @@ async function migrateReactionRoleMessageId(client, guildId, panelData, newMessa
await client.db.delete(oldKey).catch(() => {});
}
-async function repostReactionRolePanel(guild, panelData, client, guildId, fallbackEmbed = null) {
- const channel = await guild.channels.fetch(panelData.channelId).catch(() => null);
+async function repostReactionRolePanel(guild: Guild, panelData: SelectMenuPanel, client: NexoClient, guildId: string, fallbackEmbed: Embed | null | undefined = null) {
+ const channel = (await guild.channels.fetch(panelData.channelId).catch(() => null)) as GuildTextBasedChannel | null;
if (!channel) {
throw createError(
'Panel channel missing',
ErrorTypes.CONFIGURATION,
- 'The configured panel channel no longer exists.',
+ 'Der eingestellte Panel-Kanal existiert nicht mehr.',
);
}
- const roleObjects = panelData.roles.map(id => guild.roles.cache.get(id)).filter(Boolean);
+ const roleObjects = panelData.roles.map(id => guild.roles.cache.get(id)).filter((role): role is Role => Boolean(role));
if (roleObjects.length === 0) {
throw createError(
'No valid roles',
ErrorTypes.VALIDATION,
- 'This panel has no valid roles left to repost.',
+ 'Dieses Panel hat keine gültigen Rollen mehr, die erneut gepostet werden können.',
);
}
- const title = fallbackEmbed?.title || 'Reaction Roles';
- const description = fallbackEmbed?.description || 'Select your roles using the menu below.';
+ const title = fallbackEmbed?.title || 'Reaktionsrollen';
+ const description = fallbackEmbed?.description || 'Wähle deine Rollen über das Menü unten.';
const panelEmbed = new EmbedBuilder()
.setTitle(title)
.setDescription(description)
.setColor(getColor('info'))
.addFields({
- name: 'Available Roles',
+ name: 'Verfügbare Rollen',
value: roleObjects.map(role => `• ${role}`).join('\n'),
});
- const row = new ActionRowBuilder().addComponents(
+ const row = new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId('reaction_roles')
- .setPlaceholder('Select your roles')
+ .setPlaceholder('Wähle deine Rollen')
.setMinValues(0)
.setMaxValues(roleObjects.length)
.addOptions(
roleObjects.map(role => ({
label: role.name.substring(0, 100),
- description: `Add/remove the ${role.name} role`.substring(0, 100),
+ description: `Rolle ${role.name} hinzufügen/entfernen`.substring(0, 100),
value: role.id,
emoji: '🎭',
})),
@@ -562,32 +588,33 @@ async function repostReactionRolePanel(guild, panelData, client, guildId, fallba
return sent;
}
-async function handleDashboard(interaction, selectedPanelId) {
+async function handleDashboard(interaction: ChatInputCommandInteraction, selectedPanelId: string | null) {
const deferSuccess = await InteractionHelper.safeDefer(interaction, { flags: DASHBOARD_EPHEMERAL });
if (!deferSuccess) return;
const client = interaction.client;
- const guildId = interaction.guild.id;
- const guild = interaction.guild;
+ const guildId = interaction.guild!.id;
+ const guild = interaction.guild!;
const panels = await getAllReactionRoleMessages(client, guildId);
if (!panels?.length) {
throw createError(
'No panels',
ErrorTypes.CONFIGURATION,
- 'No reaction role panels found. Use `/reactroles setup` first.',
+ 'Keine Reaktionsrollen-Panels gefunden. Nutze zuerst `/reactroles setup`.',
);
}
- let panelData = selectedPanelId ? panels.find(p => p.messageId === selectedPanelId) : null;
+ // See SelectMenuPanel: the dashboard edits select-menu panels whose roles are a list.
+ let panelData = (selectedPanelId ? panels.find(p => p.messageId === selectedPanelId) : null) as SelectMenuPanel | null | undefined;
if (!panelData) {
if (panels.length === 1) {
- panelData = panels[0];
+ panelData = panels[0] as SelectMenuPanel;
} else {
throw createError(
'Panel required',
ErrorTypes.VALIDATION,
- 'Multiple panels exist. Choose one using the **panel** option.',
+ 'Es gibt mehrere Panels. Wähle eins über die Option **panel**.',
);
}
}
@@ -606,7 +633,7 @@ async function handleDashboard(interaction, selectedPanelId) {
...payload,
flags: DASHBOARD_EPHEMERAL,
selectMenuId: `rr_opts_${guildId}`,
- buttonMatcher: (customId) =>
+ buttonMatcher: (customId: string) =>
customId === `rr_edit_text_${guildId}` ||
customId === `rr_delete_${guildId}` ||
customId === `rr_repost_${guildId}`,
@@ -630,7 +657,7 @@ async function handleDashboard(interaction, selectedPanelId) {
fallbackEmbed,
);
await btnInteraction.followUp({
- embeds: [successEmbed('Panel Reposted', `Reaction role panel restored in ${newMsg.channel}.`)],
+ embeds: [successEmbed('Panel erneut gepostet', `Reaktionsrollen-Panel in ${newMsg.channel} wiederhergestellt.`)],
flags: MessageFlags.Ephemeral,
});
await showPanelDashboard(
@@ -657,8 +684,8 @@ async function handleDashboard(interaction, selectedPanelId) {
});
}
-async function handleEditText(buttonInteraction, rootInteraction, panelData, guildId, guild, client) {
- const channel = guild.channels.cache.get(panelData.channelId);
+async function handleEditText(buttonInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, panelData: SelectMenuPanel, guildId: string, guild: Guild, client: NexoClient) {
+ const channel = guild.channels.cache.get(panelData.channelId) as GuildTextBasedChannel | undefined;
const discordMsg = channel
? await channel.messages.fetch(panelData.messageId).catch(() => null)
: null;
@@ -668,22 +695,22 @@ async function handleEditText(buttonInteraction, rootInteraction, panelData, gui
const modal = new ModalBuilder()
.setCustomId('rr_edit_text')
- .setTitle('Edit Panel Text')
+ .setTitle('Panel-Text bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('panel_title')
- .setLabel('Title')
+ .setLabel('Titel')
.setStyle(TextInputStyle.Short)
.setValue(currentTitle)
.setMaxLength(256)
.setMinLength(1)
.setRequired(true),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('panel_description')
- .setLabel('Description')
+ .setLabel('Beschreibung')
.setStyle(TextInputStyle.Paragraph)
.setValue(currentDesc)
.setMaxLength(2048)
@@ -698,7 +725,7 @@ async function handleEditText(buttonInteraction, rootInteraction, panelData, gui
logger.error('Error showing edit text modal:', error);
await replyUserError(buttonInteraction, {
type: ErrorTypes.UNKNOWN,
- message: 'Failed to show the edit panel text modal. Please try again.',
+ message: 'Das Formular zum Bearbeiten konnte nicht geöffnet werden. Bitte versuche es erneut.',
}).catch(() => {});
return;
}
@@ -718,18 +745,18 @@ async function handleEditText(buttonInteraction, rootInteraction, panelData, gui
if (discordMsg) {
const roleObjects = panelData.roles
.map(id => guild.roles.cache.get(id))
- .filter(Boolean);
+ .filter((role): role is Role => Boolean(role));
const updatedEmbed = EmbedBuilder.from(discordMsg.embeds[0])
.setTitle(newTitle)
.setDescription(newDescription);
if (roleObjects.length > 0) {
const fields = discordMsg.embeds[0].fields?.map(f => ({ name: f.name, value: f.value, inline: f.inline })) || [];
- const roleFieldIdx = fields.findIndex(f => f.name === 'Available Roles');
+ const roleFieldIdx = fields.findIndex(f => (f.name === 'Verfügbare Rollen' || f.name === 'Available Roles'));
const newRoleValue = roleObjects.map(r => `• ${r}`).join('\n');
if (roleFieldIdx !== -1) {
- fields[roleFieldIdx] = { name: 'Available Roles', value: newRoleValue, inline: false };
+ fields[roleFieldIdx] = { name: 'Verfügbare Rollen', value: newRoleValue, inline: false };
} else {
- fields.push({ name: 'Available Roles', value: newRoleValue, inline: false });
+ fields.push({ name: 'Verfügbare Rollen', value: newRoleValue, inline: false });
}
updatedEmbed.setFields(fields);
}
@@ -737,7 +764,7 @@ async function handleEditText(buttonInteraction, rootInteraction, panelData, gui
}
await submitted.reply({
- embeds: [successEmbed('Panel Updated', 'The title and description have been updated.')],
+ embeds: [successEmbed('Panel aktualisiert', 'Titel und Beschreibung wurden aktualisiert.')],
flags: MessageFlags.Ephemeral,
});
@@ -747,36 +774,36 @@ async function handleEditText(buttonInteraction, rootInteraction, panelData, gui
await showPanelDashboard(rootInteraction, panelData, refreshedMsg, guildId, guild, client);
}
-async function handleAddRole(selectInteraction, rootInteraction, panelData, guildId, guild, client) {
+async function handleAddRole(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, panelData: SelectMenuPanel, guildId: string, guild: Guild, client: NexoClient) {
await selectInteraction.deferUpdate();
if (panelData.roles.length >= 25) {
await replyUserError(selectInteraction, {
type: ErrorTypes.VALIDATION,
- message: 'This panel already has the maximum of 25 roles.',
+ message: 'Dieses Panel hat bereits das Maximum von 25 Rollen.',
});
return;
}
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('rr_add_role_pick')
- .setPlaceholder('Select a role to add...')
+ .setPlaceholder('Wähle eine Rolle zum Hinzufügen...')
.setMaxValues(1);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Add Role')
+ .setTitle('Rolle hinzufügen')
.setDescription(
- `**Current roles:** ${panelData.roles.length}/25\n\nSelect a role to add to this panel.`,
+ `**Aktuelle Rollen:** ${panelData.roles.length}/25\n\nWähle eine Rolle, die zu diesem Panel hinzugefügt werden soll.`,
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(roleSelect)],
+ components: [new ActionRowBuilder().addComponents(roleSelect)],
flags: MessageFlags.Ephemeral,
});
- const roleCollector = rootInteraction.channel.createMessageComponentCollector({
+ const roleCollector = (rootInteraction.channel as GuildTextBasedChannel).createMessageComponentCollector({
componentType: ComponentType.RoleSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'rr_add_role_pick',
@@ -786,40 +813,40 @@ async function handleAddRole(selectInteraction, rootInteraction, panelData, guil
roleCollector.on('collect', async roleInteraction => {
await roleInteraction.deferUpdate();
- const role = roleInteraction.roles.first();
+ const role = roleInteraction.roles.first() as Role;
if (panelData.roles.includes(role.id)) {
await replyUserError(roleInteraction, {
type: ErrorTypes.VALIDATION,
- message: `${role} is already in this panel.`,
+ message: `${role} ist bereits in diesem Panel.`,
});
return;
}
if (role.id === guild.id) {
await replyUserError(roleInteraction, {
type: ErrorTypes.VALIDATION,
- message: 'You cannot use @everyone.',
+ message: 'Du kannst @everyone nicht verwenden.',
});
return;
}
if (role.managed) {
await replyUserError(roleInteraction, {
type: ErrorTypes.VALIDATION,
- message: 'Managed/bot roles cannot be used.',
+ message: 'Verwaltete Rollen und Bot-Rollen können nicht verwendet werden.',
});
return;
}
if (hasDangerousPermissions(role)) {
await replyUserError(roleInteraction, {
type: ErrorTypes.PERMISSION,
- message: 'That role has sensitive permissions (Administrator, Manage Server, etc.) and cannot be used.',
+ message: 'Diese Rolle hat sensible Berechtigungen (Administrator, Server verwalten usw.) und kann nicht verwendet werden.',
});
return;
}
- if (role.position >= guild.members.me.roles.highest.position) {
+ if (role.position >= guild.members.me!.roles.highest.position) {
await replyUserError(roleInteraction, {
type: ErrorTypes.PERMISSION,
- message: "That role is above my highest role in the hierarchy. Move my role above it first.",
+ message: "Diese Rolle steht in der Hierarchie über meiner höchsten Rolle. Verschiebe meine Rolle zuerst darüber.",
});
return;
}
@@ -831,11 +858,11 @@ async function handleAddRole(selectInteraction, rootInteraction, panelData, guil
await rebuildLivePanelMessage(guild, panelData);
await roleInteraction.followUp({
- embeds: [successEmbed('Role Added', `${role} has been added to the panel.`)],
+ embeds: [successEmbed('Rolle hinzugefügt', `${role} wurde zum Panel hinzugefügt.`)],
flags: MessageFlags.Ephemeral,
});
- const channel = guild.channels.cache.get(panelData.channelId);
+ const channel = guild.channels.cache.get(panelData.channelId) as GuildTextBasedChannel | undefined;
const discordMsg = channel
? await channel.messages.fetch(panelData.messageId).catch(() => null)
: null;
@@ -846,13 +873,13 @@ async function handleAddRole(selectInteraction, rootInteraction, panelData, guil
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No role selected. Nothing was changed.',
+ message: 'Keine Rolle ausgewählt. Es wurde nichts geändert.',
}).catch(() => {});
}
});
}
-async function handleRemoveRole(selectInteraction, rootInteraction, panelData, panels, guildId, guild, client) {
+async function handleRemoveRole(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, panelData: SelectMenuPanel, panels: ReactionRoleMessage[], guildId: string, guild: Guild, client: NexoClient) {
await selectInteraction.deferUpdate();
const roleOptions = panelData.roles
@@ -860,19 +887,19 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p
const role = guild.roles.cache.get(id);
return role ? { label: role.name.substring(0, 100), value: id } : null;
})
- .filter(Boolean);
+ .filter((option): option is { label: string; value: string } => Boolean(option));
if (roleOptions.length === 0) {
await replyUserError(selectInteraction, {
type: ErrorTypes.USER_INPUT,
- message: 'The roles on this panel no longer exist in the server.',
+ message: 'Die Rollen dieses Panels existieren auf dem Server nicht mehr.',
});
return;
}
const removeSelect = new StringSelectMenuBuilder()
.setCustomId('rr_remove_role_pick')
- .setPlaceholder('Select a role to remove...')
+ .setPlaceholder('Wähle eine Rolle zum Entfernen...')
.setMaxValues(1)
.addOptions(
roleOptions.map(r =>
@@ -883,15 +910,15 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Remove Role')
- .setDescription('Select the role you want to remove from this panel.')
+ .setTitle('Rolle entfernen')
+ .setDescription('Wähle die Rolle, die du aus diesem Panel entfernen willst.')
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(removeSelect)],
+ components: [new ActionRowBuilder().addComponents(removeSelect)],
flags: MessageFlags.Ephemeral,
});
- const removeCollector = rootInteraction.channel.createMessageComponentCollector({
+ const removeCollector = (rootInteraction.channel as GuildTextBasedChannel).createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'rr_remove_role_pick',
@@ -907,7 +934,7 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p
panelData.roles = panelData.roles.filter(id => id !== roleId);
if (panelData.roles.length === 0) {
- const channel = guild.channels.cache.get(panelData.channelId);
+ const channel = guild.channels.cache.get(panelData.channelId) as GuildTextBasedChannel | undefined;
if (channel) {
const msg = await channel.messages.fetch(panelData.messageId).catch(() => null);
if (msg) await msg.delete().catch(() => {});
@@ -917,8 +944,8 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p
await removeInteraction.followUp({
embeds: [
successEmbed(
- '✅ Role Removed',
- 'That was the last role on the panel. The panel has been deleted.',
+ '✅ Rolle entfernt',
+ 'Das war die letzte Rolle im Panel. Das Panel wurde gelöscht.',
),
],
flags: MessageFlags.Ephemeral,
@@ -933,8 +960,8 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p
await InteractionHelper.safeEditReply(rootInteraction, {
embeds: [
new EmbedBuilder()
- .setTitle('Reaction Roles Dashboard')
- .setDescription('No panels remain. Use `/reactroles setup` to create one.')
+ .setTitle('Reaktionsrollen-Dashboard')
+ .setDescription('Keine Panels mehr vorhanden. Nutze `/reactroles setup`, um eins zu erstellen.')
.setColor(getColor('info')),
],
components: [],
@@ -945,8 +972,8 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p
await InteractionHelper.safeEditReply(rootInteraction, {
embeds: [
new EmbedBuilder()
- .setTitle('Reaction Roles Dashboard')
- .setDescription('Panel deleted. Run `/reactroles dashboard` to manage another panel.')
+ .setTitle('Reaktionsrollen-Dashboard')
+ .setDescription('Panel gelöscht. Führe `/reactroles dashboard` aus, um ein anderes Panel zu verwalten.')
.setColor(getColor('success')),
],
components: [],
@@ -961,14 +988,14 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p
await removeInteraction.followUp({
embeds: [
successEmbed(
- '✅ Role Removed',
- `${role ? role.toString() :`<@&${roleId}>`} has been removed from the panel.`,
+ '✅ Rolle entfernt',
+ `${role ? role.toString() :`<@&${roleId}>`} wurde aus dem Panel entfernt.`,
),
],
flags: MessageFlags.Ephemeral,
});
- const channel = guild.channels.cache.get(panelData.channelId);
+ const channel = guild.channels.cache.get(panelData.channelId) as GuildTextBasedChannel | undefined;
const discordMsg = channel
? await channel.messages.fetch(panelData.messageId).catch(() => null)
: null;
@@ -980,32 +1007,32 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No role selected. Nothing was changed.',
+ message: 'Keine Rolle ausgewählt. Es wurde nichts geändert.',
}).catch(() => {});
}
});
}
-async function handleDeletePanel(btnInteraction, rootInteraction, panelData, panels, guildId, guild, client) {
- const channel = guild.channels.cache.get(panelData.channelId);
+async function handleDeletePanel(btnInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, panelData: SelectMenuPanel, panels: ReactionRoleMessage[], guildId: string, guild: Guild, client: NexoClient) {
+ const channel = guild.channels.cache.get(panelData.channelId) as GuildTextBasedChannel | undefined;
const discordMsg = channel
? await channel.messages.fetch(panelData.messageId).catch(() => null)
: null;
- const title = discordMsg?.embeds?.[0]?.title ?? 'this panel';
+ const title = discordMsg?.embeds?.[0]?.title ?? 'dieses Panel';
const deleteModal = new ModalBuilder()
.setCustomId('rr_delete_confirm_modal')
- .setTitle('Delete Reaction Role Panel');
+ .setTitle('Reaktionsrollen-Panel löschen');
const deleteWarningText = new TextDisplayBuilder()
- .setContent(`⚠️ You are about to permanently delete the panel **${title}**. This will remove the Discord message and all associated reaction role assignments.`);
+ .setContent(`⚠️ Du bist dabei, das Panel **${title}** dauerhaft zu löschen. Dabei werden die Discord-Nachricht und alle zugehörigen Reaktionsrollen-Zuweisungen entfernt.`);
const deleteCheckbox = new CheckboxBuilder()
.setCustomId('delete_confirmation')
.setDefault(false);
const deleteCheckboxLabel = new LabelBuilder()
- .setLabel('I confirm — this cannot be undone')
+ .setLabel('Ich bestätige – das ist endgültig')
.setCheckboxComponent(deleteCheckbox);
deleteModal
@@ -1029,7 +1056,7 @@ async function handleDeletePanel(btnInteraction, rootInteraction, panelData, pan
const confirmed = submitted.fields.getCheckbox('delete_confirmation');
if (!confirmed) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'You must tick the confirmation checkbox to delete the panel.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Du musst das Bestätigungskästchen anhaken, um das Panel zu löschen.' });
await showPanelDashboard(rootInteraction, panelData, discordMsg, guildId, guild, client);
return;
}
@@ -1047,12 +1074,12 @@ async function handleDeletePanel(btnInteraction, rootInteraction, panelData, pan
guildId,
eventType: EVENT_TYPES.REACTION_ROLE_DELETE,
data: {
- description: `Reaction role panel deleted by ${submitted.user.tag}`,
+ description: `Reaktionsrollen-Panel gelöscht von ${submitted.user.tag}`,
userId: submitted.user.id,
channelId: panelData.channelId,
fields: [
{ name: 'Panel', value: title, inline: true },
- { name: 'Channel', value: channel ? channel.toString() : 'Unknown', inline: true },
+ { name: 'Kanal', value: channel ? channel.toString() : 'Unbekannt', inline: true },
],
},
});
@@ -1061,7 +1088,7 @@ async function handleDeletePanel(btnInteraction, rootInteraction, panelData, pan
}
await submitted.followUp({
- embeds: [successEmbed('Panel Deleted', `**${title}** has been deleted.`)],
+ embeds: [successEmbed('Panel gelöscht', `**${title}** wurde gelöscht.`)],
flags: MessageFlags.Ephemeral,
});
@@ -1074,8 +1101,8 @@ async function handleDeletePanel(btnInteraction, rootInteraction, panelData, pan
await InteractionHelper.safeEditReply(rootInteraction, {
embeds: [
new EmbedBuilder()
- .setTitle('Reaction Roles Dashboard')
- .setDescription('No panels remain. Use `/reactroles setup` to create one.')
+ .setTitle('Reaktionsrollen-Dashboard')
+ .setDescription('Keine Panels mehr vorhanden. Nutze `/reactroles setup`, um eins zu erstellen.')
.setColor(getColor('info')),
],
components: [],
@@ -1085,8 +1112,8 @@ async function handleDeletePanel(btnInteraction, rootInteraction, panelData, pan
await InteractionHelper.safeEditReply(rootInteraction, {
embeds: [
new EmbedBuilder()
- .setTitle('Reaction Roles Dashboard')
- .setDescription('Panel deleted. Run `/reactroles dashboard` to manage another panel.')
+ .setTitle('Reaktionsrollen-Dashboard')
+ .setDescription('Panel gelöscht. Führe `/reactroles dashboard` aus, um ein anderes Panel zu verwalten.')
.setColor(getColor('success')),
],
components: [],
diff --git a/src/commands/Search/modules/search_define.js b/src/commands/Search/modules/search_define.ts
similarity index 71%
rename from src/commands/Search/modules/search_define.js
rename to src/commands/Search/modules/search_define.ts
index 149ec0f3aa..dba8276325 100644
--- a/src/commands/Search/modules/search_define.js
+++ b/src/commands/Search/modules/search_define.ts
@@ -1,9 +1,26 @@
-import axios from 'axios';
+import type { SubcommandModule } from '../../../types/index.js';
+import axios, { type AxiosError } from 'axios';
import { createEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
import { handleInteractionError, replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
+interface DictionaryDefinition {
+ definition: string;
+ example?: string;
+}
+
+interface DictionaryMeaning {
+ partOfSpeech?: string;
+ definitions: DictionaryDefinition[];
+}
+
+interface DictionaryEntry {
+ word: string;
+ phonetic?: string;
+ meanings: DictionaryMeaning[];
+}
+
export default {
async execute(interaction) {
try {
@@ -12,7 +29,7 @@ export default {
return;
}
- const word = interaction.options.getString('word');
+ const word = interaction.options.getString('word')!;
if (word.length < 2) {
logger.warn('Define command - word too short', {
@@ -20,16 +37,16 @@ export default {
word: word,
guildId: interaction.guildId
});
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Please enter a word with at least 2 characters.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Bitte gib ein Wort mit mindestens 2 Zeichen ein.' });
}
- const response = await axios.get(
+ const response = await axios.get(
`https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(word)}`,
{ timeout: 5000 }
);
if (!response.data || response.data.length === 0) {
- return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `No definitions found for "${word}".` });
+ return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Keine Definitionen für „${word}“ gefunden.` });
}
const data = response.data[0];
@@ -45,7 +62,7 @@ export default {
.map((def, idx) => {
let text = `${idx + 1}. ${def.definition}`;
if (def.example) {
- text += `\n *Example: ${def.example}*`;
+ text += `\n *Beispiel: ${def.example}*`;
}
return text;
})
@@ -60,7 +77,7 @@ export default {
}
});
- embed.setFooter({ text: 'Powered by Free Dictionary API' });
+ embed.setFooter({ text: 'Bereitgestellt von der Free Dictionary API' });
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
@@ -72,17 +89,18 @@ export default {
});
} catch (error) {
+ const err = error as AxiosError;
logger.error('Dictionary lookup error', {
- error: error.message,
- stack: error.stack,
+ error: err.message,
+ stack: err.stack,
userId: interaction.user.id,
word: interaction.options.getString('word'),
guildId: interaction.guildId,
commandName: 'define'
});
- if (error.response?.status === 404) {
- await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `No definitions found for "${interaction.options.getString('word')}".` });
+ if (err.response?.status === 404) {
+ await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Keine Definitionen für „${interaction.options.getString('word')}“ gefunden.` });
} else {
await handleInteractionError(interaction, error, {
commandName: 'define',
@@ -91,4 +109,4 @@ export default {
}
}
},
-};
+} satisfies SubcommandModule;
diff --git a/src/commands/Search/modules/search_google.js b/src/commands/Search/modules/search_google.ts
similarity index 68%
rename from src/commands/Search/modules/search_google.js
rename to src/commands/Search/modules/search_google.ts
index c277046d49..4ae900a16e 100644
--- a/src/commands/Search/modules/search_google.js
+++ b/src/commands/Search/modules/search_google.ts
@@ -1,18 +1,19 @@
+import type { SubcommandModule } from '../../../types/index.js';
import { createEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
export default {
async execute(interaction) {
- const query = interaction.options.getString('query');
+ const query = interaction.options.getString('query')!;
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
const embed = createEmbed({
- title: 'Google Search',
- description: `[Search for "${query}"](${searchUrl})`,
+ title: 'Google-Suche',
+ description: `[Nach „${query}“ suchen](${searchUrl})`,
color: 'info'
})
- .setFooter({ text: 'Google Search Results' });
+ .setFooter({ text: 'Google-Suchergebnisse' });
await InteractionHelper.safeReply(interaction, { embeds: [embed] });
@@ -23,4 +24,4 @@ export default {
commandName: 'google'
});
},
-};
+} satisfies SubcommandModule;
diff --git a/src/commands/Search/modules/search_urban.js b/src/commands/Search/modules/search_urban.ts
similarity index 70%
rename from src/commands/Search/modules/search_urban.js
rename to src/commands/Search/modules/search_urban.ts
index b382fe30c0..22331b839c 100644
--- a/src/commands/Search/modules/search_urban.js
+++ b/src/commands/Search/modules/search_urban.ts
@@ -1,13 +1,28 @@
-import axios from 'axios';
+import type { SubcommandModule } from '../../../types/index.js';
+import axios, { type AxiosError } from 'axios';
import { createEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
import { handleInteractionError, replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
+interface UrbanDefinition {
+ word: string;
+ definition: string;
+ example: string;
+ permalink: string;
+ thumbs_up: number;
+ thumbs_down: number;
+ author?: string;
+}
+
+interface UrbanResponse {
+ list?: UrbanDefinition[];
+}
+
export default {
async execute(interaction) {
try {
- const term = interaction.options.getString('term');
+ const term = interaction.options.getString('term')!;
if (term.length < 2) {
logger.warn('Urban command - term too short', {
@@ -15,10 +30,10 @@ export default {
term: term,
guildId: interaction.guildId
});
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Please enter a term with at least 2 characters.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Bitte gib einen Begriff mit mindestens 2 Zeichen ein.' });
}
- let deferTimer = null;
+ let deferTimer: ReturnType | null = null;
const clearDeferTimer = () => {
if (deferTimer) {
clearTimeout(deferTimer);
@@ -36,17 +51,17 @@ export default {
});
}, 1500);
- const response = await axios.get(
+ const response = await axios.get(
`https://api.urbandictionary.com/v0/define?term=${encodeURIComponent(term)}`,
{ timeout: 5000 }
);
clearDeferTimer();
if (!response.data?.list?.length) {
- return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `No definitions found for "${term}" on Urban Dictionary.` });
+ return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Keine Definitionen für „${term}“ im Urban Dictionary gefunden.` });
}
- const definition = response.data.list[0];
+ const definition = response.data.list![0];
const cleanDefinition = definition.definition.replace(/\[|\]/g, '');
const cleanExample = definition.example.replace(/\[|\]/g, '');
@@ -56,7 +71,7 @@ export default {
const formattedExample = cleanExample
? `*"${cleanExample.replace(/\n/g, ' ').slice(0, 500)}..."*`
- : '*No example provided*';
+ : '*Kein Beispiel vorhanden*';
const embed = createEmbed({
title: definition.word,
@@ -66,18 +81,18 @@ export default {
.setURL(definition.permalink)
.addFields(
{
- name: 'Example',
+ name: 'Beispiel',
value: formattedExample,
inline: false
},
{
- name: 'Stats',
+ name: 'Statistik',
value: `${definition.thumbs_up.toLocaleString()} • ${definition.thumbs_down.toLocaleString()}`,
inline: true
},
{
- name: 'Author',
- value: definition.author || 'Anonymous',
+ name: 'Autor',
+ value: definition.author || 'Anonym',
inline: true
}
)
@@ -96,20 +111,21 @@ export default {
});
} catch (error) {
+ const err = error as AxiosError;
logger.error('Urban Dictionary error', {
- error: error.message,
- stack: error.stack,
+ error: err.message,
+ stack: err.stack,
userId: interaction.user.id,
term: interaction.options.getString('term'),
guildId: interaction.guildId,
- apiStatus: error.response?.status,
+ apiStatus: err.response?.status,
commandName: 'urban'
});
- if (error.response?.status === 404 || !error.response) {
- await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `No definitions found for "${interaction.options.getString('term')}" on Urban Dictionary.` });
- } else if (error.response?.status === 429) {
- await replyUserError(interaction, { type: ErrorTypes.RATE_LIMIT, message: 'Too many requests to Urban Dictionary. Please try again in a few minutes.' });
+ if (err.response?.status === 404 || !err.response) {
+ await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Keine Definitionen für „${interaction.options.getString('term')}“ im Urban Dictionary gefunden.` });
+ } else if (err.response?.status === 429) {
+ await replyUserError(interaction, { type: ErrorTypes.RATE_LIMIT, message: 'Zu viele Anfragen an das Urban Dictionary. Bitte versuch es in ein paar Minuten noch einmal.' });
} else {
await handleInteractionError(interaction, error, {
commandName: 'urban',
@@ -118,4 +134,4 @@ export default {
}
}
},
-};
+} satisfies SubcommandModule;
diff --git a/src/commands/Search/search.js b/src/commands/Search/search.ts
similarity index 61%
rename from src/commands/Search/search.js
rename to src/commands/Search/search.ts
index 52c1871902..6861792048 100644
--- a/src/commands/Search/search.js
+++ b/src/commands/Search/search.ts
@@ -1,3 +1,4 @@
+import type { Command } from '../../types/index.js';
import { SlashCommandBuilder } from 'discord.js';
import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
@@ -8,32 +9,32 @@ import searchUrban from './modules/search_urban.js';
export default {
data: new SlashCommandBuilder()
.setName('search')
- .setDescription('Search the web and dictionaries')
+ .setDescription('Im Web und in Wörterbüchern suchen')
.addSubcommand(subcommand =>
subcommand
.setName('define')
- .setDescription('Look up a word definition')
+ .setDescription('Die englische Definition eines Worts nachschlagen')
.addStringOption(option =>
option.setName('word')
- .setDescription('The word to look up')
+ .setDescription('Das (englische) Wort, das du nachschlagen willst')
.setRequired(true))
)
.addSubcommand(subcommand =>
subcommand
.setName('google')
- .setDescription('Search Google')
+ .setDescription('Bei Google suchen')
.addStringOption(option =>
option.setName('query')
- .setDescription('What would you like to search for?')
+ .setDescription('Wonach möchtest du suchen?')
.setRequired(true))
)
.addSubcommand(subcommand =>
subcommand
.setName('urban')
- .setDescription('Search Urban Dictionary for definitions')
+ .setDescription('Definitionen im Urban Dictionary suchen')
.addStringOption(option =>
option.setName('term')
- .setDescription('The term to look up on Urban Dictionary')
+ .setDescription('Der Begriff, den du im Urban Dictionary suchen willst')
.setRequired(true))
),
@@ -42,13 +43,13 @@ export default {
switch (subcommand) {
case 'define':
- return await searchDefine.execute(interaction, config, client);
+ return await searchDefine.execute(interaction);
case 'google':
- return await searchGoogle.execute(interaction, config, client);
+ return await searchGoogle.execute(interaction);
case 'urban':
- return await searchUrban.execute(interaction, config, client);
+ return await searchUrban.execute(interaction);
default:
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unbekannter Subcommand' });
}
}
-};
+} satisfies Command;
diff --git a/src/commands/ServerStats/modules/serverstats_create.js b/src/commands/ServerStats/modules/serverstats_create.ts
similarity index 56%
rename from src/commands/ServerStats/modules/serverstats_create.js
rename to src/commands/ServerStats/modules/serverstats_create.ts
index e5e750a933..44e6f00771 100644
--- a/src/commands/ServerStats/modules/serverstats_create.js
+++ b/src/commands/ServerStats/modules/serverstats_create.ts
@@ -1,13 +1,16 @@
import { PermissionFlagsBits, ChannelType } from 'discord.js';
+import type { ServerCounter } from '../../../services/serverstatsService.js';
import { createEmbed, successEmbed } from '../../../utils/embeds.js';
import { getServerCounters, saveServerCounters, updateCounter, getCounterBaseName, getCounterTypeLabel } from '../../../services/serverstatsService.js';
import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
-export async function handleCreate(interaction, client) {
- const guild = interaction.guild;
- const type = interaction.options.getString("type");
+import type { ChatInputCommandInteraction, GuildMember } from 'discord.js';
+import type { NexoClient } from '../../../types/index.js';
+export async function handleCreate(interaction: ChatInputCommandInteraction, client: NexoClient) {
+ const guild = interaction.guild!;
+ const type = interaction.options.getString("type")!;
const channelType = interaction.options.getString("channel_type");
const category = interaction.options.getChannel("category");
@@ -18,14 +21,14 @@ export async function handleCreate(interaction, client) {
return;
}
- if (!interaction.member.permissions.has(PermissionFlagsBits.ManageChannels)) {
- await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Channels** permission to create counters.' }).catch(logger.error);
+ if (!(interaction.member as GuildMember).permissions.has(PermissionFlagsBits.ManageChannels)) {
+ await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Kanäle verwalten**, um Zähler zu erstellen.' }).catch(logger.error);
return;
}
try {
if (!category || category.type !== ChannelType.GuildCategory) {
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Please select a valid category for the counter channel.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Bitte wähle eine gültige Kategorie für den Zählerkanal.' }).catch(logger.error);
return;
}
@@ -38,7 +41,7 @@ export async function handleCreate(interaction, client) {
if (duplicateType) {
const duplicateChannel = guild.channels.cache.get(duplicateType.channelId);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `A **${getCounterTypeLabel(type)}** counter already exists for this server${duplicateChannel ? ` in ${duplicateChannel}` : ''}. Delete it first before creating another.` }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Auf diesem Server gibt es bereits einen **${getCounterTypeLabel(type)}**-Zähler${duplicateChannel ? ` in ${duplicateChannel}` : ''}. Lösche ihn zuerst, bevor du einen neuen erstellst.` }).catch(logger.error);
return;
}
@@ -46,16 +49,16 @@ export async function handleCreate(interaction, client) {
name: baseChannelName,
type: targetChannelType,
parent: category.id,
- reason: `Counter channel created by ${interaction.user.tag}`
+ reason: `Zählerkanal erstellt von ${interaction.user.tag}`
});
const existingCounter = counters.find(c => c.channelId === targetChannel.id);
if (existingCounter) {
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `A counter already exists for channel **${targetChannel.name}**. Please delete it first or choose a different type.` }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Für den Kanal **${targetChannel.name}** gibt es bereits einen Zähler. Lösche ihn zuerst oder wähle einen anderen Typ.` }).catch(logger.error);
return;
}
- const newCounter = {
+ const newCounter: ServerCounter = {
id: Date.now().toString(),
type: type,
channelId: targetChannel.id,
@@ -68,23 +71,23 @@ export async function handleCreate(interaction, client) {
const saved = await saveServerCounters(client, guild.id, counters);
if (!saved) {
- await targetChannel.delete('Counter creation failed during save').catch(() => null);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Failed to save counter data. Please try again.' }).catch(logger.error);
+ await targetChannel.delete('Zählererstellung beim Speichern fehlgeschlagen').catch(() => null);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Zählerdaten konnten nicht gespeichert werden. Bitte versuche es erneut.' }).catch(logger.error);
return;
}
const updated = await updateCounter(client, guild, newCounter);
if (!updated) {
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Counter created but failed to update channel name. The counter will update on the next scheduled run.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Zähler erstellt, aber der Kanalname konnte nicht aktualisiert werden. Er wird beim nächsten geplanten Durchlauf aktualisiert.' }).catch(logger.error);
return;
}
await InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed(`**Counter Created Successfully!**\n\n**Type:** ${getCounterTypeLabel(type)}\n**Channel Type:** ${targetChannel.type === ChannelType.GuildVoice ? 'voice' : 'text'}\n**Category:** ${category}\n**Channel:** ${targetChannel}\n**Channel Name:** ${targetChannel.name}\n**Counter ID:** \`${newCounter.id}\`\n\nThe counter will automatically update every 15 minutes.\n\nUse \`/serverstats list\` to view all counters.`)]
+ embeds: [successEmbed(`**Zähler erfolgreich erstellt!**\n\n**Typ:** ${getCounterTypeLabel(type)}\n**Kanaltyp:** ${targetChannel.type === ChannelType.GuildVoice ? 'Sprachkanal' : 'Textkanal'}\n**Kategorie:** ${category}\n**Kanal:** ${targetChannel}\n**Kanalname:** ${targetChannel.name}\n**Zähler-ID:** \`${newCounter.id}\`\n\nDer Zähler wird automatisch alle 15 Minuten aktualisiert.\n\nNutze \`/serverstats list\`, um alle Zähler anzusehen.`)]
}).catch(logger.error);
} catch (error) {
logger.error("Error creating counter:", error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while creating the counter. Please try again.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Erstellen des Zählers ist ein Fehler aufgetreten. Bitte versuche es erneut.' }).catch(logger.error);
}
}
\ No newline at end of file
diff --git a/src/commands/ServerStats/modules/serverstats_delete.js b/src/commands/ServerStats/modules/serverstats_delete.ts
similarity index 61%
rename from src/commands/ServerStats/modules/serverstats_delete.js
rename to src/commands/ServerStats/modules/serverstats_delete.ts
index 54e3e9ccfc..5fe3892668 100644
--- a/src/commands/ServerStats/modules/serverstats_delete.js
+++ b/src/commands/ServerStats/modules/serverstats_delete.ts
@@ -6,8 +6,10 @@ import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { replyUserError, ErrorTypes, createError, wrapServiceBoundary } from '../../../utils/errorHandler.js';
-export async function handleDelete(interaction, client) {
- const guild = interaction.guild;
+import type { ChatInputCommandInteraction, Guild, GuildMember } from 'discord.js';
+import type { NexoClient } from '../../../types/index.js';
+export async function handleDelete(interaction: ChatInputCommandInteraction, client: NexoClient) {
+ const guild = interaction.guild!;
const counterId = interaction.options.getString("counter-id");
try {
@@ -17,8 +19,8 @@ export async function handleDelete(interaction, client) {
return;
}
- if (!interaction.member.permissions.has(PermissionFlagsBits.ManageChannels)) {
- await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Channels** permission to delete counters.' }).catch(logger.error);
+ if (!(interaction.member as GuildMember).permissions.has(PermissionFlagsBits.ManageChannels)) {
+ await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Kanäle verwalten**, um Zähler zu löschen.' }).catch(logger.error);
return;
}
@@ -26,32 +28,32 @@ export async function handleDelete(interaction, client) {
const counters = await getServerCounters(client, guild.id);
if (counters.length === 0) {
- await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'No counters found to delete.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Keine Zähler zum Löschen gefunden.' }).catch(logger.error);
return;
}
const counterToDelete = counters.find(c => c.id === counterId);
if (!counterToDelete) {
- await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Counter with ID \`${counterId}\` not found. Use \`/serverstats list\` to see all counters.` }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Zähler mit der ID \`${counterId}\` nicht gefunden. Nutze \`/serverstats list\`, um alle Zähler anzusehen.` }).catch(logger.error);
return;
}
const channel = guild.channels.cache.get(counterToDelete.channelId);
const embed = createEmbed({
- title: "Delete Counter & Channel",
- description: `Are you sure you want to delete this counter and its channel?\n\n**ID:** \`${counterToDelete.id}\`\n**Type:** ${getCounterTypeDisplay(counterToDelete.type)}\n**Channel:** ${channel || 'Deleted Channel'}\n\n **The channel will be permanently deleted!**`,
+ title: "Zähler & Kanal löschen",
+ description: `Willst du diesen Zähler und seinen Kanal wirklich löschen?\n\n**ID:** \`${counterToDelete.id}\`\n**Typ:** ${getCounterTypeDisplay(counterToDelete.type)}\n**Kanal:** ${channel || 'Gelöschter Kanal'}\n\n **Der Kanal wird dauerhaft gelöscht!**`,
color: getColor('error')
});
- const row = new ActionRowBuilder().addComponents(
+ const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`counter-delete:confirm:${counterToDelete.id}:${interaction.user.id}`)
- .setLabel("Confirm Delete")
+ .setLabel("Löschen bestätigen")
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`counter-delete:cancel:${counterToDelete.id}:${interaction.user.id}`)
- .setLabel("Cancel")
+ .setLabel("Abbrechen")
.setStyle(ButtonStyle.Secondary)
);
@@ -59,11 +61,11 @@ export async function handleDelete(interaction, client) {
} catch (error) {
logger.error("Error in handleDelete:", error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while fetching counters. Please try again.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Abrufen der Zähler ist ein Fehler aufgetreten. Bitte versuche es erneut.' }).catch(logger.error);
}
}
-export const performDeletionByCounterId = wrapServiceBoundary(async function performDeletionByCounterId(client, guild, counterId) {
+export const performDeletionByCounterId = wrapServiceBoundary(async function performDeletionByCounterId(client: NexoClient, guild: Guild, counterId: string) {
const counters = await getServerCounters(client, guild.id);
const counter = counters.find(c => c.id === counterId);
@@ -71,7 +73,7 @@ export const performDeletionByCounterId = wrapServiceBoundary(async function per
throw createError(
'Counter not found',
ErrorTypes.USER_INPUT,
- `Counter with ID \`${counterId}\` was not found.`,
+ `Zähler mit der ID \`${counterId}\` wurde nicht gefunden.`,
{ guildId: guild.id, counterId, operation: 'performDeletionByCounterId' }
);
}
@@ -83,7 +85,7 @@ export const performDeletionByCounterId = wrapServiceBoundary(async function per
throw createError(
'Counter delete failed',
ErrorTypes.DATABASE,
- 'Failed to delete counter. Please try again.',
+ 'Zähler konnte nicht gelöscht werden. Bitte versuche es erneut.',
{ guildId: guild.id, counterId, operation: 'performDeletionByCounterId' }
);
}
@@ -93,30 +95,30 @@ export const performDeletionByCounterId = wrapServiceBoundary(async function per
if (channel) {
try {
- await channel.delete(`Counter deleted - removing channel: ${counter.id}`);
+ await channel.delete(`Zähler gelöscht – Kanal wird entfernt: ${counter.id}`);
channelDeleted = true;
} catch (error) {
logger.error("Error deleting channel:", error);
}
}
- let message = `✅ **Counter Deleted Successfully!**\n\n**ID:** \`${counter.id}\`\n**Type:** ${getCounterTypeDisplay(counter.type)}`;
+ let message = `✅ **Zähler erfolgreich gelöscht!**\n\n**ID:** \`${counter.id}\`\n**Typ:** ${getCounterTypeDisplay(counter.type)}`;
if (channelDeleted) {
- message += `\n**Channel:** ${channel.name} (deleted)`;
+ message += `\n**Kanal:** ${channel!.name} (gelöscht)`;
} else if (channel) {
- message += `\n**Channel:** ${channel.name} (failed to delete)`;
+ message += `\n**Kanal:** ${channel.name} (Löschen fehlgeschlagen)`;
} else {
- message += `\n**Channel:** Already deleted`;
+ message += `\n**Kanal:** Bereits gelöscht`;
}
return { message };
}, {
service: 'serverstats',
operation: 'performDeletionByCounterId',
- userMessage: 'An error occurred while deleting the counter. Please try again.',
+ userMessage: 'Beim Löschen des Zählers ist ein Fehler aufgetreten. Bitte versuche es erneut.',
});
-function getCounterTypeDisplay(type) {
+function getCounterTypeDisplay(type: string) {
return `${getCounterEmoji(type)} ${getCounterTypeLabel(type)}`;
}
\ No newline at end of file
diff --git a/src/commands/ServerStats/modules/serverstats_list.js b/src/commands/ServerStats/modules/serverstats_list.ts
similarity index 58%
rename from src/commands/ServerStats/modules/serverstats_list.js
rename to src/commands/ServerStats/modules/serverstats_list.ts
index ad6d789227..9a22827888 100644
--- a/src/commands/ServerStats/modules/serverstats_list.js
+++ b/src/commands/ServerStats/modules/serverstats_list.ts
@@ -1,13 +1,16 @@
import { getColor } from '../../../config/bot.js';
import { PermissionFlagsBits } from 'discord.js';
+import type { ServerCounter } from '../../../services/serverstatsService.js';
import { createEmbed } from '../../../utils/embeds.js';
import { getServerCounters, saveServerCounters, getCounterEmoji as getCounterTypeEmoji, getCounterTypeLabel, getGuildCounterStats } from '../../../services/serverstatsService.js';
import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
-export async function handleList(interaction, client) {
- const guild = interaction.guild;
+import type { ChatInputCommandInteraction, GuildMember } from 'discord.js';
+import type { NexoClient } from '../../../types/index.js';
+export async function handleList(interaction: ChatInputCommandInteraction, client: NexoClient) {
+ const guild = interaction.guild!;
try {
await InteractionHelper.safeDefer(interaction);
@@ -16,8 +19,8 @@ export async function handleList(interaction, client) {
return;
}
- if (!interaction.member.permissions.has(PermissionFlagsBits.ManageChannels)) {
- await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Channels** permission to view counters.' }).catch(logger.error);
+ if (!(interaction.member as GuildMember).permissions.has(PermissionFlagsBits.ManageChannels)) {
+ await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Kanäle verwalten**, um Zähler anzusehen.' }).catch(logger.error);
return;
}
@@ -25,8 +28,8 @@ export async function handleList(interaction, client) {
const counters = await getServerCounters(client, guild.id);
const stats = await getGuildCounterStats(guild);
- const validCounters = [];
- const orphanedCounters = [];
+ const validCounters: ServerCounter[] = [];
+ const orphanedCounters: ServerCounter[] = [];
for (const counter of counters) {
const channel = guild.channels.cache.get(counter.channelId);
@@ -45,25 +48,25 @@ export async function handleList(interaction, client) {
if (validCounters.length === 0) {
const embed = createEmbed({
- title: "Server Counters",
- description: "No counters have been set up for this server yet.\n\nUse `/serverstats create` to set up your first counter!",
+ title: "Server-Zähler",
+ description: "Für diesen Server wurden noch keine Zähler eingerichtet.\n\nNutze `/serverstats create`, um deinen ersten Zähler einzurichten!",
color: getColor('warning')
});
embed.addFields({
- name: "**Available Counter Types**",
- value: "**Members + Bots** - Total server members\n **Members Only** - Human members only\n **Bots Only** - Bot members only",
+ name: "**Verfügbare Zählertypen**",
+ value: "**Mitglieder + Bots** - Alle Servermitglieder\n **Nur Mitglieder** - Nur menschliche Mitglieder\n **Nur Bots** - Nur Bot-Mitglieder",
inline: false
});
embed.addFields({
- name: "**Usage Examples**",
+ name: "**Beispiele**",
value: "`/serverstats create type:members channel_type:voice category:Stats`\n`/serverstats create type:bots channel_type:text category:Server Info`\n`/serverstats list`",
inline: false
});
embed.setFooter({
- text: "Counter System • Automatic updates every 15 minutes"
+ text: "Zählersystem • Automatische Aktualisierung alle 15 Minuten"
});
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }).catch(logger.error);
@@ -71,8 +74,8 @@ export async function handleList(interaction, client) {
}
const embed = createEmbed({
- title: `Server Counters (${validCounters.length})`,
- description: "Here are all the active counters for this server.\n\nCounters automatically update every 15 minutes.",
+ title: `Server-Zähler (${validCounters.length})`,
+ description: "Hier sind alle aktiven Zähler dieses Servers.\n\nZähler werden automatisch alle 15 Minuten aktualisiert.",
color: getColor('info')
});
@@ -87,32 +90,32 @@ export async function handleList(interaction, client) {
}
const currentCount = getCurrentCount(stats, counter.type);
- const status = channel.name.includes(':') ? '✅ Active' : '⚠️ Not Updated';
+ const status = channel.name.includes(':') ? '✅ Aktiv' : '⚠️ Nicht aktualisiert';
embed.addFields({
- name: `${getCounterTypeEmoji(counter.type)} Counter #${i + 1} - ${channel.name}`,
- value: `**ID:** \`${counter.id}\`\n**Type:** ${getCounterTypeDisplay(counter.type)}\n**Channel:** ${channel}\n**Current Count:** ${currentCount}\n**Status:** ${status}\n**Created:** ${new Date(counter.createdAt).toLocaleDateString()}`,
+ name: `${getCounterTypeEmoji(counter.type)} Zähler #${i + 1} - ${channel.name}`,
+ value: `**ID:** \`${counter.id}\`\n**Typ:** ${getCounterTypeDisplay(counter.type)}\n**Kanal:** ${channel}\n**Aktueller Stand:** ${currentCount}\n**Status:** ${status}\n**Erstellt:** ${new Date(counter.createdAt).toLocaleDateString()}`,
inline: false
});
}
embed.addFields({
- name: "**Statistics**",
- value: `**Total Counters:** ${validCounters.length}\n**Active Counters:** ${validCounters.filter(c => {
+ name: "**Statistiken**",
+ value: `**Zähler gesamt:** ${validCounters.length}\n**Aktive Zähler:** ${validCounters.filter(c => {
const channel = guild.channels.cache.get(c.channelId);
return channel && channel.name.includes(':');
- }).length}\n**Next Update:** `,
+ }).length}\n**Nächste Aktualisierung:** `,
inline: false
});
embed.addFields({
- name: "**Management Commands**",
- value: "`/serverstats create` - Create new counter\n`/serverstats update` - Update existing counter\n`/serverstats delete` - Delete counter",
+ name: "**Verwaltungs-Commands**",
+ value: "`/serverstats create` - Neuen Zähler erstellen\n`/serverstats update` - Bestehenden Zähler aktualisieren\n`/serverstats delete` - Zähler löschen",
inline: false
});
embed.setFooter({
- text: "Counter System • Automatic updates every 15 minutes"
+ text: "Zählersystem • Automatische Aktualisierung alle 15 Minuten"
});
embed.setTimestamp();
@@ -120,19 +123,19 @@ export async function handleList(interaction, client) {
} catch (error) {
logger.error("Error displaying counters:", error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while fetching counters. Please try again.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Abrufen der Zähler ist ein Fehler aufgetreten. Bitte versuche es erneut.' }).catch(logger.error);
}
}
-function getCounterTypeDisplay(type) {
+function getCounterTypeDisplay(type: string) {
return `${getCounterTypeEmoji(type)} ${getCounterTypeLabel(type)}`;
}
-function getCounterEmoji(type) {
+function getCounterEmoji(type: string) {
return getCounterTypeEmoji(type);
}
-function getCurrentCount(stats, type) {
+function getCurrentCount(stats: Awaited>, type: string) {
switch (type) {
case "members":
return stats.totalCount;
diff --git a/src/commands/ServerStats/modules/serverstats_update.js b/src/commands/ServerStats/modules/serverstats_update.ts
similarity index 57%
rename from src/commands/ServerStats/modules/serverstats_update.js
rename to src/commands/ServerStats/modules/serverstats_update.ts
index b2ebb45434..f28113c94c 100644
--- a/src/commands/ServerStats/modules/serverstats_update.js
+++ b/src/commands/ServerStats/modules/serverstats_update.ts
@@ -5,8 +5,10 @@ import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
-export async function handleUpdate(interaction, client) {
- const guild = interaction.guild;
+import type { ChatInputCommandInteraction, GuildMember } from 'discord.js';
+import type { NexoClient } from '../../../types/index.js';
+export async function handleUpdate(interaction: ChatInputCommandInteraction, client: NexoClient) {
+ const guild = interaction.guild!;
const counterId = interaction.options.getString("counter-id");
const newType = interaction.options.getString("type");
@@ -17,13 +19,13 @@ export async function handleUpdate(interaction, client) {
return;
}
- if (!interaction.member.permissions.has(PermissionFlagsBits.ManageChannels)) {
- await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Channels** permission to update counters.' }).catch(logger.error);
+ if (!(interaction.member as GuildMember).permissions.has(PermissionFlagsBits.ManageChannels)) {
+ await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Kanäle verwalten**, um Zähler zu aktualisieren.' }).catch(logger.error);
return;
}
if (!newType) {
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You must provide a new counter type to update.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du musst einen neuen Zählertyp angeben.' }).catch(logger.error);
return;
}
@@ -32,7 +34,7 @@ export async function handleUpdate(interaction, client) {
const counterIndex = counters.findIndex(c => c.id === counterId);
if (counterIndex === -1) {
- await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Counter with ID \`${counterId}\` not found. Use \`/serverstats list\` to see all counters.` }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Zähler mit der ID \`${counterId}\` nicht gefunden. Nutze \`/serverstats list\`, um alle Zähler anzusehen.` }).catch(logger.error);
return;
}
@@ -40,7 +42,7 @@ export async function handleUpdate(interaction, client) {
const oldChannel = guild.channels.cache.get(counter.channelId);
if (!oldChannel) {
- await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'The channel for this counter no longer exists. You cannot update a counter for a deleted channel.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Der Kanal dieses Zählers existiert nicht mehr. Zähler gelöschter Kanäle können nicht aktualisiert werden.' }).catch(logger.error);
return;
}
@@ -48,7 +50,7 @@ export async function handleUpdate(interaction, client) {
const existingTypeCounter = counters.find(c => c.type === newType && c.id !== counter.id);
if (existingTypeCounter) {
const existingChannel = guild.channels.cache.get(existingTypeCounter.channelId);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `A **${getCounterTypeLabel(newType)}** counter already exists for this server${existingChannel ? ` in ${existingChannel}` : ''}. Delete it first before reusing that type.` }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Auf diesem Server gibt es bereits einen **${getCounterTypeLabel(newType)}**-Zähler${existingChannel ? ` in ${existingChannel}` : ''}. Lösche ihn zuerst, bevor du diesen Typ erneut verwendest.` }).catch(logger.error);
return;
}
}
@@ -60,25 +62,25 @@ export async function handleUpdate(interaction, client) {
const saved = await saveServerCounters(client, guild.id, counters);
if (!saved) {
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Failed to save updated counter data. Please try again.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Die aktualisierten Zählerdaten konnten nicht gespeichert werden. Bitte versuche es erneut.' }).catch(logger.error);
return;
}
const updatedCounter = counters[counterIndex];
const updated = await updateCounter(client, guild, updatedCounter);
if (!updated) {
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Counter updated but failed to update channel name. The counter will update on the next scheduled run.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Zähler aktualisiert, aber der Kanalname konnte nicht aktualisiert werden. Er wird beim nächsten geplanten Durchlauf aktualisiert.' }).catch(logger.error);
return;
}
const finalChannel = guild.channels.cache.get(updatedCounter.channelId);
await InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed(`**Counter Updated Successfully!**\n\n**Counter ID:** \`${counterId}\`\n**Type Changed:** ${getCounterEmoji(oldType)} ${getCounterTypeLabel(oldType)} → ${getCounterEmoji(newType)} ${getCounterTypeLabel(newType)}\n\n**Current Settings:**\n**Type:** ${getCounterEmoji(updatedCounter.type)} ${getCounterTypeLabel(updatedCounter.type)}\n**Channel:** ${finalChannel}\n**Channel Name:** ${finalChannel.name}\n\nThe counter will automatically update every 15 minutes.`)]
+ embeds: [successEmbed(`**Zähler erfolgreich aktualisiert!**\n\n**Zähler-ID:** \`${counterId}\`\n**Typ geändert:** ${getCounterEmoji(oldType)} ${getCounterTypeLabel(oldType)} → ${getCounterEmoji(newType)} ${getCounterTypeLabel(newType)}\n\n**Aktuelle Einstellungen:**\n**Typ:** ${getCounterEmoji(updatedCounter.type)} ${getCounterTypeLabel(updatedCounter.type)}\n**Kanal:** ${finalChannel}\n**Kanalname:** ${finalChannel!.name}\n\nDer Zähler wird automatisch alle 15 Minuten aktualisiert.`)]
}).catch(logger.error);
} catch (error) {
logger.error("Error updating counter:", error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while updating the counter. Please try again.' }).catch(logger.error);
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Aktualisieren des Zählers ist ein Fehler aufgetreten. Bitte versuche es erneut.' }).catch(logger.error);
}
}
\ No newline at end of file
diff --git a/src/commands/ServerStats/serverstats.js b/src/commands/ServerStats/serverstats.ts
similarity index 66%
rename from src/commands/ServerStats/serverstats.js
rename to src/commands/ServerStats/serverstats.ts
index 9fe3dda3b8..2557fb87e5 100644
--- a/src/commands/ServerStats/serverstats.js
+++ b/src/commands/ServerStats/serverstats.ts
@@ -10,40 +10,41 @@ import { handleDelete } from './modules/serverstats_delete.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
+import type { Command } from '../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName("serverstats")
- .setDescription("Manage server statistics that track member counts and channel data")
+ .setDescription("Serverstatistiken verwalten, die Mitgliederzahlen und Kanaldaten erfassen")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
.addSubcommand(subcommand =>
subcommand
.setName("create")
- .setDescription("Create a new statistics tracker channel in a category")
+ .setDescription("Neuen Statistik-Kanal in einer Kategorie erstellen")
.addStringOption(option =>
option
.setName("type")
- .setDescription("The type of statistics to track")
+ .setDescription("Art der zu erfassenden Statistik")
.setRequired(true)
.addChoices(
- { name: "members + bots", value: "members" },
- { name: "members only", value: "members_only" },
- { name: "bots only", value: "bots" }
+ { name: "Mitglieder + Bots", value: "members" },
+ { name: "nur Mitglieder", value: "members_only" },
+ { name: "nur Bots", value: "bots" }
)
)
.addStringOption(option =>
option
.setName("channel_type")
- .setDescription("The channel type to create for this tracker")
+ .setDescription("Kanaltyp, der für diesen Zähler erstellt wird")
.setRequired(true)
.addChoices(
- { name: "voice channel (recommended)", value: "voice" },
- { name: "text channel", value: "text" }
+ { name: "Sprachkanal (empfohlen)", value: "voice" },
+ { name: "Textkanal", value: "text" }
)
)
.addChannelOption(option =>
option
.setName("category")
- .setDescription("The category where the statistics tracker channel will be created")
+ .setDescription("Kategorie, in der der Statistik-Kanal erstellt wird")
.setRequired(true)
.addChannelTypes(ChannelType.GuildCategory)
)
@@ -51,38 +52,38 @@ export default {
.addSubcommand(subcommand =>
subcommand
.setName("list")
- .setDescription("List all statistics trackers for this server")
+ .setDescription("Alle Statistik-Zähler dieses Servers anzeigen")
)
.addSubcommand(subcommand =>
subcommand
.setName("update")
- .setDescription("Update an existing statistics tracker")
+ .setDescription("Bestehenden Statistik-Zähler aktualisieren")
.addStringOption(option =>
option
.setName("counter-id")
- .setDescription("The ID of the tracker to update")
+ .setDescription("ID des zu aktualisierenden Zählers")
.setRequired(true)
)
.addStringOption(option =>
option
.setName("type")
- .setDescription("The new tracker type")
+ .setDescription("Neuer Zählertyp")
.setRequired(false)
.addChoices(
- { name: "members + bots", value: "members" },
- { name: "members only", value: "members_only" },
- { name: "bots only", value: "bots" }
+ { name: "Mitglieder + Bots", value: "members" },
+ { name: "nur Mitglieder", value: "members_only" },
+ { name: "nur Bots", value: "bots" }
)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("delete")
- .setDescription("Delete an existing statistics tracker")
+ .setDescription("Bestehenden Statistik-Zähler löschen")
.addStringOption(option =>
option
.setName("counter-id")
- .setDescription("The ID of the tracker to delete")
+ .setDescription("ID des zu löschenden Zählers")
.setRequired(true)
)
),
@@ -104,7 +105,7 @@ export default {
await handleDelete(interaction, client);
break;
default:
- await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Unknown subcommand.' });
+ await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Unbekannter Subcommand.' });
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Ticket/claim.js b/src/commands/Ticket/claim.js
deleted file mode 100644
index d1d1c1f938..0000000000
--- a/src/commands/Ticket/claim.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { getColor } from '../../config/bot.js';
-import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { getTicketPermissionContext } from '../../utils/ticket/ticketPermissions.js';
-import { claimTicket } from '../../services/ticket.js';
-export default {
- data: new SlashCommandBuilder()
- .setName("claim")
- .setDescription("Claims an open ticket, assigning it to you.")
- .setDMPermission(false),
-
- async execute(interaction, guildConfig, client) {
- const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
- if (!deferred) {
- return;
- }
-
- const permissionContext = await getTicketPermissionContext({ client, interaction });
- if (!permissionContext.ticketData) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'This command can only be used in a valid ticket channel.' });
- }
-
- if (!permissionContext.canManageTicket) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the `Manage Channels` permission or the configured `Ticket Staff Role` to claim tickets.' });
- }
-
- await claimTicket(interaction.channel, interaction.user);
-
- await InteractionHelper.safeEditReply(interaction, {
- embeds: [
- successEmbed(
- "Ticket Claimed!",
- "You have successfully claimed this ticket.",
- ),
- ],
- });
-
- logger.info('Ticket claimed successfully', {
- userId: interaction.user.id,
- userTag: interaction.user.tag,
- channelId: interaction.channel.id,
- channelName: interaction.channel.name,
- guildId: interaction.guildId,
- commandName: 'claim'
- });
- },
-};
\ No newline at end of file
diff --git a/src/commands/Ticket/close.js b/src/commands/Ticket/close.js
deleted file mode 100644
index 056a699b9b..0000000000
--- a/src/commands/Ticket/close.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { getColor } from '../../config/bot.js';
-import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, MessageFlags } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { getTicketPermissionContext } from '../../utils/ticket/ticketPermissions.js';
-import { closeTicket } from '../../services/ticket.js';
-export default {
- data: new SlashCommandBuilder()
- .setName("close")
- .setDescription("Closes the current ticket.")
- .setDMPermission(false)
- .addStringOption((option) =>
- option
- .setName("reason")
- .setDescription("The reason for closing the ticket.")
- .setRequired(false),
- ),
-
- async execute(interaction, guildConfig, client) {
- const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
- if (!deferred) {
- return;
- }
-
- const permissionContext = await getTicketPermissionContext({ client, interaction });
- if (!permissionContext.ticketData) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'This command can only be used in a valid ticket channel.' });
- }
-
- if (!permissionContext.canCloseTicket) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the `Manage Channels` permission, the configured `Ticket Staff Role`, or be the ticket creator to close this ticket.' });
- }
-
- const reason =
- interaction.options?.getString("reason") ||
- "Closed via command without a specific reason.";
-
- await closeTicket(interaction.channel, interaction.user, reason);
-
- await InteractionHelper.safeEditReply(interaction, {
- embeds: [
- successEmbed(
- "Ticket Closed!",
- "This ticket has been closed successfully.",
- ),
- ],
- });
-
- logger.info('Ticket closed successfully', {
- userId: interaction.user.id,
- userTag: interaction.user.tag,
- channelId: interaction.channel.id,
- channelName: interaction.channel.name,
- guildId: interaction.guildId,
- reason: reason,
- commandName: 'close'
- });
- },
-};
diff --git a/src/commands/Ticket/modules/claim.ts b/src/commands/Ticket/modules/claim.ts
new file mode 100644
index 0000000000..a411e682c8
--- /dev/null
+++ b/src/commands/Ticket/modules/claim.ts
@@ -0,0 +1,51 @@
+import type { Command } from '../../../types/index.js';
+import { getColor } from '../../../config/bot.js';
+import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, type TextChannel } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { getTicketPermissionContext } from '../../../utils/ticket/ticketPermissions.js';
+import { claimTicket } from '../../../services/ticket.js';
+export default {
+ data: new SlashCommandBuilder()
+ .setName("claim")
+ .setDescription("Ein offenes Ticket übernehmen und dir zuweisen.")
+ .setDMPermission(false),
+
+ async execute(interaction, guildConfig, client) {
+ const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
+ if (!deferred) {
+ return;
+ }
+
+ const permissionContext = await getTicketPermissionContext({ client, interaction });
+ if (!permissionContext.ticketData) {
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Dieser Command funktioniert nur in einem gültigen Ticket-Kanal.' });
+ }
+
+ if (!permissionContext.canManageTicket) {
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung „Kanäle verwalten“ oder die eingestellte Ticket-Team-Rolle, um Tickets zu übernehmen.' });
+ }
+
+ await claimTicket(interaction.channel as TextChannel, interaction.user);
+
+ await InteractionHelper.safeEditReply(interaction, {
+ embeds: [
+ successEmbed(
+ "Ticket übernommen!",
+ "Du hast dieses Ticket erfolgreich übernommen.",
+ ),
+ ],
+ });
+
+ logger.info('Ticket claimed successfully', {
+ userId: interaction.user.id,
+ userTag: interaction.user.tag,
+ channelId: interaction.channel!.id,
+ channelName: (interaction.channel as TextChannel).name,
+ guildId: interaction.guildId,
+ commandName: 'claim'
+ });
+ },
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Ticket/modules/close.ts b/src/commands/Ticket/modules/close.ts
new file mode 100644
index 0000000000..bebb0c5184
--- /dev/null
+++ b/src/commands/Ticket/modules/close.ts
@@ -0,0 +1,62 @@
+import type { Command } from '../../../types/index.js';
+import { getColor } from '../../../config/bot.js';
+import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, MessageFlags, type TextChannel } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { getTicketPermissionContext } from '../../../utils/ticket/ticketPermissions.js';
+import { closeTicket } from '../../../services/ticket.js';
+export default {
+ data: new SlashCommandBuilder()
+ .setName("close")
+ .setDescription("Schließt das aktuelle Ticket.")
+ .setDMPermission(false)
+ .addStringOption((option) =>
+ option
+ .setName("reason")
+ .setDescription("Der Grund für das Schließen des Tickets.")
+ .setRequired(false),
+ ),
+
+ async execute(interaction, guildConfig, client) {
+ const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
+ if (!deferred) {
+ return;
+ }
+
+ const permissionContext = await getTicketPermissionContext({ client, interaction });
+ if (!permissionContext.ticketData) {
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Dieser Command funktioniert nur in einem gültigen Ticket-Kanal.' });
+ }
+
+ if (!permissionContext.canCloseTicket) {
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung „Kanäle verwalten“, die eingestellte Ticket-Team-Rolle oder musst das Ticket erstellt haben, um es zu schließen.' });
+ }
+
+ const reason =
+ interaction.options?.getString("reason") ||
+ "Per Command ohne bestimmten Grund geschlossen.";
+
+ await closeTicket(interaction.channel as TextChannel, interaction.user, reason);
+
+ await InteractionHelper.safeEditReply(interaction, {
+ embeds: [
+ successEmbed(
+ "Ticket geschlossen!",
+ "Dieses Ticket wurde erfolgreich geschlossen.",
+ ),
+ ],
+ });
+
+ logger.info('Ticket closed successfully', {
+ userId: interaction.user.id,
+ userTag: interaction.user.tag,
+ channelId: interaction.channel!.id,
+ channelName: (interaction.channel as TextChannel).name,
+ guildId: interaction.guildId,
+ reason: reason,
+ commandName: 'close'
+ });
+ },
+} satisfies Command;
diff --git a/src/commands/Ticket/modules/priority.ts b/src/commands/Ticket/modules/priority.ts
new file mode 100644
index 0000000000..75a310c5d7
--- /dev/null
+++ b/src/commands/Ticket/modules/priority.ts
@@ -0,0 +1,68 @@
+import type { Command } from '../../../types/index.js';
+import { getColor, botConfig } from '../../../config/bot.js';
+import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, type TextChannel } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { getTicketPermissionContext } from '../../../utils/ticket/ticketPermissions.js';
+import { updateTicketPriority } from '../../../services/ticket.js';
+
+export default {
+ data: new SlashCommandBuilder()
+ .setName("priority")
+ .setDescription("Legt die Priorität des aktuellen Support-Tickets fest.")
+ .addStringOption((option) =>
+ option
+ .setName("level")
+ .setDescription("Die Priorität für das Ticket.")
+ .setRequired(true)
+ .addChoices(
+ { name: "Dringend", value: "urgent" },
+ { name: "Hoch", value: "high" },
+ { name: "Mittel", value: "medium" },
+ { name: "Niedrig", value: "low" },
+ { name: "Keine", value: "none" },
+ ),
+ )
+ .setDMPermission(false),
+ category: "Ticket",
+
+ async execute(interaction, guildConfig, client) {
+ const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
+ if (!deferred) {
+ return;
+ }
+
+ const permissionContext = await getTicketPermissionContext({ client, interaction });
+ if (!permissionContext.ticketData) {
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Dieser Command funktioniert nur in einem gültigen Ticket-Kanal.' });
+ }
+
+ if (!permissionContext.canManageTicket) {
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung „Kanäle verwalten“ oder die eingestellte Ticket-Team-Rolle, um die Ticket-Priorität zu ändern.' });
+ }
+
+ const priorityLevel = interaction.options.getString("level")!;
+ await updateTicketPriority(interaction.channel as TextChannel, priorityLevel, interaction.user);
+
+ await InteractionHelper.safeEditReply(interaction, {
+ embeds: [
+ successEmbed(
+ "Priorität aktualisiert",
+ `Ticket-Priorität auf **${(botConfig.tickets.priorities[priorityLevel as keyof typeof botConfig.tickets.priorities]?.label ?? priorityLevel).toUpperCase()}** gesetzt.`,
+ ),
+ ],
+ });
+
+ logger.info('Ticket priority updated successfully', {
+ userId: interaction.user.id,
+ userTag: interaction.user.tag,
+ channelId: interaction.channel!.id,
+ channelName: (interaction.channel as TextChannel).name,
+ guildId: interaction.guildId,
+ priority: priorityLevel,
+ commandName: 'priority'
+ });
+ },
+} satisfies Command;
diff --git a/src/commands/Ticket/modules/ticket_dashboard.js b/src/commands/Ticket/modules/ticket_dashboard.ts
similarity index 63%
rename from src/commands/Ticket/modules/ticket_dashboard.js
rename to src/commands/Ticket/modules/ticket_dashboard.ts
index 17f3f43f88..b69bed1e9c 100644
--- a/src/commands/Ticket/modules/ticket_dashboard.js
+++ b/src/commands/Ticket/modules/ticket_dashboard.ts
@@ -1,3 +1,4 @@
+import type { Command, GuildConfig, NexoClient, SubcommandModule } from '../../../types/index.js';
import { getColor } from '../../../config/bot.js';
import {
ActionRowBuilder,
@@ -15,32 +16,39 @@ import {
MessageFlags,
ComponentType,
EmbedBuilder,
+ type ButtonInteraction,
+ type CategoryChannel,
+ type ChatInputCommandInteraction,
+ type Guild,
+ type GuildTextBasedChannel,
+ type StringSelectMenuInteraction,
} from 'discord.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { successEmbed, infoEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError, type ErrorLike } from '../../../utils/errorHandler.js';
import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js';
-import { getGuildTicketStats } from '../../../utils/database/tickets.js';
+import { getGuildTicketStats, type GuildTicketStats } from '../../../utils/database/tickets.js';
import { getUserTicketCount } from '../../../services/ticket.js';
import {
getTicketPanelStatus,
messageHasButtonCustomId,
formatPanelStatusField,
+ type PanelStatus,
} from '../../../utils/panelStatus.js';
import { startDashboardSession } from '../../../utils/dashboardSession.js';
-function buildButtonRow(guildConfig, guildId, disabled = false, panelStatus = null) {
+function buildButtonRow(guildConfig: GuildConfig, guildId: string, disabled = false, panelStatus: PanelStatus | null = null) {
const dmEnabled = guildConfig.dmOnClose !== false;
const showRepost = panelStatus?.exists === false && panelStatus?.reason === 'panel_deleted';
- const buttons = [];
+ const buttons: ButtonBuilder[] = [];
if (showRepost) {
buttons.push(
new ButtonBuilder()
.setCustomId(`ticket_cfg_repost_${guildId}`)
- .setLabel('Repost Panel')
+ .setLabel('Panel neu posten')
.setStyle(ButtonStyle.Primary)
.setEmoji('📌')
.setDisabled(disabled),
@@ -50,28 +58,28 @@ function buildButtonRow(guildConfig, guildId, disabled = false, panelStatus = nu
buttons.push(
new ButtonBuilder()
.setCustomId(`ticket_cfg_dm_toggle_${guildId}`)
- .setLabel('DM on Close')
+ .setLabel('DM beim Schließen')
.setStyle(dmEnabled ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji(dmEnabled ? '📬' : '📭')
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId(`ticket_cfg_staff_role_btn_${guildId}`)
- .setLabel('Staff Role')
+ .setLabel('Team-Rolle')
.setStyle(ButtonStyle.Secondary)
.setEmoji('🛡️')
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId(`ticket_cfg_delete_${guildId}`)
- .setLabel('Delete System')
+ .setLabel('System löschen')
.setStyle(ButtonStyle.Danger)
.setEmoji('🗑️')
.setDisabled(disabled),
);
- return new ActionRowBuilder().addComponents(buttons);
+ return new ActionRowBuilder().addComponents(buttons);
}
-async function persistPanelMessageId(client, guildId, guildConfig, messageId) {
+async function persistPanelMessageId(client: NexoClient, guildId: string, guildConfig: GuildConfig, messageId: string | null | undefined) {
if (!messageId || guildConfig.ticketPanelMessageId === messageId) return;
guildConfig.ticketPanelMessageId = messageId;
if (client.db) {
@@ -79,30 +87,30 @@ async function persistPanelMessageId(client, guildId, guildConfig, messageId) {
}
}
-function buildPanelEmbed(config) {
+function buildPanelEmbed(config: GuildConfig) {
return new EmbedBuilder()
- .setTitle('Support Tickets')
- .setDescription(config.ticketPanelMessage || 'Click the button below to create a support ticket.')
+ .setTitle('Support-Tickets')
+ .setDescription(config.ticketPanelMessage || 'Klick auf den Button unten, um ein Support-Ticket zu erstellen.')
.setColor(getColor('info'));
}
-function buildPanelButtonRow(config) {
- return new ActionRowBuilder().addComponents(
+function buildPanelButtonRow(config: GuildConfig) {
+ return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('create_ticket')
- .setLabel(config.ticketButtonLabel || 'Create Ticket')
+ .setLabel(config.ticketButtonLabel || 'Ticket erstellen')
.setStyle(ButtonStyle.Primary)
.setEmoji('📩'),
);
}
-async function repostTicketPanel(client, guild, guildConfig, guildId) {
- const channel = await guild.channels.fetch(guildConfig.ticketPanelChannelId).catch(() => null);
+async function repostTicketPanel(client: NexoClient, guild: Guild, guildConfig: GuildConfig, guildId: string) {
+ const channel = await guild.channels.fetch(guildConfig.ticketPanelChannelId!).catch(() => null) as GuildTextBasedChannel | null;
if (!channel) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Panel channel missing',
ErrorTypes.CONFIGURATION,
- 'The configured ticket panel channel no longer exists. Set a new panel channel from the dashboard.',
+ 'Der eingestellte Ticket-Panel-Kanal existiert nicht mehr. Leg im Dashboard einen neuen Panel-Kanal fest.',
);
}
@@ -115,111 +123,111 @@ async function repostTicketPanel(client, guild, guildConfig, guildId) {
return sentPanel;
}
-function formatCloseDuration(ms) {
- if (ms == null) return '`N/A`';
+function formatCloseDuration(ms: number | null | undefined) {
+ if (ms == null) return '`–`';
const hours = Math.floor(ms / 3_600_000);
const minutes = Math.floor((ms % 3_600_000) / 60_000);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
-function buildDashboardEmbed(config, guild, panelStatus = null, ticketStats = null) {
- const panelChannel = config.ticketPanelChannelId ? `<#${config.ticketPanelChannelId}>` : '`Not set`';
- const staffRole = config.ticketStaffRoleId ? `<@&${config.ticketStaffRoleId}>` : '`Not set`';
- const ticketLogsChannel = config.ticketLogsChannelId ? `<#${config.ticketLogsChannelId}>` : '`Not set`';
- const transcriptChannel = config.ticketTranscriptChannelId ? `<#${config.ticketTranscriptChannelId}>` : '`Not set`';
+function buildDashboardEmbed(config: GuildConfig, guild: Guild, panelStatus: PanelStatus | null = null, ticketStats: GuildTicketStats | null = null) {
+ const panelChannel = config.ticketPanelChannelId ? `<#${config.ticketPanelChannelId}>` : '`Nicht gesetzt`';
+ const staffRole = config.ticketStaffRoleId ? `<@&${config.ticketStaffRoleId}>` : '`Nicht gesetzt`';
+ const ticketLogsChannel = config.ticketLogsChannelId ? `<#${config.ticketLogsChannelId}>` : '`Nicht gesetzt`';
+ const transcriptChannel = config.ticketTranscriptChannelId ? `<#${config.ticketTranscriptChannelId}>` : '`Nicht gesetzt`';
const openCategoryChannel = config.ticketCategoryId ? guild.channels.cache.get(config.ticketCategoryId) : null;
- const openCategory = openCategoryChannel ? openCategoryChannel.toString() : '`Not set`';
+ const openCategory = openCategoryChannel ? openCategoryChannel.toString() : '`Nicht gesetzt`';
const closedCategoryChannel = config.ticketClosedCategoryId ? guild.channels.cache.get(config.ticketClosedCategoryId) : null;
- const closedCategory = closedCategoryChannel ? closedCategoryChannel.toString() : '`Not set`';
+ const closedCategory = closedCategoryChannel ? closedCategoryChannel.toString() : '`Nicht gesetzt`';
- const rawMsg = config.ticketPanelMessage || 'Click the button below to create a support ticket.';
+ const rawMsg = config.ticketPanelMessage || 'Klick auf den Button unten, um ein Support-Ticket zu erstellen.';
const panelMsg = `\`${rawMsg.length > 60 ? rawMsg.substring(0, 60) + '…' : rawMsg}\``;
- const btnLabel = `\`${config.ticketButtonLabel || 'Create Ticket'}\``;
+ const btnLabel = `\`${config.ticketButtonLabel || 'Ticket erstellen'}\``;
let panelStatusValue = formatPanelStatusField(panelStatus);
const openTickets = ticketStats ? String(ticketStats.openCount) : '`—`';
const avgCloseTime = ticketStats ? formatCloseDuration(ticketStats.avgCloseTimeMs) : '`—`';
const feedbackSummary = ticketStats?.feedbackCount
- ? `${ticketStats.avgRating}/5 (${ticketStats.feedbackCount} rating${ticketStats.feedbackCount !== 1 ? 's' : ''})`
- : '`No ratings yet`';
+ ? `${ticketStats.avgRating}/5 (${ticketStats.feedbackCount} Bewertung${ticketStats.feedbackCount !== 1 ? 'en' : ''})`
+ : '`Noch keine Bewertungen`';
return new EmbedBuilder()
- .setTitle('🎫 Ticket System Dashboard')
- .setDescription(`Manage ticket system settings for **${guild.name}**.\nSelect an option below to modify a setting.`)
+ .setTitle('🎫 Ticket-System-Dashboard')
+ .setDescription(`Verwalte die Ticket-Einstellungen für **${guild.name}**.\nWähl unten eine Option, um eine Einstellung zu ändern.`)
.setColor(getColor('info'))
.addFields(
- { name: 'Panel Status', value: panelStatusValue, inline: false },
- { name: 'Panel Channel', value: panelChannel, inline: true },
- { name: 'Staff Role', value: staffRole, inline: true },
+ { name: 'Panel-Status', value: panelStatusValue, inline: false },
+ { name: 'Panel-Kanal', value: panelChannel, inline: true },
+ { name: 'Team-Rolle', value: staffRole, inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
- { name: 'Open Tickets Category', value: openCategory, inline: true },
- { name: 'Closed Tickets Category', value: closedCategory, inline: true },
+ { name: 'Kategorie offene Tickets', value: openCategory, inline: true },
+ { name: 'Kategorie geschlossene Tickets', value: closedCategory, inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
- { name: 'Panel Message', value: panelMsg, inline: false },
- { name: 'Button Label', value: btnLabel, inline: true },
- { name: 'Max Tickets/User', value: String(config.maxTicketsPerUser || 3), inline: true },
- { name: 'DM on Close', value: config.dmOnClose !== false ? 'Enabled' : 'Disabled', inline: true },
- { name: 'Ticket Logs Channel', value: ticketLogsChannel, inline: true },
- { name: 'Transcript Channel', value: transcriptChannel, inline: true },
+ { name: 'Panel-Nachricht', value: panelMsg, inline: false },
+ { name: 'Button-Beschriftung', value: btnLabel, inline: true },
+ { name: 'Max. Tickets/Nutzer', value: String(config.maxTicketsPerUser || 3), inline: true },
+ { name: 'DM beim Schließen', value: config.dmOnClose !== false ? 'Aktiviert' : 'Deaktiviert', inline: true },
+ { name: 'Ticket-Log-Kanal', value: ticketLogsChannel, inline: true },
+ { name: 'Transkript-Kanal', value: transcriptChannel, inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
- { name: 'Open Tickets', value: openTickets, inline: true },
- { name: 'Avg Close Time', value: avgCloseTime, inline: true },
- { name: 'Feedback Rating', value: feedbackSummary, inline: true },
+ { name: 'Offene Tickets', value: openTickets, inline: true },
+ { name: 'Ø Zeit bis Schließen', value: avgCloseTime, inline: true },
+ { name: 'Feedback-Bewertung', value: feedbackSummary, inline: true },
)
- .setFooter({ text: 'Select an option below • Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Wähl unten eine Option • Das Dashboard schließt nach 10 Minuten Inaktivität' })
.setTimestamp();
}
-function buildSelectMenu(guildId) {
+function buildSelectMenu(guildId: string) {
return new StringSelectMenuBuilder()
.setCustomId(`ticket_config_${guildId}`)
- .setPlaceholder('Select a setting to configure...')
+ .setPlaceholder('Einstellung zum Konfigurieren auswählen...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Edit Panel Message')
- .setDescription('Change the message displayed on the ticket creation panel')
+ .setLabel('Panel-Nachricht bearbeiten')
+ .setDescription('Die Nachricht auf dem Ticket-Panel ändern')
.setValue('panel_message')
.setEmoji('📝'),
new StringSelectMenuOptionBuilder()
- .setLabel('Edit Button Label')
- .setDescription('Change the label on the Create Ticket button')
+ .setLabel('Button-Beschriftung bearbeiten')
+ .setDescription('Die Beschriftung des Buttons „Ticket erstellen“ ändern')
.setValue('button_label')
.setEmoji('🏷️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Change Open Tickets Category')
- .setDescription('Category where new tickets are created')
+ .setLabel('Kategorie für offene Tickets ändern')
+ .setDescription('Kategorie, in der neue Tickets erstellt werden')
.setValue('open_category')
.setEmoji('📁'),
new StringSelectMenuOptionBuilder()
- .setLabel('Change Closed Tickets Category')
- .setDescription('Category where closed tickets are moved')
+ .setLabel('Kategorie für geschlossene Tickets ändern')
+ .setDescription('Kategorie, in die geschlossene Tickets verschoben werden')
.setValue('closed_category')
.setEmoji('📂'),
new StringSelectMenuOptionBuilder()
- .setLabel('Set Max Tickets per User')
- .setDescription('Limit how many open tickets one user can have at once')
+ .setLabel('Max. Tickets pro Nutzer festlegen')
+ .setDescription('Begrenzen, wie viele offene Tickets ein Nutzer gleichzeitig haben kann')
.setValue('max_tickets')
.setEmoji('🔢'),
new StringSelectMenuOptionBuilder()
- .setLabel('Set Ticket Logs Channel')
- .setDescription('Channel to receive ticket feedback, lifecycle events, and logs')
+ .setLabel('Ticket-Log-Kanal festlegen')
+ .setDescription('Kanal für Ticket-Feedback, Ticket-Ereignisse und Logs')
.setValue('logs_channel')
.setEmoji('🎫'),
new StringSelectMenuOptionBuilder()
- .setLabel('Set Transcript Channel')
- .setDescription('Channel to receive auto-generated transcripts on deletion')
+ .setLabel('Transkript-Kanal festlegen')
+ .setDescription('Kanal für automatisch erstellte Transkripte beim Löschen')
.setValue('transcript_channel')
.setEmoji('📜'),
);
}
-async function refreshDashboard(rootInteraction, guildConfig, guildId, client) {
+async function refreshDashboard(rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
const panelStatus = client
- ? await getTicketPanelStatus(client, rootInteraction.guild, guildConfig)
+ ? await getTicketPanelStatus(client, rootInteraction.guild!, guildConfig)
: null;
const ticketStats = client ? await getGuildTicketStats(guildId) : null;
@@ -228,14 +236,14 @@ async function refreshDashboard(rootInteraction, guildConfig, guildId, client) {
}
const buttonRow = buildButtonRow(guildConfig, guildId, false, panelStatus);
- const selectRow = new ActionRowBuilder().addComponents(buildSelectMenu(guildId));
+ const selectRow = new ActionRowBuilder().addComponents(buildSelectMenu(guildId));
await InteractionHelper.safeEditReply(rootInteraction, {
- embeds: [buildDashboardEmbed(guildConfig, rootInteraction.guild, panelStatus, ticketStats)],
+ embeds: [buildDashboardEmbed(guildConfig, rootInteraction.guild!, panelStatus, ticketStats)],
components: [buttonRow, selectRow],
}).catch(() => {});
}
-async function updateLivePanel(client, guild, config, guildId) {
+async function updateLivePanel(client: NexoClient, guild: Guild, config: GuildConfig, guildId: string) {
if (!config.ticketPanelChannelId) return false;
try {
const panelStatus = await getTicketPanelStatus(client, guild, config);
@@ -250,7 +258,7 @@ async function updateLivePanel(client, guild, config, guildId) {
});
return true;
} catch (error) {
- logger.warn('Failed to update live ticket panel:', error.message);
+ logger.warn('Failed to update live ticket panel:', (error as ErrorLike).message);
return false;
}
}
@@ -259,30 +267,30 @@ export default {
prefixOnly: false,
async execute(interaction, config, client) {
try {
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
const guildConfig = await getGuildConfig(client, guildId);
if (!guildConfig.ticketPanelChannelId) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Ticket system not configured',
ErrorTypes.CONFIGURATION,
- 'The ticket system has not been set up yet. Run `/ticket setup` first to configure it.',
+ 'Das Ticket-System ist noch nicht eingerichtet. Nutze zuerst `/ticket setup`, um es einzurichten.',
);
}
- const panelStatus = await getTicketPanelStatus(client, interaction.guild, guildConfig);
+ const panelStatus = await getTicketPanelStatus(client, interaction.guild!, guildConfig);
if (panelStatus.recoveredId) {
await persistPanelMessageId(client, guildId, guildConfig, panelStatus.recoveredId);
}
const ticketStats = await getGuildTicketStats(guildId);
- const selectRow = new ActionRowBuilder().addComponents(buildSelectMenu(guildId));
+ const selectRow = new ActionRowBuilder().addComponents(buildSelectMenu(guildId));
const buttonRow = buildButtonRow(guildConfig, guildId, false, panelStatus);
await startDashboardSession({
interaction,
- embeds: [buildDashboardEmbed(guildConfig, interaction.guild, panelStatus, ticketStats)],
+ embeds: [buildDashboardEmbed(guildConfig, interaction.guild!, panelStatus, ticketStats)],
components: [buttonRow, selectRow],
selectMenuId: `ticket_config_${guildId}`,
buttonMatcher: (customId) =>
@@ -332,35 +340,35 @@ export default {
},
});
} catch (error) {
- if (error instanceof TitanBotError) throw error;
+ if (error instanceof NexoBotError) throw error;
logger.error('Unexpected error in ticket_config:', error);
- throw new TitanBotError(
- `Ticket config failed: ${error.message}`,
+ throw new NexoBotError(
+ `Ticket config failed: ${(error as ErrorLike).message}`,
ErrorTypes.UNKNOWN,
- 'Failed to open the ticket configuration dashboard.',
+ 'Das Ticket-Dashboard konnte nicht geöffnet werden.',
);
}
},
-};
+} satisfies SubcommandModule & Pick;
-async function handlePanelMessage(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handlePanelMessage(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('ticket_cfg_panel_msg')
- .setTitle('📝 Edit Panel Message')
+ .setTitle('📝 Panel-Nachricht bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('panel_msg_input')
- .setLabel('Panel Message')
+ .setLabel('Panel-Nachricht')
.setStyle(TextInputStyle.Paragraph)
.setValue(
guildConfig.ticketPanelMessage ||
- 'Click the button below to create a support ticket.',
+ 'Klick auf den Button unten, um ein Support-Ticket zu erstellen.',
)
.setMaxLength(2000)
.setMinLength(1)
.setRequired(true)
- .setPlaceholder('Click the button below to create a support ticket.'),
+ .setPlaceholder('Klick auf den Button unten, um ein Support-Ticket zu erstellen.'),
),
);
@@ -380,16 +388,16 @@ async function handlePanelMessage(selectInteraction, rootInteraction, guildConfi
guildConfig.ticketPanelMessage = newMessage;
await setGuildConfig(client, guildId, guildConfig);
- const panelUpdated = await updateLivePanel(client, rootInteraction.guild, guildConfig, guildId);
+ const panelUpdated = await updateLivePanel(client, rootInteraction.guild!, guildConfig, guildId);
await submitted.reply({
embeds: [
successEmbed(
- '✅ Panel Message Updated',
- `The panel message has been updated.${
+ '✅ Panel-Nachricht aktualisiert',
+ `Die Panel-Nachricht wurde aktualisiert.${
panelUpdated
- ? '\nThe live ticket panel has also been refreshed.'
- : '\n> **Note:** The live panel could not be located. Use **Repost Panel** on the dashboard to restore it.'
+ ? '\nDas Live-Ticket-Panel wurde ebenfalls aktualisiert.'
+ : '\n> **Hinweis:** Das Live-Panel wurde nicht gefunden. Nutze **Panel neu posten** im Dashboard, um es wiederherzustellen.'
}`,
),
],
@@ -399,21 +407,21 @@ async function handlePanelMessage(selectInteraction, rootInteraction, guildConfi
await refreshDashboard(rootInteraction, guildConfig, guildId, client);
}
-async function handleButtonLabel(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleButtonLabel(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('ticket_cfg_btn_label')
- .setTitle('🏷️ Edit Button Label')
+ .setTitle('🏷️ Button-Beschriftung bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('btn_label_input')
- .setLabel('Button Label (max 80 characters)')
+ .setLabel('Button-Beschriftung (max. 80 Zeichen)')
.setStyle(TextInputStyle.Short)
- .setValue(guildConfig.ticketButtonLabel || 'Create Ticket')
+ .setValue(guildConfig.ticketButtonLabel || 'Ticket erstellen')
.setMaxLength(80)
.setMinLength(1)
.setRequired(true)
- .setPlaceholder('Create Ticket'),
+ .setPlaceholder('Ticket erstellen'),
),
);
@@ -433,16 +441,16 @@ async function handleButtonLabel(selectInteraction, rootInteraction, guildConfig
guildConfig.ticketButtonLabel = newLabel;
await setGuildConfig(client, guildId, guildConfig);
- const panelUpdated = await updateLivePanel(client, rootInteraction.guild, guildConfig, guildId);
+ const panelUpdated = await updateLivePanel(client, rootInteraction.guild!, guildConfig, guildId);
await submitted.reply({
embeds: [
successEmbed(
- '✅ Button Label Updated',
- `Button label changed to \`${newLabel}\`.${
+ '✅ Button-Beschriftung aktualisiert',
+ `Button-Beschriftung auf \`${newLabel}\` geändert.${
panelUpdated
- ? '\nThe live ticket panel button has also been updated.'
- : '\n> **Note:** The live panel could not be located. Use **Repost Panel** on the dashboard to restore it.'
+ ? '\nDer Button im Live-Ticket-Panel wurde ebenfalls aktualisiert.'
+ : '\n> **Hinweis:** Das Live-Panel wurde nicht gefunden. Nutze **Panel neu posten** im Dashboard, um es wiederherzustellen.'
}`,
),
],
@@ -452,22 +460,22 @@ async function handleButtonLabel(selectInteraction, rootInteraction, guildConfig
await refreshDashboard(rootInteraction, guildConfig, guildId, client);
}
-async function handleStaffRole(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleStaffRole(selectInteraction: StringSelectMenuInteraction | ButtonInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('ticket_cfg_staff_role')
- .setPlaceholder('Select the staff role...')
+ .setPlaceholder('Team-Rolle auswählen...')
.setMaxValues(1);
- const row = new ActionRowBuilder().addComponents(roleSelect);
+ const row = new ActionRowBuilder().addComponents(roleSelect);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('🛡️ Change Staff Role')
+ .setTitle('🛡️ Team-Rolle ändern')
.setDescription(
- `**Current:** ${guildConfig.ticketStaffRoleId ? `<@&${guildConfig.ticketStaffRoleId}>` : '`Not set`'}\n\nSelect the role that should have staff access to manage tickets.`,
+ `**Aktuell:** ${guildConfig.ticketStaffRoleId ? `<@&${guildConfig.ticketStaffRoleId}>` : '`Nicht gesetzt`'}\n\nWähl die Rolle, die als Team Tickets verwalten darf.`,
)
.setColor(getColor('info')),
],
@@ -475,7 +483,7 @@ async function handleStaffRole(selectInteraction, rootInteraction, guildConfig,
flags: MessageFlags.Ephemeral,
});
- const roleCollector = rootInteraction.channel.createMessageComponentCollector({
+ const roleCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.RoleSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_staff_role',
@@ -485,13 +493,13 @@ async function handleStaffRole(selectInteraction, rootInteraction, guildConfig,
roleCollector.on('collect', async roleInteraction => {
await roleInteraction.deferUpdate();
- const role = roleInteraction.roles.first();
+ const role = roleInteraction.roles.first()!;
guildConfig.ticketStaffRoleId = role.id;
await setGuildConfig(client, guildId, guildConfig);
await roleInteraction.followUp({
- embeds: [successEmbed('Staff Role Updated', `Staff role set to ${role}.`)],
+ embeds: [successEmbed('Team-Rolle aktualisiert', `Team-Rolle auf ${role} gesetzt.`)],
flags: MessageFlags.Ephemeral,
});
@@ -502,35 +510,35 @@ async function handleStaffRole(selectInteraction, rootInteraction, guildConfig,
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No role was selected. The staff role was not changed.',
+ message: 'Es wurde keine Rolle ausgewählt. Die Team-Rolle wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleOpenCategory(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleOpenCategory(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('ticket_cfg_open_cat')
- .setPlaceholder('Select a category...')
+ .setPlaceholder('Kategorie auswählen...')
.addChannelTypes(ChannelType.GuildCategory)
.setMaxValues(1);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('📁 Change Open Tickets Category')
+ .setTitle('📁 Kategorie für offene Tickets ändern')
.setDescription(
- `**Current:** ${guildConfig.ticketCategoryId ? `<#${guildConfig.ticketCategoryId}>` : '`Not set`'}\n\nSelect the category where new tickets will be created.`,
+ `**Aktuell:** ${guildConfig.ticketCategoryId ? `<#${guildConfig.ticketCategoryId}>` : '`Nicht gesetzt`'}\n\nWähl die Kategorie, in der neue Tickets erstellt werden.`,
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(channelSelect)],
+ components: [new ActionRowBuilder().addComponents(channelSelect)],
flags: MessageFlags.Ephemeral,
});
- const catCollector = rootInteraction.channel.createMessageComponentCollector({
+ const catCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_open_cat',
@@ -540,7 +548,7 @@ async function handleOpenCategory(selectInteraction, rootInteraction, guildConfi
catCollector.on('collect', async catInteraction => {
await catInteraction.deferUpdate();
- const category = catInteraction.channels.first();
+ const category = catInteraction.channels.first() as CategoryChannel;
guildConfig.ticketCategoryId = category.id;
await setGuildConfig(client, guildId, guildConfig);
@@ -548,8 +556,8 @@ async function handleOpenCategory(selectInteraction, rootInteraction, guildConfi
await catInteraction.followUp({
embeds: [
successEmbed(
- 'Open Category Updated',
- `New tickets will now be created in **${category.name}**.`,
+ 'Kategorie für offene Tickets aktualisiert',
+ `Neue Tickets werden jetzt in **${category.name}** erstellt.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -562,35 +570,35 @@ async function handleOpenCategory(selectInteraction, rootInteraction, guildConfi
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No category was selected. The setting was not changed.',
+ message: 'Es wurde keine Kategorie ausgewählt. Die Einstellung wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleClosedCategory(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleClosedCategory(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('ticket_cfg_closed_cat')
- .setPlaceholder('Select a category...')
+ .setPlaceholder('Kategorie auswählen...')
.addChannelTypes(ChannelType.GuildCategory)
.setMaxValues(1);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('📂 Change Closed Tickets Category')
+ .setTitle('📂 Kategorie für geschlossene Tickets ändern')
.setDescription(
- `**Current:** ${guildConfig.ticketClosedCategoryId ? `<#${guildConfig.ticketClosedCategoryId}>` : '`Not set`'}\n\nSelect the category where closed tickets will be moved.`,
+ `**Aktuell:** ${guildConfig.ticketClosedCategoryId ? `<#${guildConfig.ticketClosedCategoryId}>` : '`Nicht gesetzt`'}\n\nWähl die Kategorie, in die geschlossene Tickets verschoben werden.`,
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(channelSelect)],
+ components: [new ActionRowBuilder().addComponents(channelSelect)],
flags: MessageFlags.Ephemeral,
});
- const catCollector = rootInteraction.channel.createMessageComponentCollector({
+ const catCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_closed_cat',
@@ -600,7 +608,7 @@ async function handleClosedCategory(selectInteraction, rootInteraction, guildCon
catCollector.on('collect', async catInteraction => {
await catInteraction.deferUpdate();
- const category = catInteraction.channels.first();
+ const category = catInteraction.channels.first() as CategoryChannel;
guildConfig.ticketClosedCategoryId = category.id;
await setGuildConfig(client, guildId, guildConfig);
@@ -608,8 +616,8 @@ async function handleClosedCategory(selectInteraction, rootInteraction, guildCon
await catInteraction.followUp({
embeds: [
successEmbed(
- 'Closed Category Updated',
- `Closed tickets will now be moved to **${category.name}**.`,
+ 'Kategorie für geschlossene Tickets aktualisiert',
+ `Geschlossene Tickets werden jetzt nach **${category.name}** verschoben.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -622,21 +630,21 @@ async function handleClosedCategory(selectInteraction, rootInteraction, guildCon
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No category was selected. The setting was not changed.',
+ message: 'Es wurde keine Kategorie ausgewählt. Die Einstellung wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleMaxTickets(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleMaxTickets(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('ticket_cfg_max_tickets')
- .setTitle('Set Max Tickets per User')
+ .setTitle('Max. Tickets pro Nutzer festlegen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('max_tickets_input')
- .setLabel('Max Open Tickets (1–10)')
+ .setLabel('Max. offene Tickets (1–10)')
.setStyle(TextInputStyle.Short)
.setValue(String(guildConfig.maxTicketsPerUser || 3))
.setMaxLength(2)
@@ -664,7 +672,7 @@ async function handleMaxTickets(selectInteraction, rootInteraction, guildConfig,
if (Number.isNaN(newMax) || newMax < 1 || newMax > 10) {
await replyUserError(submitted, {
type: ErrorTypes.VALIDATION,
- message: 'Max tickets must be a whole number between **1** and **10**.',
+ message: 'Die maximale Ticketanzahl muss eine ganze Zahl zwischen **1** und **10** sein.',
});
return;
}
@@ -675,8 +683,8 @@ async function handleMaxTickets(selectInteraction, rootInteraction, guildConfig,
await submitted.reply({
embeds: [
successEmbed(
- 'Max Tickets Updated',
- `Users can now have at most **${newMax}** open ticket${newMax !== 1 ? 's' : ''} at a time.`,
+ 'Max. Tickets aktualisiert',
+ `Nutzer können jetzt höchstens **${newMax}** offene${newMax !== 1 ? '' : 's'} Ticket${newMax !== 1 ? 's' : ''} gleichzeitig haben.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -685,7 +693,7 @@ async function handleMaxTickets(selectInteraction, rootInteraction, guildConfig,
await refreshDashboard(rootInteraction, guildConfig, guildId, client);
}
-async function handleDmOnClose(btnInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleDmOnClose(btnInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await btnInteraction.deferUpdate();
const newState = guildConfig.dmOnClose === false;
@@ -695,8 +703,8 @@ async function handleDmOnClose(btnInteraction, rootInteraction, guildConfig, gui
await btnInteraction.followUp({
embeds: [
successEmbed(
- 'DM on Close Updated',
- `Users will **${newState ? 'now' : 'no longer'}** receive a DM when their ticket is closed.`,
+ 'DM beim Schließen aktualisiert',
+ `Nutzer bekommen ${newState ? '**jetzt** eine DM' : '**keine** DM mehr'}, wenn ihr Ticket geschlossen wird.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -705,27 +713,27 @@ async function handleDmOnClose(btnInteraction, rootInteraction, guildConfig, gui
await refreshDashboard(rootInteraction, guildConfig, guildId, client);
}
-async function handleLogsChannel(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleLogsChannel(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('ticket_cfg_logs_channel')
- .setPlaceholder('Select a channel...')
+ .setPlaceholder('Kanal auswählen...')
.addChannelTypes(ChannelType.GuildText)
.setMaxValues(1);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('🎫 Select Ticket Logs Channel')
- .setDescription('Choose where ticket feedback, lifecycle events (open, close, claim, etc.), and other logs will be sent.')
+ .setTitle('🎫 Ticket-Log-Kanal auswählen')
+ .setDescription('Wähl, wohin Ticket-Feedback, Ticket-Ereignisse (Öffnen, Schließen, Übernehmen usw.) und andere Logs gesendet werden.')
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(channelSelect)],
+ components: [new ActionRowBuilder().addComponents(channelSelect)],
flags: MessageFlags.Ephemeral,
});
- const collector = rootInteraction.channel.createMessageComponentCollector({
+ const collector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect,
filter: i => i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_logs_channel',
time: 60_000,
@@ -734,13 +742,13 @@ async function handleLogsChannel(selectInteraction, rootInteraction, guildConfig
collector.on('collect', async channelInteraction => {
await channelInteraction.deferUpdate();
- const channel = channelInteraction.channels.first();
+ const channel = channelInteraction.channels.first()!;
guildConfig.ticketLogsChannelId = channel.id;
await setGuildConfig(client, guildId, guildConfig);
await channelInteraction.followUp({
- embeds: [successEmbed('Logs Channel Updated', `Ticket logs will be sent to ${channel}`)],
+ embeds: [successEmbed('Log-Kanal aktualisiert', `Ticket-Logs werden an ${channel} gesendet`)],
flags: MessageFlags.Ephemeral,
});
@@ -751,33 +759,33 @@ async function handleLogsChannel(selectInteraction, rootInteraction, guildConfig
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No channel selected. No changes were made.',
+ message: 'Kein Kanal ausgewählt. Es wurde nichts geändert.',
}).catch(() => {});
}
});
}
-async function handleTranscriptChannel(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleTranscriptChannel(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('ticket_cfg_transcript_channel')
- .setPlaceholder('Select a channel...')
+ .setPlaceholder('Kanal auswählen...')
.addChannelTypes(ChannelType.GuildText)
.setMaxValues(1);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('📜 Select Transcript Channel')
- .setDescription('Choose where auto-generated transcripts will be sent when tickets are deleted.')
+ .setTitle('📜 Transkript-Kanal auswählen')
+ .setDescription('Wähl, wohin automatisch erstellte Transkripte beim Löschen von Tickets gesendet werden.')
.setColor(getColor('info'))
],
- components: [new ActionRowBuilder().addComponents(channelSelect)],
+ components: [new ActionRowBuilder().addComponents(channelSelect)],
flags: MessageFlags.Ephemeral
});
- const collector = rootInteraction.channel.createMessageComponentCollector({
+ const collector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect,
filter: i => i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_transcript_channel',
time: 60_000,
@@ -786,13 +794,13 @@ async function handleTranscriptChannel(selectInteraction, rootInteraction, guild
collector.on('collect', async channelInteraction => {
await channelInteraction.deferUpdate();
- const channel = channelInteraction.channels.first();
+ const channel = channelInteraction.channels.first()!;
guildConfig.ticketTranscriptChannelId = channel.id;
await setGuildConfig(client, guildId, guildConfig);
await channelInteraction.followUp({
- embeds: [successEmbed('Transcript Channel Updated', `Transcripts will be sent to ${channel}`)],
+ embeds: [successEmbed('Transkript-Kanal aktualisiert', `Transkripte werden an ${channel} gesendet`)],
flags: MessageFlags.Ephemeral
});
@@ -803,34 +811,34 @@ async function handleTranscriptChannel(selectInteraction, rootInteraction, guild
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No channel selected. No changes were made.',
+ message: 'Kein Kanal ausgewählt. Es wurde nichts geändert.',
}).catch(() => {});
}
});
}
-async function handleCheckUser(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleCheckUser(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const userSelect = new UserSelectMenuBuilder()
.setCustomId('ticket_cfg_check_user')
- .setPlaceholder('Select a user to check...')
+ .setPlaceholder('Nutzer zum Prüfen auswählen...')
.setMaxValues(1);
- const row = new ActionRowBuilder().addComponents(userSelect);
+ const row = new ActionRowBuilder().addComponents(userSelect);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Check User Tickets')
- .setDescription('Select a user to view their current open ticket count.')
+ .setTitle('Tickets eines Nutzers prüfen')
+ .setDescription('Wähl einen Nutzer, um seine aktuell offenen Tickets zu sehen.')
.setColor(getColor('info')),
],
components: [row],
flags: MessageFlags.Ephemeral,
});
- const userCollector = rootInteraction.channel.createMessageComponentCollector({
+ const userCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.UserSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_check_user',
@@ -840,7 +848,7 @@ async function handleCheckUser(selectInteraction, rootInteraction, guildConfig,
userCollector.on('collect', async userInteraction => {
await userInteraction.deferUpdate();
- const targetUser = userInteraction.users.first();
+ const targetUser = userInteraction.users.first()!;
const maxTickets = guildConfig.maxTicketsPerUser || 3;
const openCount = await getUserTicketCount(guildId, targetUser.id);
const atLimit = openCount >= maxTickets;
@@ -848,13 +856,13 @@ async function handleCheckUser(selectInteraction, rootInteraction, guildConfig,
await userInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle(`Ticket Check — ${targetUser.username}`)
+ .setTitle(`Ticket-Check – ${targetUser.username}`)
.setDescription(
- `**Open Tickets:** ${openCount} / ${maxTickets}\n` +
- `**Remaining:** ${Math.max(0, maxTickets - openCount)}\n\n` +
+ `**Offene Tickets:** ${openCount} / ${maxTickets}\n` +
+ `**Verbleibend:** ${Math.max(0, maxTickets - openCount)}\n\n` +
(atLimit
- ? '⚠️ This user has reached their ticket limit.'
- : '✅ This user can still open more tickets.'),
+ ? '⚠️ Dieser Nutzer hat sein Ticket-Limit erreicht.'
+ : '✅ Dieser Nutzer kann noch weitere Tickets öffnen.'),
)
.setColor(atLimit ? getColor('error') : getColor('success'))
.setThumbnail(targetUser.displayAvatarURL({ size: 64 }))
@@ -868,33 +876,33 @@ async function handleCheckUser(selectInteraction, rootInteraction, guildConfig,
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No user was selected.',
+ message: 'Es wurde kein Nutzer ausgewählt.',
}).catch(() => {});
}
});
}
-async function handleRepostPanel(btnInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleRepostPanel(btnInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await btnInteraction.deferUpdate();
- const panelStatus = await getTicketPanelStatus(client, rootInteraction.guild, guildConfig);
+ const panelStatus = await getTicketPanelStatus(client, rootInteraction.guild!, guildConfig);
if (panelStatus.exists) {
await btnInteraction.followUp({
- embeds: [infoEmbed('Panel Already Active', 'The ticket panel is already posted in the configured channel.')],
+ embeds: [infoEmbed('Panel bereits aktiv', 'Das Ticket-Panel ist im eingestellten Kanal bereits gepostet.')],
flags: MessageFlags.Ephemeral,
}).catch(() => {});
await refreshDashboard(rootInteraction, guildConfig, guildId, client);
return;
}
- const sentPanel = await repostTicketPanel(client, rootInteraction.guild, guildConfig, guildId);
+ const sentPanel = await repostTicketPanel(client, rootInteraction.guild!, guildConfig, guildId);
await btnInteraction.followUp({
embeds: [
successEmbed(
- 'Panel Reposted',
- `A new ticket panel was posted in <#${guildConfig.ticketPanelChannelId}>.${
- sentPanel.url ? `\n[Open panel message](${sentPanel.url})` : ''
+ 'Panel neu gepostet',
+ `Ein neues Ticket-Panel wurde in <#${guildConfig.ticketPanelChannelId}> gepostet.${
+ sentPanel.url ? `\n[Panel-Nachricht öffnen](${sentPanel.url})` : ''
}`,
),
],
@@ -904,15 +912,15 @@ async function handleRepostPanel(btnInteraction, rootInteraction, guildConfig, g
await refreshDashboard(rootInteraction, guildConfig, guildId, client);
}
-async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleDeleteSystem(btnInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
const deleteModal = new ModalBuilder()
.setCustomId('ticket_delete_confirm_modal')
- .setTitle('Delete Ticket System')
+ .setTitle('Ticket-System löschen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('delete_confirmation')
- .setLabel('Type "DELETE" to confirm')
+ .setLabel('Gib „DELETE“ zur Bestätigung ein')
.setStyle(TextInputStyle.Short)
.setPlaceholder('DELETE')
.setMaxLength(6)
@@ -938,14 +946,14 @@ async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig,
const confirmation = submitted.fields.getTextInputValue('delete_confirmation').trim();
if (confirmation !== 'DELETE') {
- await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'You must type "DELETE" exactly to confirm deletion.' });
+ await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'Du musst genau „DELETE“ eingeben, um das Löschen zu bestätigen.' });
await refreshDashboard(rootInteraction, guildConfig, guildId, client);
return;
}
await submitted.deferUpdate();
- const keysToDelete = [
+ const keysToDelete: (keyof GuildConfig)[] = [
'ticketPanelChannelId',
'ticketPanelMessageId',
'ticketStaffRoleId',
@@ -959,7 +967,7 @@ async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig,
if (guildConfig.ticketPanelChannelId) {
try {
- const panelChannel = await client.guilds.cache.get(guildId)?.channels.fetch(guildConfig.ticketPanelChannelId).catch(() => null);
+ const panelChannel = await client.guilds.cache.get(guildId)?.channels.fetch(guildConfig.ticketPanelChannelId).catch(() => null) as GuildTextBasedChannel | null | undefined;
if (panelChannel) {
if (guildConfig.ticketPanelMessageId) {
const panelMessage = await panelChannel.messages.fetch(guildConfig.ticketPanelMessageId).catch(() => null);
@@ -976,7 +984,7 @@ async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig,
}
}
} catch (panelDeleteError) {
- logger.warn('Could not delete ticket panel message:', panelDeleteError.message);
+ logger.warn('Could not delete ticket panel message:', (panelDeleteError as ErrorLike).message);
}
}
@@ -989,7 +997,7 @@ async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig,
);
}
} catch (ticketDeleteError) {
- logger.warn('Could not clear ticket records from database:', ticketDeleteError.message);
+ logger.warn('Could not clear ticket records from database:', (ticketDeleteError as ErrorLike).message);
}
for (const key of keysToDelete) {
@@ -1000,8 +1008,8 @@ async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig,
await submitted.followUp({
embeds: [
successEmbed(
- '✅ Ticket System Deleted',
- 'All ticket system configuration has been cleared. Run `/ticket setup` to set it up again.',
+ '✅ Ticket-System gelöscht',
+ 'Die gesamte Ticket-Konfiguration wurde entfernt. Nutze `/ticket setup`, um es neu einzurichten.',
),
],
flags: MessageFlags.Ephemeral,
@@ -1010,8 +1018,8 @@ async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig,
await InteractionHelper.safeEditReply(rootInteraction, {
embeds: [
new EmbedBuilder()
- .setTitle('Ticket System Deleted')
- .setDescription('The ticket system configuration has been cleared.')
+ .setTitle('Ticket-System gelöscht')
+ .setDescription('Die Ticket-Konfiguration wurde entfernt.')
.setColor(getColor('error'))
.setTimestamp(),
],
diff --git a/src/commands/Ticket/priority.js b/src/commands/Ticket/priority.js
deleted file mode 100644
index fde4195156..0000000000
--- a/src/commands/Ticket/priority.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { getColor } from '../../config/bot.js';
-import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { getTicketPermissionContext } from '../../utils/ticket/ticketPermissions.js';
-import { updateTicketPriority } from '../../services/ticket.js';
-
-export default {
- data: new SlashCommandBuilder()
- .setName("priority")
- .setDescription("Sets the priority level for the current support ticket.")
- .addStringOption((option) =>
- option
- .setName("level")
- .setDescription("The priority level for the ticket.")
- .setRequired(true)
- .addChoices(
- { name: "Urgent", value: "urgent" },
- { name: "High", value: "high" },
- { name: "Medium", value: "medium" },
- { name: "Low", value: "low" },
- { name: "None", value: "none" },
- ),
- )
- .setDMPermission(false),
- category: "Ticket",
-
- async execute(interaction, guildConfig, client) {
- const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral });
- if (!deferred) {
- return;
- }
-
- const permissionContext = await getTicketPermissionContext({ client, interaction });
- if (!permissionContext.ticketData) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'This command can only be used in a valid ticket channel.' });
- }
-
- if (!permissionContext.canManageTicket) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the `Manage Channels` permission or the configured `Ticket Staff Role` to change ticket priority.' });
- }
-
- const priorityLevel = interaction.options.getString("level");
- await updateTicketPriority(interaction.channel, priorityLevel, interaction.user);
-
- await InteractionHelper.safeEditReply(interaction, {
- embeds: [
- successEmbed(
- "Priority Updated",
- `Ticket priority set to **${priorityLevel.toUpperCase()}**.`,
- ),
- ],
- });
-
- logger.info('Ticket priority updated successfully', {
- userId: interaction.user.id,
- userTag: interaction.user.tag,
- channelId: interaction.channel.id,
- channelName: interaction.channel.name,
- guildId: interaction.guildId,
- priority: priorityLevel,
- commandName: 'priority'
- });
- },
-};
diff --git a/src/commands/Ticket/ticket.js b/src/commands/Ticket/ticket.ts
similarity index 72%
rename from src/commands/Ticket/ticket.js
rename to src/commands/Ticket/ticket.ts
index 982747b118..b3ce45164d 100644
--- a/src/commands/Ticket/ticket.js
+++ b/src/commands/Ticket/ticket.ts
@@ -1,29 +1,30 @@
+import type { Command } from '../../types/index.js';
import { getColor } from '../../config/bot.js';
-import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js';
+import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, type GuildMember, type TextChannel } from 'discord.js';
import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
import { getGuildConfig, setGuildConfig } from '../../services/config/guildConfig.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import { logger } from '../../utils/logger.js';
-import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
+import { handleInteractionError, replyUserError, ErrorTypes, type ErrorLike } from '../../utils/errorHandler.js';
import ticketConfig from './modules/ticket_dashboard.js';
export default {
data: new SlashCommandBuilder()
.setName("ticket")
- .setDescription("Manages the server's ticket system.")
+ .setDescription("Verwaltet das Ticket-System des Servers.")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
.addSubcommand((subcommand) =>
subcommand
.setName("setup")
.setDescription(
- "Sets up the ticket creation panel in a specified channel.",
+ "Richtet das Ticket-Panel in einem bestimmten Kanal ein.",
)
.addChannelOption((option) =>
option
.setName("panel_channel")
.setDescription(
- "The channel where the ticket panel will be sent.",
+ "Der Kanal, in den das Ticket-Panel gesendet wird.",
)
.addChannelTypes(ChannelType.GuildText)
.setRequired(true),
@@ -33,7 +34,7 @@ export default {
option
.setName("panel_message")
.setDescription(
- "The main message/description for the ticket panel.",
+ "Die Hauptnachricht/Beschreibung für das Ticket-Panel.",
)
.setRequired(true),
)
@@ -41,7 +42,7 @@ export default {
option
.setName("button_label")
.setDescription(
- "The label for the ticket creation button (default: Create Ticket)",
+ "Beschriftung des Buttons zum Erstellen (Standard: Ticket erstellen)",
)
.setRequired(false),
)
@@ -49,7 +50,7 @@ export default {
option
.setName("category")
.setDescription(
- "The category where new tickets will be created (optional).",
+ "Die Kategorie, in der neue Tickets erstellt werden (optional).",
)
.addChannelTypes(ChannelType.GuildCategory)
.setRequired(false),
@@ -58,7 +59,7 @@ export default {
option
.setName("closed_category")
.setDescription(
- "The category where closed tickets will be moved (optional).",
+ "Die Kategorie, in die geschlossene Tickets verschoben werden (optional).",
)
.addChannelTypes(ChannelType.GuildCategory)
.setRequired(false),
@@ -67,14 +68,14 @@ export default {
option
.setName("staff_role")
.setDescription(
- "The role that can access tickets (optional).",
+ "Die Rolle, die Zugriff auf Tickets hat (optional).",
)
.setRequired(false),
)
.addIntegerOption((option) =>
option
.setName("max_tickets_per_user")
- .setDescription("Maximum number of tickets a user can create (default: 3)")
+ .setDescription("Maximale Anzahl an Tickets pro Nutzer (Standard: 3)")
.setMinValue(1)
.setMaxValue(10)
.setRequired(false),
@@ -82,14 +83,14 @@ export default {
.addBooleanOption((option) =>
option
.setName("dm_on_close")
- .setDescription("Send DM to user when their ticket is closed (default: true)")
+ .setDescription("Nutzer per DM informieren, wenn das Ticket geschlossen wird (Standard: true)")
.setRequired(false),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName("dashboard")
- .setDescription("Open the interactive ticket system dashboard"),
+ .setDescription("Das interaktive Ticket-Dashboard öffnen"),
),
category: "ticket",
@@ -100,7 +101,7 @@ export default {
}
if (
- !interaction.member.permissions.has(
+ !(interaction.member as GuildMember).permissions.has(
PermissionFlagsBits.ManageChannels,
)
) {
@@ -109,7 +110,7 @@ export default {
guildId: interaction.guildId,
commandName: 'ticket'
});
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the `Manage Channels` permission for this action.' });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst für diese Aktion die Berechtigung „Kanäle verwalten“.' });
}
const subcommand = interaction.options.getSubcommand();
@@ -119,30 +120,30 @@ export default {
}
if (subcommand === "setup") {
- const existingConfig = await getGuildConfig(client, interaction.guildId);
+ const existingConfig = await getGuildConfig(client, interaction.guildId!);
if (existingConfig?.ticketPanelChannelId) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `This server already has a ticket system set up (panel in <#${existingConfig.ticketPanelChannelId}>).\n\nOnly one ticket system is supported per server. Use \`/ticket dashboard\` to edit or update the existing setup, or select **Delete System** from the dashboard to remove it and start fresh.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Auf diesem Server ist bereits ein Ticket-System eingerichtet (Panel in <#${existingConfig.ticketPanelChannelId}>).\n\nPro Server wird nur ein Ticket-System unterstützt. Nutze \`/ticket dashboard\`, um die Einrichtung zu bearbeiten, oder wähl im Dashboard **System löschen**, um neu zu starten.` });
}
const panelChannel =
- interaction.options.getChannel("panel_channel");
+ interaction.options.getChannel("panel_channel") as TextChannel;
const categoryChannel = interaction.options.getChannel("category");
const closedCategoryChannel = interaction.options.getChannel("closed_category");
const staffRole = interaction.options.getRole("staff_role");
-const panelMessage = interaction.options.getString("panel_message") || "Click the button below to create a support ticket.";
+const panelMessage = interaction.options.getString("panel_message") || "Klick auf den Button unten, um ein Support-Ticket zu erstellen.";
const buttonLabel =
interaction.options.getString("button_label") ||
-"Create Ticket";
+"Ticket erstellen";
const maxTicketsPerUser = interaction.options.getInteger("max_tickets_per_user") || 3;
const dmOnClose = interaction.options.getBoolean("dm_on_close") !== false;
const setupEmbed = createEmbed({
- title: "Support Tickets",
+ title: "Support-Tickets",
description: panelMessage,
color: getColor('info')
});
- const ticketButton = new ActionRowBuilder().addComponents(
+ const ticketButton = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("create_ticket")
.setLabel(buttonLabel)
@@ -183,28 +184,28 @@ description: panelMessage,
});
}
- let successMessage = `The ticket creation panel has been sent to ${panelChannel}.`;
+ let successMessage = `Das Ticket-Panel wurde in ${panelChannel} gesendet.`;
if (categoryChannel) {
- successMessage += `New tickets will be created in the **${categoryChannel.name}** category.`;
+ successMessage += ` Neue Tickets werden in der Kategorie **${categoryChannel.name}** erstellt.`;
} else {
- successMessage += 'New tickets will be created in a new "Tickets" category.';
+ successMessage += ' Neue Tickets werden in einer neuen Kategorie „Tickets“ erstellt.';
}
if (closedCategoryChannel) {
- successMessage += `Closed tickets will be moved to **${closedCategoryChannel.name}**.`;
+ successMessage += ` Geschlossene Tickets werden nach **${closedCategoryChannel.name}** verschoben.`;
}
if (staffRole) {
- successMessage += `**${staffRole.name}** role will have access to tickets.`;
+ successMessage += ` Die Rolle **${staffRole.name}** hat Zugriff auf Tickets.`;
}
- successMessage += `\n\n**Max Tickets Per User:** ${maxTicketsPerUser}\n**DM on Close:** ${dmOnClose ? 'Enabled' : 'Disabled'}`;
+ successMessage += `\n\n**Max. Tickets pro Nutzer:** ${maxTicketsPerUser}\n**DM beim Schließen:** ${dmOnClose ? 'Aktiviert' : 'Deaktiviert'}`;
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- "Ticket Panel Set Up",
+ "Ticket-Panel eingerichtet",
successMessage,
),
],
@@ -224,45 +225,45 @@ description: panelMessage,
});
const logEmbed = createEmbed({
- title: "Ticket System Setup (Configuration Log)",
- description: `The ticket panel was set up in ${panelChannel} by ${interaction.user}.`,
+ title: "Einrichtung des Ticket-Systems (Konfigurations-Log)",
+ description: `Das Ticket-Panel wurde von ${interaction.user} in ${panelChannel} eingerichtet.`,
color: getColor('warning')
})
.addFields(
{
- name: "Panel Channel",
+ name: "Panel-Kanal",
value: panelChannel.toString(),
inline: true,
},
{
- name: "Ticket Category",
+ name: "Ticket-Kategorie",
value: categoryChannel
? categoryChannel.toString()
- : "None specified.",
+ : "Keine angegeben.",
inline: true,
},
{
- name: "Closed Category",
+ name: "Kategorie für geschlossene Tickets",
value: closedCategoryChannel
? closedCategoryChannel.toString()
- : "None specified.",
+ : "Keine angegeben.",
inline: true,
},
{
- name: "Staff Role",
+ name: "Team-Rolle",
value: staffRole
? staffRole.toString()
- : "None specified.",
+ : "Keine angegeben.",
inline: true,
},
{
- name: "Max Tickets Per User",
+ name: "Max. Tickets pro Nutzer",
value: maxTicketsPerUser.toString(),
inline: true,
},
{
- name: "DM on Close",
- value: dmOnClose ? 'Enabled' : 'Disabled',
+ name: "DM beim Schließen",
+ value: dmOnClose ? 'Aktiviert' : 'Deaktiviert',
inline: true,
},
{
@@ -274,14 +275,14 @@ description: panelMessage,
} catch (error) {
logger.error('Ticket setup error', {
- error: error.message,
- stack: error.stack,
+ error: (error as ErrorLike).message,
+ stack: (error as ErrorLike).stack,
userId: interaction.user.id,
guildId: interaction.guildId,
commandName: 'ticket_setup'
});
if (interaction.deferred || interaction.replied) {
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Could not send the ticket panel or save configuration. Check the bot\'s permissions (especially the ability to send messages in the target channel) and database connection.' }).catch(err => {
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Das Ticket-Panel konnte nicht gesendet oder die Konfiguration nicht gespeichert werden. Prüf die Berechtigungen des Bots (vor allem Nachrichten senden im Zielkanal) und die Datenbankverbindung.' }).catch(err => {
logger.error('Failed to send error reply', {
error: err.message,
guildId: interaction.guildId
@@ -296,4 +297,4 @@ description: panelMessage,
}
}
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Ticket/tickets.ts b/src/commands/Ticket/tickets.ts
new file mode 100644
index 0000000000..0f33187b2a
--- /dev/null
+++ b/src/commands/Ticket/tickets.ts
@@ -0,0 +1,15 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import claimCommand from './modules/claim.js';
+import closeCommand from './modules/close.js';
+import priorityCommand from './modules/priority.js';
+
+export default createRouterCommand({
+ name: 'tickets',
+ description: 'Ticket-Aktionen in einem Ticket-Kanal',
+ category: 'Ticket',
+ routes: [
+ { legacyName: 'claim', module: claimCommand },
+ { legacyName: 'close', module: closeCommand },
+ { legacyName: 'priority', module: priorityCommand },
+ ],
+});
diff --git a/src/commands/Tools/baseconvert.js b/src/commands/Tools/modules/baseconvert.ts
similarity index 68%
rename from src/commands/Tools/baseconvert.js
rename to src/commands/Tools/modules/baseconvert.ts
index 1ef560dc7e..c71ec604e7 100644
--- a/src/commands/Tools/baseconvert.js
+++ b/src/commands/Tools/modules/baseconvert.ts
@@ -1,15 +1,23 @@
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { getColor } from '../../config/bot.js';
-
-const BASE_ALPHABETS = {
- 'BIN': { base: 2, prefix: '0b', name: 'Binary', alphabet: '01' },
- 'OCT': { base: 8, prefix: '0o', name: 'Octal', alphabet: '0-7' },
- 'DEC': { base: 10, prefix: '', name: 'Decimal', alphabet: '0-9' },
- 'HEX': { base: 16, prefix: '0x', name: 'Hexadecimal', alphabet: '0-9A-F' },
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { getColor } from '../../../config/bot.js';
+import type { Command } from '../../../types/index.js';
+
+interface BaseInfo {
+ base: number;
+ prefix: string;
+ name: string;
+ alphabet: string;
+}
+
+const BASE_ALPHABETS: Record = {
+ 'BIN': { base: 2, prefix: '0b', name: 'Binär', alphabet: '01' },
+ 'OCT': { base: 8, prefix: '0o', name: 'Oktal', alphabet: '0-7' },
+ 'DEC': { base: 10, prefix: '', name: 'Dezimal', alphabet: '0-9' },
+ 'HEX': { base: 16, prefix: '0x', name: 'Hexadezimal', alphabet: '0-9A-F' },
'B64': { base: 64, prefix: 'b64:', name: 'Base64', alphabet: 'A-Za-z0-9+/=' },
'B36': { base: 36, prefix: '', name: 'Base36', alphabet: '0-9A-Z' },
'B58': { base: 58, prefix: '', name: 'Base58', alphabet: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' },
@@ -17,7 +25,7 @@ const BASE_ALPHABETS = {
};
const BASE_NAMES = Object.entries(BASE_ALPHABETS).map(([key, { name }]) => ({ name: `${key} (${name})`, value: key }));
-const BASE_CHARSETS = {
+const BASE_CHARSETS: Record = {
BIN: '01',
OCT: '01234567',
DEC: '0123456789',
@@ -27,7 +35,7 @@ const BASE_CHARSETS = {
B62: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
};
-function parseBigIntFromBase(value, baseKey) {
+function parseBigIntFromBase(value: string, baseKey: string): bigint {
if (baseKey === 'B64') {
const bytes = Buffer.from(value, 'base64');
return bytes.reduce((acc, byte) => (acc * 256n) + BigInt(byte), 0n);
@@ -56,7 +64,7 @@ function parseBigIntFromBase(value, baseKey) {
return result;
}
-function formatBigIntToBase(value, baseKey) {
+function formatBigIntToBase(value: bigint, baseKey: string): string {
if (baseKey === 'B64') {
if (value === 0n) {
return Buffer.from([0]).toString('base64');
@@ -97,19 +105,19 @@ function formatBigIntToBase(value, baseKey) {
export default {
data: new SlashCommandBuilder()
.setName('baseconvert')
- .setDescription('Convert numbers between different bases')
+ .setDescription('Rechne Zahlen zwischen verschiedenen Zahlensystemen um')
.addStringOption(option =>
option.setName('number')
- .setDescription('The number to convert')
+ .setDescription('Die Zahl, die umgerechnet werden soll')
.setRequired(true))
.addStringOption(option =>
option.setName('from')
- .setDescription('Source base/format')
+ .setDescription('Ausgangssystem/-format')
.setRequired(true)
.addChoices(...BASE_NAMES))
.addStringOption(option =>
option.setName('to')
- .setDescription('Target base/format (default: all)')
+ .setDescription('Zielsystem/-format (Standard: alle)')
.setRequired(false)
.addChoices(...BASE_NAMES)),
@@ -124,8 +132,8 @@ export default {
return;
}
- const numberStr = interaction.options.getString('number').trim();
- const fromBase = interaction.options.getString('from');
+ const numberStr = interaction.options.getString('number')!.trim();
+ const fromBase = interaction.options.getString('from')!;
const toBase = interaction.options.getString('to');
const { prefix: fromPrefix, name: fromName } = BASE_ALPHABETS[fromBase];
@@ -137,7 +145,7 @@ export default {
if (!cleanNumber) {
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'You must provide a number to convert.\n\n**Example:** `/baseconvert number:1010 from:BIN to:DEC`',
+ message: 'Gib eine Zahl zum Umrechnen an.\n\n**Beispiel:** `/tools baseconvert number:1010 from:BIN to:DEC`',
});
}
@@ -147,22 +155,22 @@ export default {
if (!regex.test(cleanNumber)) {
let examples = '';
if (fromBase === 'BIN') {
- examples = '\n\n**Valid:** 101, 1010, 11111 | **Invalid:** 5 (digit 5 not allowed)';
+ examples = '\n\n**Gültig:** 101, 1010, 11111 | **Ungültig:** 5 (Ziffer 5 nicht erlaubt)';
} else if (fromBase === 'OCT') {
- examples = '\n\n**Valid:** 77, 123, 755 | **Invalid:** 8 (only 0-7 allowed)';
+ examples = '\n\n**Gültig:** 77, 123, 755 | **Ungültig:** 8 (nur 0-7 erlaubt)';
} else if (fromBase === 'DEC') {
- examples = '\n\n**Valid:** 42, 123, 999 | **Invalid:** 12.34 (no decimals)';
+ examples = '\n\n**Gültig:** 42, 123, 999 | **Ungültig:** 12.34 (keine Nachkommastellen)';
} else if (fromBase === 'HEX') {
- examples = '\n\n**Valid:** FF, A1B2, DEADBEEF | **Invalid:** G (only 0-9, A-F)';
+ examples = '\n\n**Gültig:** FF, A1B2, DEADBEEF | **Ungültig:** G (nur 0-9, A-F)';
}
logger.warn(`Invalid base conversion input: ${cleanNumber} for base ${fromBase}`);
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: `You provided: \`${cleanNumber}\`\n\nValid characters: \`${alphabet}\`${examples}`,
+ message: `Deine Eingabe: \`${cleanNumber}\`\n\nGültige Zeichen: \`${alphabet}\`${examples}`,
});
}
- let decimalValue;
+ let decimalValue: bigint;
try {
if (fromBase === 'B64') {
decimalValue = parseBigIntFromBase(cleanNumber, fromBase);
@@ -173,7 +181,7 @@ export default {
logger.error('Base conversion parse error:', error);
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'The number is too large to process.\n\nTry with a smaller number.',
+ message: 'Die Zahl ist zu groß zum Verarbeiten.\n\nVersuch es mit einer kleineren Zahl.',
});
}
@@ -185,10 +193,10 @@ export default {
result = formatBigIntToBase(decimalValue, toBase);
const embed = successEmbed(
- '🔄 Base Conversion Result',
- `**From ${fromName} (${fromBase}):** \`${fromPrefix}${cleanNumber}\`\n` +
- `**To ${toName} (${toBase}):** \`${toPrefix}${result}\`\n` +
- `**Decimal:** \`${decimalValue.toLocaleString()}\``
+ '🔄 Ergebnis der Umrechnung',
+ `**Von ${fromName} (${fromBase}):** \`${fromPrefix}${cleanNumber}\`\n` +
+ `**Nach ${toName} (${toBase}):** \`${toPrefix}${result}\`\n` +
+ `**Dezimal:** \`${decimalValue.toLocaleString()}\``
);
embed.setColor(getColor('success'));
@@ -198,13 +206,13 @@ export default {
logger.error(`Base conversion error to ${toName}:`, error);
await replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'The result would be too large or incompatible.\n\nTry with a smaller number or different target base.',
+ message: 'Das Ergebnis wäre zu groß oder nicht kompatibel.\n\nVersuch es mit einer kleineren Zahl oder einem anderen Zielsystem.',
});
}
} else {
- let description = `**Input (${fromName}):** \`${fromPrefix}${cleanNumber}\`\n`;
- description += `**Decimal:** \`${decimalValue.toLocaleString()}\`\n\n`;
+ let description = `**Eingabe (${fromName}):** \`${fromPrefix}${cleanNumber}\`\n`;
+ description += `**Dezimal:** \`${decimalValue.toLocaleString()}\`\n\n`;
for (const [baseKey, { prefix, name }] of Object.entries(BASE_ALPHABETS)) {
if (baseKey === fromBase) continue;
@@ -214,12 +222,12 @@ export default {
description += `**${name} (${baseKey}):** \`${prefix}${value}\`\n`;
} catch (error) {
- description += `**${name} (${baseKey}):** *Too large to convert*\n`;
+ description += `**${name} (${baseKey}):** *Zu groß zum Umrechnen*\n`;
}
}
const embed = successEmbed(
- '🔄 Base Conversion Results',
+ '🔄 Ergebnisse der Umrechnung',
description
);
embed.setColor(getColor('primary'));
@@ -227,4 +235,4 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
}
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/calculate.js b/src/commands/Tools/modules/calculate.ts
similarity index 69%
rename from src/commands/Tools/calculate.js
rename to src/commands/Tools/modules/calculate.ts
index 233b88ab8f..14cb44ff75 100644
--- a/src/commands/Tools/calculate.js
+++ b/src/commands/Tools/modules/calculate.ts
@@ -1,17 +1,34 @@
-import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { evaluateMathExpression } from '../../utils/safeMathParser.js';
+import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, type MessageComponentInteraction } from 'discord.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { evaluateMathExpression } from '../../../utils/safeMathParser.js';
+import type { Command } from '../../../types/index.js';
+
+/** Pending operand-modal context, read by handlers/calculateModals. */
+export interface CalculationContext {
+ expression: string;
+ formattedResult: string;
+ operator: string;
+ messageId: string | undefined;
+ channelId: string;
+ userId: string;
+}
+
+interface CalculationHistoryEntry {
+ expression: string;
+ result: string;
+ timestamp: number;
+}
-const calculationContexts = new Map();
+const calculationContexts = new Map();
-function evaluate(expression) {
+function evaluate(expression: string) {
return evaluateMathExpression(expression);
}
-const calculationHistory = new Map();
+const calculationHistory = new Map();
const MAX_HISTORY = 5;
export { calculationContexts };
@@ -19,12 +36,12 @@ export { calculationContexts };
export default {
data: new SlashCommandBuilder()
.setName("calculate")
- .setDescription("Evaluate a mathematical expression")
+ .setDescription("Berechnet einen mathematischen Ausdruck")
.addStringOption((option) =>
option
.setName("expression")
.setDescription(
- "The mathematical expression to evaluate (e.g., 2+2*3, sin(45 deg), 16^0.5)",
+ "Der Ausdruck, der berechnet werden soll (z. B. 2+2*3, sin(45 deg), 16^0.5)",
)
.setRequired(true),
),
@@ -40,16 +57,16 @@ export default {
return;
}
- const expression = interaction.options.getString("expression");
+ const expression = interaction.options.getString("expression")!;
if (
!/^[0-9+\-*/.()^%! ,<>=&|~?:\[\]{}a-z√π∞°]+$/i.test(expression)
) {
return await replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: '**Contains unsupported characters.**\n\n' +
- '✅ Supported: Numbers, decimals, + - * / ^ %, sin cos tan sqrt abs log exp, pi e, ()\n' +
- '❌ Not supported: Brackets, curly braces, and other symbols'
+ message: '**Enthält nicht unterstützte Zeichen.**\n\n' +
+ '✅ Unterstützt: Zahlen, Dezimalzahlen, + - * / ^ %, sin cos tan sqrt abs log exp, pi e, ()\n' +
+ '❌ Nicht unterstützt: eckige und geschweifte Klammern sowie andere Symbole'
});
}
@@ -66,9 +83,9 @@ export default {
if (pattern.test(expression)) {
return await replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: '**Contains blocked code patterns.**\n\n' +
- '🚫 **Blocked:** import, require, eval, Function, setTimeout, setInterval, process, fs, document, window, fetch, loops, async/await\n\n' +
- 'Code-like syntax is not allowed in calculations.'
+ message: '**Enthält gesperrte Code-Muster.**\n\n' +
+ '🚫 **Gesperrt:** import, require, eval, Function, setTimeout, setInterval, process, fs, document, window, fetch, Schleifen, async/await\n\n' +
+ 'Code-ähnliche Syntax ist in Berechnungen nicht erlaubt.'
});
}
}
@@ -92,7 +109,7 @@ export default {
} else if (typeof result === "boolean") {
formattedResult = result ? "true" : "false";
} else if (result === null || result === undefined) {
- formattedResult = "No result";
+ formattedResult = "Kein Ergebnis";
} else if (
Array.isArray(result) ||
typeof result === "object"
@@ -108,7 +125,7 @@ export default {
calculationHistory.set(userId, []);
}
- const history = calculationHistory.get(userId);
+ const history = calculationHistory.get(userId)!;
history.unshift({
expression,
result: formattedResult,
@@ -119,7 +136,7 @@ export default {
history.pop();
}
- const row = new ActionRowBuilder().addComponents(
+ const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`calc_${interaction.id}_add`)
.setLabel("+")
@@ -138,15 +155,15 @@ export default {
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`calc_${interaction.id}_history`)
- .setLabel("History")
+ .setLabel("Verlauf")
.setStyle(ButtonStyle.Secondary),
);
const embed = successEmbed(
- "🧮 Calculation Result",
- `**Expression:** \`${expression.replace(/`/g, "\`")}\`\n` +
- `**Result:** \`${formattedResult}\`\n\n` +
- `*Use the buttons below to perform operations with the result.*`,
+ "🧮 Rechenergebnis",
+ `**Ausdruck:** \`${expression.replace(/`/g, "\`")}\`\n` +
+ `**Ergebnis:** \`${formattedResult}\`\n\n` +
+ `*Nutze die Buttons unten, um mit dem Ergebnis weiterzurechnen.*`,
);
await InteractionHelper.safeEditReply(interaction, {
@@ -154,12 +171,12 @@ export default {
components: [row],
});
- const filter = (i) =>
+ const filter = (i: MessageComponentInteraction) =>
i.customId.startsWith(`calc_${interaction.id}`) &&
i.user.id === interaction.user.id;
const BUTTON_TIMEOUT = 300000;
const collector =
- interaction.channel.createMessageComponentCollector({
+ interaction.channel!.createMessageComponentCollector({
filter,
time: BUTTON_TIMEOUT,
});
@@ -178,7 +195,7 @@ export default {
if (userHistory.length === 0) {
await i.followUp({
- content: "No calculation history found.",
+ content: "Kein Rechenverlauf gefunden.",
flags: ["Ephemeral"],
});
return;
@@ -193,7 +210,7 @@ export default {
.join("\n\n");
await i.followUp({
- content: `📜 **Your Calculation History**\n\n${historyText}`,
+ content: `📜 **Dein Rechenverlauf**\n\n${historyText}`,
flags: ["Ephemeral"],
});
return;
@@ -222,14 +239,15 @@ export default {
expression,
formattedResult,
operator,
- messageId: interaction.message?.id,
+ // ChatInputCommandInteraction has no .message, so this was always undefined in JS
+ messageId: undefined,
channelId: interaction.channelId,
userId: i.user.id
});
await i.showModal({
customId: `calc_modal:${operation}`,
- title: `Enter a number to ${operation}`,
+ title: `Zahl eingeben (${operator})`,
components: [
{
type: 1,
@@ -237,8 +255,8 @@ export default {
{
type: 4,
customId: `operand:${contextKey}`,
- label: `Number to ${operator} with ${formattedResult}`,
- placeholder: "Enter a number...",
+ label: `Rechnung: ${formattedResult} ${operator} …`,
+ placeholder: "Zahl eingeben …",
style: 1,
required: true,
maxLength: 50,
@@ -251,7 +269,7 @@ export default {
logger.error("Failed to show modal:", modalError);
if (!i.replied && !i.deferred) {
await i.reply({
- content: "Failed to open calculator. Please try again.",
+ content: "Der Rechner konnte nicht geöffnet werden. Versuch es noch mal.",
flags: ["Ephemeral"],
}).catch(console.error);
}
@@ -262,7 +280,7 @@ export default {
logger.error("Button interaction error:", error);
if (!i.deferred && !i.replied) {
await i.followUp({
- content: "An error occurred while processing your request.",
+ content: "Beim Verarbeiten deiner Anfrage ist ein Fehler aufgetreten.",
flags: ["Ephemeral"],
}).catch(console.error);
}
@@ -272,12 +290,12 @@ export default {
collector.on("end", (collected, reason) => {
if (reason === "timeout") {
const disabledRow =
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(
`calc_${interaction.id}_expired`,
)
- .setLabel("Calculator Expired")
+ .setLabel("Rechner abgelaufen")
.setStyle(ButtonStyle.Secondary)
.setDisabled(true),
);
@@ -286,11 +304,11 @@ export default {
.editReply({
components: [disabledRow],
content:
- "⏱️ This calculator has expired. Use the command again to perform more calculations.",
+ "⏱️ Dieser Rechner ist abgelaufen. Nutze den Command erneut, um weiterzurechnen.",
})
.catch(console.error);
} else {
- const disabledRow = ActionRowBuilder.from(
+ const disabledRow = ActionRowBuilder.from(
row,
).setComponents(
row.components.map((component) =>
@@ -306,24 +324,25 @@ export default {
} catch (error) {
logger.error('Calculation error:', error);
- let errorMessage = 'Failed to evaluate the expression.';
+ let errorMessage = 'Der Ausdruck konnte nicht berechnet werden. ';
+ const errorText = error instanceof Error ? error.message : String(error);
- if (error.message.includes('Unexpected type')) {
+ if (errorText.includes('Unexpected type')) {
errorMessage +=
- 'The expression contains an unsupported operation or function.';
- } else if (error.message.includes('Undefined symbol')) {
+ 'Er enthält eine nicht unterstützte Operation oder Funktion.';
+ } else if (errorText.includes('Undefined symbol')) {
errorMessage +=
- 'The expression contains an undefined variable or function.';
- } else if (error.message.includes('Brackets not balanced')) {
- errorMessage += 'The expression has unbalanced brackets.';
+ 'Er enthält eine unbekannte Variable oder Funktion.';
+ } else if (errorText.includes('Brackets not balanced')) {
+ errorMessage += 'Die Klammern sind nicht ausgeglichen.';
} else if (
- error.message.includes('Unexpected operator') ||
- error.message.includes('Unexpected character')
+ errorText.includes('Unexpected operator') ||
+ errorText.includes('Unexpected character')
) {
errorMessage +=
- 'The expression contains an invalid operator or character.';
+ 'Er enthält einen ungültigen Operator oder ein ungültiges Zeichen.';
} else {
- errorMessage += 'Please check the syntax and try again.';
+ errorMessage += 'Bitte prüf die Syntax und versuch es noch mal.';
}
await replyUserError(interaction, {
@@ -332,4 +351,4 @@ export default {
});
}
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/countdown.js b/src/commands/Tools/modules/countdown.ts
similarity index 55%
rename from src/commands/Tools/countdown.js
rename to src/commands/Tools/modules/countdown.ts
index d7561ddbf3..8ed3251fd9 100644
--- a/src/commands/Tools/countdown.js
+++ b/src/commands/Tools/modules/countdown.ts
@@ -1,21 +1,34 @@
-import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { createControlButtons, formatTime, startCountdown } from '../../handlers/countdownButtons.js';
+import { SlashCommandBuilder, MessageFlags, type ActionRowBuilder, type ButtonBuilder, type GuildTextBasedChannel, type Message } from 'discord.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { createControlButtons, formatTime, startCountdown } from '../../../handlers/countdownButtons.js';
+import type { Command } from '../../../types/index.js';
-const activeCountdowns = new Map();
+/** Live countdown state, shared with handlers/countdownButtons. */
+export interface CountdownData {
+ message: Message;
+ endTime: number;
+ remainingTime: number;
+ isPaused: boolean;
+ title: string;
+ lastUpdate: number;
+ interval: ReturnType | null;
+}
+
+const activeCountdowns = new Map();
export { activeCountdowns };
export default {
data: new SlashCommandBuilder()
.setName("countdown")
- .setDescription("Start a countdown timer")
+ .setDescription("Starte einen Countdown")
.addIntegerOption((option) =>
option
.setName("minutes")
- .setDescription("Number of minutes to count down (0-1440)")
+ .setDescription("Anzahl der Minuten (0-1440)")
.setMinValue(0)
.setMaxValue(1440)
.setRequired(false),
@@ -23,7 +36,7 @@ export default {
.addIntegerOption((option) =>
option
.setName("seconds")
- .setDescription("Number of seconds to count down (0-59)")
+ .setDescription("Anzahl der Sekunden (0-59)")
.setMinValue(0)
.setMaxValue(59)
.setRequired(false),
@@ -31,7 +44,7 @@ export default {
.addStringOption((option) =>
option
.setName("title")
- .setDescription("Optional title for the countdown")
+ .setDescription("Optionaler Titel für den Countdown")
.setRequired(false),
),
@@ -48,34 +61,34 @@ export default {
const minutes = interaction.options.getInteger("minutes") || 0;
const seconds = interaction.options.getInteger("seconds") || 0;
- const title = interaction.options.getString("title") || "Countdown Timer";
+ const title = interaction.options.getString("title") || "Countdown";
const totalSeconds = minutes * 60 + seconds;
if (totalSeconds <= 0) {
- throw new Error("Please specify a duration of at least 1 second.");
+ throw createError("Please specify a duration of at least 1 second.", ErrorTypes.VALIDATION, "Bitte gib eine Dauer von mindestens 1 Sekunde an.", { expected: true });
}
if (totalSeconds > 86400) {
- throw new Error("Countdown cannot be longer than 24 hours.");
+ throw createError("Countdown cannot be longer than 24 hours.", ErrorTypes.VALIDATION, "Ein Countdown darf höchstens 24 Stunden dauern.", { expected: true });
}
const endTime = Date.now() + totalSeconds * 1000;
const countdownId = `${interaction.channelId}-${Date.now()}`;
- const row = createControlButtons(countdownId);
+ const row = createControlButtons(countdownId) as ActionRowBuilder;
const initialEmbed = successEmbed(
`⏱️ ${title}`,
- `Time remaining: **${formatTime(totalSeconds)}**`,
+ `Verbleibende Zeit: **${formatTime(totalSeconds)}**`,
);
- const message = await interaction.channel.send({
+ const message = await (interaction.channel as GuildTextBasedChannel).send({
embeds: [initialEmbed],
components: [row],
});
- const countdownData = {
+ const countdownData: CountdownData = {
message,
endTime,
remainingTime: totalSeconds * 1000,
@@ -89,8 +102,8 @@ export default {
startCountdown(countdownId, countdownData, activeCountdowns);
await InteractionHelper.safeEditReply(interaction, {
- content: "✅ Countdown started!",
+ content: "✅ Countdown gestartet!",
flags: MessageFlags.Ephemeral,
});
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/embedbuilder.js b/src/commands/Tools/modules/embedbuilder.ts
similarity index 70%
rename from src/commands/Tools/embedbuilder.js
rename to src/commands/Tools/modules/embedbuilder.ts
index 3da94f3dc1..ec05005840 100644
--- a/src/commands/Tools/embedbuilder.js
+++ b/src/commands/Tools/modules/embedbuilder.ts
@@ -16,31 +16,68 @@ import {
EmbedBuilder,
LabelBuilder,
RadioGroupBuilder,
+ type ButtonInteraction,
+ type ChatInputCommandInteraction,
+ type EmbedAuthorOptions,
+ type EmbedFooterOptions,
+ type Guild,
+ type NewsChannel,
+ type TextChannel,
} from 'discord.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { successEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { TitanBotError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { getColor } from '../../config/bot.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { successEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { NexoBotError, replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { getColor } from '../../../config/bot.js';
+import type { Command } from '../../../types/index.js';
+
+interface EmbedAuthorState {
+ name: string;
+ iconUrl: string | null;
+ url: string | null;
+}
+
+interface EmbedFooterState {
+ text: string;
+ iconUrl: string | null;
+}
+
+interface EmbedFieldState {
+ name: string;
+ value: string;
+ inline: boolean;
+}
+
+interface EmbedBuilderState {
+ title: string | null;
+ description: string | null;
+ color: string | number | null;
+ author: EmbedAuthorState | null;
+ footer: EmbedFooterState | null;
+ thumbnail: string | null;
+ image: string | null;
+ timestamp: boolean;
+ fields: EmbedFieldState[];
+}
const MAX_FIELDS = 25;
const IDLE_TIMEOUT = 900_000;
const COLOR_PRESETS = [
- { label: 'Primary (Blue)', value: '#336699', emoji: '' },
- { label: 'Success (Green)', value: '#57F287', emoji: '' },
- { label: 'Error (Red)', value: '#ED4245', emoji: '' },
- { label: 'Warning (Yellow)', value: '#FEE75C', emoji: '' },
- { label: 'Info (Bright Blue)', value: '#3498DB', emoji: '' },
+ { label: 'Primär (Blau)', value: '#336699', emoji: '' },
+ { label: 'Erfolg (Grün)', value: '#57F287', emoji: '' },
+ { label: 'Fehler (Rot)', value: '#ED4245', emoji: '' },
+ { label: 'Warnung (Gelb)', value: '#FEE75C', emoji: '' },
+ { label: 'Info (Hellblau)', value: '#3498DB', emoji: '' },
{ label: 'Blurple (Discord)', value: '#5865F2', emoji: '' },
{ label: 'Fuchsia', value: '#EB459E', emoji: '' },
{ label: 'Gold', value: '#F1C40F', emoji: '' },
- { label: 'White', value: '#FFFFFF', emoji: '' },
- { label: 'Dark', value: '#202225', emoji: '' },
- { label: 'Custom Hex...', value: '__custom__', emoji: '' },
+ { label: 'Weiß', value: '#FFFFFF', emoji: '' },
+ { label: 'Dunkel', value: '#202225', emoji: '' },
+ { label: 'Eigener Hex-Wert …', value: '__custom__', emoji: '' },
];
-function isValidUrl(str) {
+function isValidUrl(str: string) {
try {
const url = new URL(str);
return url.protocol === 'http:' || url.protocol === 'https:';
@@ -49,11 +86,11 @@ function isValidUrl(str) {
}
}
-function isValidHex(str) {
+function isValidHex(str: string) {
return /^#[0-9A-Fa-f]{6}$/.test(str);
}
-function resolveEmbedColor(value) {
+function resolveEmbedColor(value: string | number | null) {
try {
const resolved = getColor(value || 'primary');
if (typeof resolved === 'number' && Number.isFinite(resolved) && resolved >= 0 && resolved <= 0xffffff) {
@@ -65,7 +102,7 @@ function resolveEmbedColor(value) {
return getColor('primary');
}
-function buildPreviewEmbed(state) {
+function buildPreviewEmbed(state: EmbedBuilderState) {
const embed = new EmbedBuilder();
if (state.title) embed.setTitle(state.title.substring(0, 256));
@@ -74,14 +111,14 @@ function buildPreviewEmbed(state) {
embed.setColor(resolveEmbedColor(state.color));
if (state.author?.name) {
- const obj = { name: state.author.name.substring(0, 256) };
+ const obj: EmbedAuthorOptions = { name: state.author.name.substring(0, 256) };
if (state.author.iconUrl && isValidUrl(state.author.iconUrl)) obj.iconURL = state.author.iconUrl;
if (state.author.url && isValidUrl(state.author.url)) obj.url = state.author.url;
embed.setAuthor(obj);
}
if (state.footer?.text) {
- const obj = { text: state.footer.text.substring(0, 2048) };
+ const obj: EmbedFooterOptions = { text: state.footer.text.substring(0, 2048) };
if (state.footer.iconUrl && isValidUrl(state.footer.iconUrl)) obj.iconURL = state.footer.iconUrl;
embed.setFooter(obj);
}
@@ -98,99 +135,99 @@ function buildPreviewEmbed(state) {
state.fields.length === 0 &&
!state.author?.name
) {
- embed.setDescription('*(Empty — use the menu below to add content)*');
+ embed.setDescription('*(Leer – nutze das Menü unten, um Inhalte hinzuzufügen)*');
}
return embed;
}
-function buildDashboardEmbed(state) {
- const trunc = (str, n) =>
+function buildDashboardEmbed(state: EmbedBuilderState) {
+ const trunc = (str: string, n: number) =>
str.length > n ? str.substring(0, n) + '…' : str;
const lines = [
- `**Title** › ${state.title ?`\`${trunc(state.title, 40)}\`` : '`Not set`'}`,
- `**Description** › ${state.description ?`${state.description.length} character(s)`: '`Not set`'}`,
- `**Color** › ${state.color ?`\`${state.color}\`` : '`Default`'}`,
- `**Author** › ${state.author?.name ?`\`${trunc(state.author.name, 30)}\`` : '`Not set`'}`,
- `**Footer** › ${state.footer?.text ?`\`${trunc(state.footer.text, 30)}\`` : '`Not set`'}`,
- `**Thumbnail** › ${state.thumbnail ? '✅ Set' : '`Not set`'}`,
- `**Image** › ${state.image ? '✅ Set' : '`Not set`'}`,
- `**Timestamp** › ${state.timestamp ? '✅ Enabled' : '`Disabled`'}`,
- `**Fields** › ${state.fields.length} / ${MAX_FIELDS}`,
+ `**Titel** › ${state.title ?`\`${trunc(state.title, 40)}\`` : '`Nicht gesetzt`'}`,
+ `**Beschreibung** › ${state.description ?`${state.description.length} Zeichen`: '`Nicht gesetzt`'}`,
+ `**Farbe** › ${state.color ?`\`${state.color}\`` : '`Standard`'}`,
+ `**Autor** › ${state.author?.name ?`\`${trunc(state.author.name, 30)}\`` : '`Nicht gesetzt`'}`,
+ `**Footer** › ${state.footer?.text ?`\`${trunc(state.footer.text, 30)}\`` : '`Nicht gesetzt`'}`,
+ `**Vorschaubild** › ${state.thumbnail ? '✅ Gesetzt' : '`Nicht gesetzt`'}`,
+ `**Bild** › ${state.image ? '✅ Gesetzt' : '`Nicht gesetzt`'}`,
+ `**Zeitstempel** › ${state.timestamp ? '✅ Aktiviert' : '`Deaktiviert`'}`,
+ `**Felder** › ${state.fields.length} / ${MAX_FIELDS}`,
];
return new EmbedBuilder()
- .setTitle('Embed Builder — Control Panel')
+ .setTitle('Embed-Builder – Steuerung')
.setDescription(lines.join('\n'))
.setColor(getColor('info'))
- .setFooter({ text: 'The preview above updates live · Closes after 5 min of inactivity' });
+ .setFooter({ text: 'Die Vorschau oben aktualisiert sich live · Schließt nach 15 Min. Inaktivität' });
}
-function buildMainMenu(state) {
- const primaryRow = new ActionRowBuilder().addComponents(
+function buildMainMenu(state: EmbedBuilderState) {
+ const primaryRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('eb_main_edit_content')
- .setLabel('Edit Content')
+ .setLabel('Inhalt bearbeiten')
.setStyle(ButtonStyle.Primary)
.setEmoji('✏️'),
new ButtonBuilder()
.setCustomId('eb_main_set_color')
- .setLabel('Set Color')
+ .setLabel('Farbe festlegen')
.setStyle(ButtonStyle.Secondary)
.setEmoji('🎨'),
new ButtonBuilder()
.setCustomId('eb_main_set_images')
- .setLabel('Set Images')
+ .setLabel('Bilder festlegen')
.setStyle(ButtonStyle.Secondary)
.setEmoji('🖼️'),
new ButtonBuilder()
.setCustomId('eb_main_post_embed')
- .setLabel('Post Embed')
+ .setLabel('Embed posten')
.setStyle(ButtonStyle.Success)
.setEmoji('📤'),
);
- const secondaryRow = new ActionRowBuilder().addComponents(
+ const secondaryRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('eb_main_add_field')
- .setLabel(`Add Field (${state.fields.length}/${MAX_FIELDS})`)
+ .setLabel(`Feld hinzufügen (${state.fields.length}/${MAX_FIELDS})`)
.setStyle(ButtonStyle.Primary)
.setEmoji('➕'),
new ButtonBuilder()
.setCustomId('eb_main_edit_field')
- .setLabel('Edit Field')
+ .setLabel('Feld bearbeiten')
.setStyle(ButtonStyle.Secondary)
.setEmoji('📝')
.setDisabled(state.fields.length === 0),
new ButtonBuilder()
.setCustomId('eb_main_remove_field')
- .setLabel('Remove Field')
+ .setLabel('Feld entfernen')
.setStyle(ButtonStyle.Danger)
.setEmoji('➖')
.setDisabled(state.fields.length === 0),
new ButtonBuilder()
.setCustomId('eb_main_toggle_timestamp')
- .setLabel(state.timestamp ? 'Disable Timestamp' : 'Enable Timestamp')
+ .setLabel(state.timestamp ? 'Zeitstempel ausschalten' : 'Zeitstempel einschalten')
.setStyle(ButtonStyle.Secondary)
.setEmoji('🕐'),
);
- const tertiaryRow = new ActionRowBuilder().addComponents(
+ const tertiaryRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('eb_main_reorder_fields')
- .setLabel('Reorder Fields')
+ .setLabel('Felder sortieren')
.setStyle(ButtonStyle.Secondary)
.setEmoji('↕️')
.setDisabled(state.fields.length < 2),
new ButtonBuilder()
.setCustomId('eb_main_json_export')
- .setLabel('JSON / Raw Data')
+ .setLabel('JSON / Rohdaten')
.setStyle(ButtonStyle.Secondary)
.setEmoji('📋'),
new ButtonBuilder()
.setCustomId('eb_main_reset_all')
- .setLabel('Reset Everything')
+ .setLabel('Alles zurücksetzen')
.setStyle(ButtonStyle.Danger)
.setEmoji('🗑️'),
);
@@ -198,37 +235,37 @@ function buildMainMenu(state) {
return [primaryRow, secondaryRow, tertiaryRow];
}
-async function refreshDashboard(interaction, state) {
+async function refreshDashboard(interaction: ChatInputCommandInteraction, state: EmbedBuilderState) {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [buildPreviewEmbed(state), buildDashboardEmbed(state)],
components: buildMainMenu(state),
});
}
-async function handleEditContent(selectInteraction, rootInteraction, state) {
+async function handleEditContent(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
const modal = new ModalBuilder()
.setCustomId('eb_content')
- .setTitle('Edit Content')
+ .setTitle('Inhalt bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('eb_title')
- .setLabel('Title (max 256 characters)')
+ .setLabel('Titel (max. 256 Zeichen)')
.setStyle(TextInputStyle.Short)
.setValue(state.title || '')
.setMaxLength(256)
.setRequired(false)
- .setPlaceholder('My Embed Title'),
+ .setPlaceholder('Mein Embed-Titel'),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('eb_description')
- .setLabel('Description (max 4000 characters)')
+ .setLabel('Beschreibung (max. 4000 Zeichen)')
.setStyle(TextInputStyle.Paragraph)
.setValue(state.description ? state.description.substring(0, 4000) : '')
.setMaxLength(4000)
.setRequired(false)
- .setPlaceholder('Write your embed description here...'),
+ .setPlaceholder('Schreib hier die Beschreibung deines Embeds …'),
),
);
@@ -252,36 +289,36 @@ async function handleEditContent(selectInteraction, rootInteraction, state) {
await refreshDashboard(rootInteraction, state);
}
-async function handleSetColor(selectInteraction, rootInteraction, state) {
+async function handleSetColor(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
await selectInteraction.deferUpdate().catch(() => {});
const colorSelect = new StringSelectMenuBuilder()
.setCustomId('eb_color_pick')
- .setPlaceholder('Choose a color...')
+ .setPlaceholder('Wähle eine Farbe …')
.addOptions(
COLOR_PRESETS.map(c =>
new StringSelectMenuOptionBuilder()
.setLabel(c.label)
.setValue(c.value)
.setEmoji(c.emoji)
- .setDescription(c.value !== '__custom__' ? c.value : 'Enter your own #RRGGBB value'),
+ .setDescription(c.value !== '__custom__' ? c.value : 'Gib deinen eigenen #RRGGBB-Wert ein'),
),
);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Set Color')
+ .setTitle('Farbe festlegen')
.setDescription(
- 'Select a preset color or choose **Custom Hex** to enter your own `#RRGGBB` value.',
+ 'Wähle eine vordefinierte Farbe oder **Eigener Hex-Wert**, um einen eigenen `#RRGGBB`-Wert einzugeben.',
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(colorSelect)],
+ components: [new ActionRowBuilder().addComponents(colorSelect)],
flags: MessageFlags.Ephemeral,
});
- const colorCollector = rootInteraction.channel.createMessageComponentCollector({
+ const colorCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'eb_color_pick',
@@ -296,12 +333,12 @@ async function handleSetColor(selectInteraction, rootInteraction, state) {
if (picked === '__custom__') {
const hexModal = new ModalBuilder()
.setCustomId('eb_custom_hex')
- .setTitle('Custom Color')
+ .setTitle('Eigene Farbe')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('hex_value')
- .setLabel('Hex Color Code')
+ .setLabel('Hex-Farbcode')
.setStyle(TextInputStyle.Short)
.setPlaceholder('#5865F2')
.setMaxLength(7)
@@ -327,7 +364,7 @@ async function handleSetColor(selectInteraction, rootInteraction, state) {
if (!isValidHex(hex)) {
await replyUserError(hexSubmit, {
type: ErrorTypes.USER_INPUT,
- message: `\`${hex}\` is not a valid hex color. Use the format \`#RRGGBB\` (e.g. \`#5865F2\`).`,
+ message: `\`${hex}\` ist keine gültige Hex-Farbe. Nutze das Format \`#RRGGBB\` (z. B. \`#5865F2\`).`,
});
return;
}
@@ -341,39 +378,39 @@ async function handleSetColor(selectInteraction, rootInteraction, state) {
await refreshDashboard(rootInteraction, state);
} catch (error) {
- logger.warn('Embed builder color picker interaction failed:', error.message);
+ logger.warn('Embed builder color picker interaction failed:', error instanceof Error ? error.message : String(error));
}
});
}
-async function handleSetAuthor(selectInteraction, rootInteraction, state) {
+async function handleSetAuthor(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
const modal = new ModalBuilder()
.setCustomId('eb_author')
- .setTitle('Set Author')
+ .setTitle('Autor festlegen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('author_name')
- .setLabel('Author Name (leave blank to remove)')
+ .setLabel('Autorname (leer lassen zum Entfernen)')
.setStyle(TextInputStyle.Short)
.setValue(state.author?.name || '')
.setMaxLength(256)
.setRequired(false)
- .setPlaceholder('Your Name'),
+ .setPlaceholder('Dein Name'),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('author_icon')
- .setLabel('Author Icon URL (optional)')
+ .setLabel('Autor-Icon-URL (optional)')
.setStyle(TextInputStyle.Short)
.setValue(state.author?.iconUrl || '')
.setRequired(false)
.setPlaceholder('https://example.com/icon.png'),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('author_url')
- .setLabel('Author Link URL (optional)')
+ .setLabel('Autor-Link-URL (optional)')
.setStyle(TextInputStyle.Short)
.setValue(state.author?.url || '')
.setRequired(false)
@@ -400,14 +437,14 @@ async function handleSetAuthor(selectInteraction, rootInteraction, state) {
if (iconUrl && !isValidUrl(iconUrl)) {
await replyUserError(submitted, {
type: ErrorTypes.USER_INPUT,
- message: 'Author icon URL must be a valid `https://` URL.',
+ message: 'Die Autor-Icon-URL muss eine gültige `https://`-URL sein.',
});
return;
}
if (url && !isValidUrl(url)) {
await replyUserError(submitted, {
type: ErrorTypes.USER_INPUT,
- message: 'Author link URL must be a valid `https://` URL.',
+ message: 'Die Autor-Link-URL muss eine gültige `https://`-URL sein.',
});
return;
}
@@ -418,25 +455,25 @@ async function handleSetAuthor(selectInteraction, rootInteraction, state) {
await refreshDashboard(rootInteraction, state);
}
-async function handleSetFooter(selectInteraction, rootInteraction, state) {
+async function handleSetFooter(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
const modal = new ModalBuilder()
.setCustomId('eb_footer')
- .setTitle('Set Footer')
+ .setTitle('Footer festlegen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('footer_text')
- .setLabel('Footer Text (leave blank to remove)')
+ .setLabel('Footer-Text (leer lassen zum Entfernen)')
.setStyle(TextInputStyle.Short)
.setValue(state.footer?.text || '')
.setMaxLength(2048)
.setRequired(false)
- .setPlaceholder('Built with TitanBot'),
+ .setPlaceholder('Erstellt mit Nexo'),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('footer_icon')
- .setLabel('Footer Icon URL (optional)')
+ .setLabel('Footer-Icon-URL (optional)')
.setStyle(TextInputStyle.Short)
.setValue(state.footer?.iconUrl || '')
.setRequired(false)
@@ -462,7 +499,7 @@ async function handleSetFooter(selectInteraction, rootInteraction, state) {
if (iconUrl && !isValidUrl(iconUrl)) {
await replyUserError(submitted, {
type: ErrorTypes.USER_INPUT,
- message: 'Footer icon URL must be a valid `https://` URL.',
+ message: 'Die Footer-Icon-URL muss eine gültige `https://`-URL sein.',
});
return;
}
@@ -473,31 +510,31 @@ async function handleSetFooter(selectInteraction, rootInteraction, state) {
await refreshDashboard(rootInteraction, state);
}
-async function handleSetImages(selectInteraction, rootInteraction, state) {
+async function handleSetImages(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
await selectInteraction.deferUpdate().catch(() => {});
const imageSelect = new StringSelectMenuBuilder()
.setCustomId('eb_image_pick')
- .setPlaceholder('What would you like to change?')
+ .setPlaceholder('Was möchtest du ändern?')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Set Thumbnail')
- .setDescription('Small image displayed in the top-right corner')
+ .setLabel('Vorschaubild setzen')
+ .setDescription('Kleines Bild oben rechts')
.setValue('set_thumbnail')
.setEmoji('🖼️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Set Large Image')
- .setDescription('Full-width banner image at the bottom')
+ .setLabel('Großes Bild setzen')
+ .setDescription('Banner in voller Breite unten')
.setValue('set_image')
.setEmoji('📸'),
new StringSelectMenuOptionBuilder()
- .setLabel('Clear Thumbnail')
- .setDescription('Remove the current thumbnail')
+ .setLabel('Vorschaubild entfernen')
+ .setDescription('Aktuelles Vorschaubild entfernen')
.setValue('clear_thumbnail')
.setEmoji('🗑️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Clear Large Image')
- .setDescription('Remove the current large image')
+ .setLabel('Großes Bild entfernen')
+ .setDescription('Aktuelles großes Bild entfernen')
.setValue('clear_image')
.setEmoji('🗑️'),
);
@@ -505,19 +542,19 @@ async function handleSetImages(selectInteraction, rootInteraction, state) {
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Set Images')
- .setDescription('Choose which image to set or remove.')
+ .setTitle('Bilder festlegen')
+ .setDescription('Wähle, welches Bild du setzen oder entfernen willst.')
.addFields(
- { name: 'Thumbnail', value: state.thumbnail ? `[View](${state.thumbnail})` : '`Not set`', inline: true },
- { name: 'Large Image', value: state.image ? `[View](${state.image})` : '`Not set`', inline: true },
+ { name: 'Vorschaubild', value: state.thumbnail ? `[Ansehen](${state.thumbnail})` : '`Nicht gesetzt`', inline: true },
+ { name: 'Großes Bild', value: state.image ? `[Ansehen](${state.image})` : '`Nicht gesetzt`', inline: true },
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(imageSelect)],
+ components: [new ActionRowBuilder().addComponents(imageSelect)],
flags: MessageFlags.Ephemeral,
});
- const imgMenuCollector = rootInteraction.channel.createMessageComponentCollector({
+ const imgMenuCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'eb_image_pick',
@@ -546,12 +583,12 @@ async function handleSetImages(selectInteraction, rootInteraction, state) {
const urlModal = new ModalBuilder()
.setCustomId('eb_image_url')
- .setTitle(isThumb ? 'Set Thumbnail' : 'Set Large Image')
+ .setTitle(isThumb ? 'Vorschaubild setzen' : 'Großes Bild setzen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('image_url')
- .setLabel('Image URL')
+ .setLabel('Bild-URL')
.setStyle(TextInputStyle.Short)
.setValue(isThumb ? (state.thumbnail || '') : (state.image || ''))
.setRequired(true)
@@ -576,7 +613,7 @@ async function handleSetImages(selectInteraction, rootInteraction, state) {
if (!isValidUrl(url)) {
await replyUserError(submitted, {
type: ErrorTypes.USER_INPUT,
- message: 'Image URL must be a valid `https://` link to a publicly accessible image.',
+ message: 'Die Bild-URL muss ein gültiger `https://`-Link zu einem öffentlich erreichbaren Bild sein.',
});
return;
}
@@ -587,57 +624,57 @@ async function handleSetImages(selectInteraction, rootInteraction, state) {
await submitted.deferUpdate().catch(() => {});
await refreshDashboard(rootInteraction, state);
} catch (error) {
- logger.warn('Embed builder image picker interaction failed:', error.message);
+ logger.warn('Embed builder image picker interaction failed:', error instanceof Error ? error.message : String(error));
}
});
}
-async function handleAddField(selectInteraction, rootInteraction, state) {
+async function handleAddField(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
if (state.fields.length >= MAX_FIELDS) {
await selectInteraction.deferUpdate();
await replyUserError(selectInteraction, {
type: ErrorTypes.VALIDATION,
- message: `Embeds can have a maximum of ${MAX_FIELDS} fields.`,
+ message: `Embeds können maximal ${MAX_FIELDS} Felder haben.`,
});
return;
}
const modal = new ModalBuilder()
.setCustomId('eb_add_field')
- .setTitle('Add Field');
+ .setTitle('Feld hinzufügen');
const fieldNameLabel = new LabelBuilder()
- .setLabel('Field Name (max 256 characters)')
+ .setLabel('Feldname (max. 256 Zeichen)')
.setTextInputComponent(
new TextInputBuilder()
.setCustomId('field_name')
.setStyle(TextInputStyle.Short)
.setMaxLength(256)
.setRequired(true)
- .setPlaceholder('Field Title'),
+ .setPlaceholder('Feldtitel'),
);
const fieldValueLabel = new LabelBuilder()
- .setLabel('Field Value (max 1024 characters)')
+ .setLabel('Feldinhalt (max. 1024 Zeichen)')
.setTextInputComponent(
new TextInputBuilder()
.setCustomId('field_value')
.setStyle(TextInputStyle.Paragraph)
.setMaxLength(1024)
.setRequired(true)
- .setPlaceholder('Field content goes here...'),
+ .setPlaceholder('Hier kommt der Feldinhalt hin …'),
);
const inlineRadio = new RadioGroupBuilder()
.setCustomId('field_inline')
.setRequired(false)
.addOptions([
- { label: 'No — full width', value: 'no' },
- { label: 'Yes — side-by-side', value: 'yes' },
+ { label: 'Nein – volle Breite', value: 'no' },
+ { label: 'Ja – nebeneinander', value: 'yes' },
]);
const inlineLabel = new LabelBuilder()
- .setLabel('Display inline?')
+ .setLabel('Inline anzeigen?')
.setRadioGroupComponent(inlineRadio);
modal.addLabelComponents(fieldNameLabel, fieldValueLabel, inlineLabel);
@@ -664,18 +701,18 @@ async function handleAddField(selectInteraction, rootInteraction, state) {
await refreshDashboard(rootInteraction, state);
}
-async function handleEditField(selectInteraction, rootInteraction, state) {
+async function handleEditField(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
await selectInteraction.deferUpdate();
const pickSelect = new StringSelectMenuBuilder()
.setCustomId('eb_edit_field_pick')
- .setPlaceholder('Select a field to edit...')
+ .setPlaceholder('Wähle ein Feld zum Bearbeiten …')
.addOptions(
state.fields.slice(0, 25).map((f, i) =>
new StringSelectMenuOptionBuilder()
.setLabel(`${i + 1}. ${f.name.substring(0, 50)}`)
.setDescription(
- `${f.value.substring(0, 80)}${f.value.length > 80 ? '…' : ''} · ${f.inline ? 'Inline' : 'Block'}`,
+ `${f.value.substring(0, 80)}${f.value.length > 80 ? '…' : ''} · ${f.inline ? 'Nebeneinander' : 'Volle Breite'}`,
)
.setValue(String(i))
.setEmoji('📝'),
@@ -685,15 +722,15 @@ async function handleEditField(selectInteraction, rootInteraction, state) {
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Edit Field')
- .setDescription('Select the field you want to modify.')
+ .setTitle('Feld bearbeiten')
+ .setDescription('Wähle das Feld, das du ändern willst.')
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(pickSelect)],
+ components: [new ActionRowBuilder().addComponents(pickSelect)],
flags: MessageFlags.Ephemeral,
});
- const pickCollector = rootInteraction.channel.createMessageComponentCollector({
+ const pickCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'eb_edit_field_pick',
@@ -709,10 +746,10 @@ async function handleEditField(selectInteraction, rootInteraction, state) {
const modal = new ModalBuilder()
.setCustomId('eb_edit_field_modal')
- .setTitle(`Edit Field ${idx + 1}`);
+ .setTitle(`Feld ${idx + 1} bearbeiten`);
const editNameLabel = new LabelBuilder()
- .setLabel('Field Name')
+ .setLabel('Feldname')
.setTextInputComponent(
new TextInputBuilder()
.setCustomId('field_name')
@@ -723,7 +760,7 @@ async function handleEditField(selectInteraction, rootInteraction, state) {
);
const editValueLabel = new LabelBuilder()
- .setLabel('Field Value')
+ .setLabel('Feldinhalt')
.setTextInputComponent(
new TextInputBuilder()
.setCustomId('field_value')
@@ -737,19 +774,19 @@ async function handleEditField(selectInteraction, rootInteraction, state) {
.setCustomId('field_inline')
.setRequired(false)
.addOptions([
- { label: 'No — full width', value: 'no' },
- { label: 'Yes — side-by-side', value: 'yes' },
+ { label: 'Nein – volle Breite', value: 'no' },
+ { label: 'Ja – nebeneinander', value: 'yes' },
]);
if (field.inline) {
editInlineRadio.setOptions([
- { label: 'No — full width', value: 'no' },
- { label: 'Yes — side-by-side', value: 'yes', default: true },
+ { label: 'Nein – volle Breite', value: 'no' },
+ { label: 'Ja – nebeneinander', value: 'yes', default: true },
]);
}
const editInlineLabel = new LabelBuilder()
- .setLabel('Display inline?')
+ .setLabel('Inline anzeigen?')
.setRadioGroupComponent(editInlineRadio);
modal.addLabelComponents(editNameLabel, editValueLabel, editInlineLabel);
@@ -776,17 +813,17 @@ async function handleEditField(selectInteraction, rootInteraction, state) {
await submitted.deferUpdate().catch(() => {});
await refreshDashboard(rootInteraction, state);
} catch (error) {
- logger.warn('Embed builder field edit interaction failed:', error.message);
+ logger.warn('Embed builder field edit interaction failed:', error instanceof Error ? error.message : String(error));
}
});
}
-async function handleRemoveField(selectInteraction, rootInteraction, state) {
+async function handleRemoveField(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
await selectInteraction.deferUpdate();
const pickSelect = new StringSelectMenuBuilder()
.setCustomId('eb_remove_field_pick')
- .setPlaceholder('Select a field to remove...')
+ .setPlaceholder('Wähle ein Feld zum Entfernen …')
.addOptions(
state.fields.slice(0, 25).map((f, i) =>
new StringSelectMenuOptionBuilder()
@@ -802,15 +839,15 @@ async function handleRemoveField(selectInteraction, rootInteraction, state) {
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Remove Field')
- .setDescription('Select the field you want to delete.')
+ .setTitle('Feld entfernen')
+ .setDescription('Wähle das Feld, das du löschen willst.')
.setColor(getColor('warning')),
],
- components: [new ActionRowBuilder().addComponents(pickSelect)],
+ components: [new ActionRowBuilder().addComponents(pickSelect)],
flags: MessageFlags.Ephemeral,
});
- const removeCollector = rootInteraction.channel.createMessageComponentCollector({
+ const removeCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'eb_remove_field_pick',
@@ -826,12 +863,12 @@ async function handleRemoveField(selectInteraction, rootInteraction, state) {
});
}
-async function handleReorderFields(selectInteraction, rootInteraction, state) {
+async function handleReorderFields(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
await selectInteraction.deferUpdate();
const pickSelect = new StringSelectMenuBuilder()
.setCustomId('eb_reorder_pick')
- .setPlaceholder('Select a field to move...')
+ .setPlaceholder('Wähle ein Feld zum Verschieben …')
.addOptions(
state.fields.slice(0, 25).map((f, i) =>
new StringSelectMenuOptionBuilder()
@@ -847,15 +884,15 @@ async function handleReorderFields(selectInteraction, rootInteraction, state) {
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Reorder Fields')
- .setDescription('Select a field, then use the arrows to move it up or down.')
+ .setTitle('Felder sortieren')
+ .setDescription('Wähle ein Feld und verschiebe es dann mit den Pfeilen nach oben oder unten.')
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(pickSelect)],
+ components: [new ActionRowBuilder().addComponents(pickSelect)],
flags: MessageFlags.Ephemeral,
});
- const pickCollector = rootInteraction.channel.createMessageComponentCollector({
+ const pickCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'eb_reorder_pick',
@@ -869,37 +906,37 @@ async function handleReorderFields(selectInteraction, rootInteraction, state) {
const upBtn = new ButtonBuilder()
.setCustomId('eb_reorder_up')
- .setLabel('Move Up')
+ .setLabel('Nach oben')
.setStyle(ButtonStyle.Primary)
.setEmoji('⬆️')
.setDisabled(sourceIdx === 0);
const downBtn = new ButtonBuilder()
.setCustomId('eb_reorder_down')
- .setLabel('Move Down')
+ .setLabel('Nach unten')
.setStyle(ButtonStyle.Primary)
.setEmoji('⬇️')
.setDisabled(sourceIdx === state.fields.length - 1);
const cancelBtn = new ButtonBuilder()
.setCustomId('eb_reorder_cancel')
- .setLabel('Cancel')
+ .setLabel('Abbrechen')
.setStyle(ButtonStyle.Secondary);
await pickInter.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Move Field')
+ .setTitle('Feld verschieben')
.setDescription(
- `Moving **${state.fields[sourceIdx].name}** — currently at position **${sourceIdx + 1}** of **${state.fields.length}**.`,
+ `Verschiebe **${state.fields[sourceIdx].name}** – aktuell an Position **${sourceIdx + 1}** von **${state.fields.length}**.`,
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(upBtn, downBtn, cancelBtn)],
+ components: [new ActionRowBuilder().addComponents(upBtn, downBtn, cancelBtn)],
flags: MessageFlags.Ephemeral,
});
- const dirCollector = rootInteraction.channel.createMessageComponentCollector({
+ const dirCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: i =>
i.user.id === selectInteraction.user.id &&
@@ -926,7 +963,7 @@ async function handleReorderFields(selectInteraction, rootInteraction, state) {
});
}
-async function handlePostEmbed(selectInteraction, rootInteraction, state, guild) {
+async function handlePostEmbed(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState, guild: Guild) {
if (
!state.title &&
!state.description &&
@@ -936,7 +973,7 @@ async function handlePostEmbed(selectInteraction, rootInteraction, state, guild)
await selectInteraction.deferUpdate();
await replyUserError(selectInteraction, {
type: ErrorTypes.VALIDATION,
- message: 'Add at least a title, description, or field before posting.',
+ message: 'Füge vor dem Posten mindestens einen Titel, eine Beschreibung oder ein Feld hinzu.',
});
return;
}
@@ -945,21 +982,21 @@ async function handlePostEmbed(selectInteraction, rootInteraction, state, guild)
const chanSelect = new ChannelSelectMenuBuilder()
.setCustomId('eb_post_channel')
- .setPlaceholder('Select a channel...')
+ .setPlaceholder('Wähle einen Kanal …')
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Post Embed')
- .setDescription('Select the channel where this embed will be sent.')
+ .setTitle('Embed posten')
+ .setDescription('Wähle den Kanal, in den das Embed gesendet werden soll.')
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(chanSelect)],
+ components: [new ActionRowBuilder().addComponents(chanSelect)],
flags: MessageFlags.Ephemeral,
});
- const chanCollector = rootInteraction.channel.createMessageComponentCollector({
+ const chanCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'eb_post_channel',
@@ -969,41 +1006,41 @@ async function handlePostEmbed(selectInteraction, rootInteraction, state, guild)
chanCollector.on('collect', async chanInter => {
await chanInter.deferUpdate();
- const channel = chanInter.channels.first();
+ const channel = chanInter.channels.first() as TextChannel | NewsChannel | undefined;
if (!channel) {
await replyUserError(chanInter, {
type: ErrorTypes.USER_INPUT,
- message: 'Could not resolve the selected channel.',
+ message: 'Der ausgewählte Kanal konnte nicht gefunden werden.',
});
return;
}
- const perms = channel.permissionsFor(guild.members.me);
+ const perms = channel.permissionsFor(guild.members.me!);
if (!perms?.has([PermissionFlagsBits.SendMessages, PermissionFlagsBits.EmbedLinks])) {
await replyUserError(chanInter, {
type: ErrorTypes.PERMISSION,
- message: `I need **Send Messages** and **Embed Links** permissions in ${channel} to post there.`,
+ message: `Ich brauche in ${channel} die Berechtigungen **Nachrichten senden** und **Links einbetten**, um dort zu posten.`,
});
return;
}
const finalEmbed = buildPreviewEmbed(state);
- if (finalEmbed.data.description === '*(Empty — use the menu below to add content)*') {
+ if (finalEmbed.data.description === '*(Leer – nutze das Menü unten, um Inhalte hinzuzufügen)*') {
finalEmbed.setDescription(null);
}
await channel.send({ embeds: [finalEmbed] });
await chanInter.followUp({
- embeds: [successEmbed('Embed Sent', `Your embed has been posted to ${channel}.`)],
+ embeds: [successEmbed('Embed gesendet', `Dein Embed wurde in ${channel} gepostet.`)],
flags: MessageFlags.Ephemeral,
});
});
}
-async function handleJsonExport(selectInteraction, rootInteraction, state) {
+async function handleJsonExport(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, state: EmbedBuilderState) {
await selectInteraction.deferUpdate();
const previewEmbed = buildPreviewEmbed(state);
@@ -1024,7 +1061,7 @@ async function handleJsonExport(selectInteraction, rootInteraction, state) {
embeds: [
new EmbedBuilder()
.setTitle('Embed JSON')
- .setDescription('The JSON is too long to display inline — see the attached file.')
+ .setDescription('Das JSON ist zu lang für die Anzeige – sieh dir die angehängte Datei an.')
.setColor(getColor('info')),
],
files: [
@@ -1042,7 +1079,7 @@ export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName('embedbuilder')
- .setDescription('Build and post a fully custom embed with live preview')
+ .setDescription('Erstelle und poste ein komplett eigenes Embed mit Live-Vorschau')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
async execute(interaction) {
@@ -1052,9 +1089,9 @@ export default {
});
if (!deferSuccess) return;
- const guild = interaction.guild;
+ const guild = interaction.guild!;
- const state = {
+ const state: EmbedBuilderState = {
title: null,
description: null,
color: getColor('primary'),
@@ -1068,7 +1105,7 @@ export default {
await refreshDashboard(interaction, state);
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: i =>
i.user.id === interaction.user.id && i.customId.startsWith('eb_main_'),
@@ -1129,9 +1166,9 @@ export default {
} catch (error) {
logger.error('Error in embedbuilder collector:', error);
const msg =
- error instanceof TitanBotError
- ? error.userMessage || 'An error occurred.'
- : 'An unexpected error occurred.';
+ error instanceof NexoBotError
+ ? error.userMessage || 'Ein Fehler ist aufgetreten.'
+ : 'Ein unerwarteter Fehler ist aufgetreten.';
if (!ci.replied && !ci.deferred) await ci.deferUpdate().catch(() => {});
await replyUserError(ci, {
type: ErrorTypes.UNKNOWN,
@@ -1146,13 +1183,13 @@ export default {
}
});
} catch (error) {
- if (error instanceof TitanBotError) throw error;
+ if (error instanceof NexoBotError) throw error;
logger.error('Unexpected error in embedbuilder:', error);
- throw new TitanBotError(
- `embedbuilder failed: ${error.message}`,
+ throw new NexoBotError(
+ `embedbuilder failed: ${error instanceof Error ? error.message : String(error)}`,
ErrorTypes.UNKNOWN,
- 'Failed to open the embed builder.',
+ 'Der Embed-Builder konnte nicht geöffnet werden.',
);
}
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/generatepassword.js b/src/commands/Tools/modules/generatepassword.ts
similarity index 78%
rename from src/commands/Tools/generatepassword.js
rename to src/commands/Tools/modules/generatepassword.ts
index 51628bfed0..54d54c2bb4 100644
--- a/src/commands/Tools/generatepassword.js
+++ b/src/commands/Tools/modules/generatepassword.ts
@@ -1,31 +1,32 @@
import { webcrypto as crypto } from 'node:crypto';
-import { getColor } from '../../config/bot.js';
+import { getColor } from '../../../config/bot.js';
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('generatepassword')
- .setDescription('Generate a strong, random password')
+ .setDescription('Erzeugt ein starkes, zufälliges Passwort')
.addIntegerOption(option =>
option.setName('length')
- .setDescription('Password length (default: 16, max: 50)')
+ .setDescription('Passwortlänge (Standard: 16, max.: 50)')
.setMinValue(8)
.setMaxValue(50)
.setRequired(false))
.addBooleanOption(option =>
option.setName('uppercase')
- .setDescription('Include uppercase letters (A-Z)')
+ .setDescription('Großbuchstaben verwenden (A-Z)')
.setRequired(false))
.addBooleanOption(option =>
option.setName('numbers')
- .setDescription('Include numbers (0-9)')
+ .setDescription('Zahlen verwenden (0-9)')
.setRequired(false))
.addBooleanOption(option =>
option.setName('symbols')
- .setDescription('Include symbols (!@#$%^&*)')
+ .setDescription('Sonderzeichen verwenden (!@#$%^&*)')
.setRequired(false)),
async execute(interaction) {
@@ -48,7 +49,7 @@ export default {
const includeSymbols = interaction.options.getBoolean('symbols') ?? true;
if (length < 8 || length > 50) {
- await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: `Password must be 8-50 characters. You provided: ${length}` });
+ await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: `Das Passwort muss 8-50 Zeichen lang sein. Deine Angabe: ${length}` });
return;
}
@@ -89,7 +90,7 @@ export default {
password = password.substring(0, randomIndex) + randomSymbol + password.substring(randomIndex + 1);
}
- let strength = 'Weak';
+ let strength = 'Schwach';
let strengthEmoji = '🔴';
let strengthColor = getColor('error');
@@ -114,33 +115,33 @@ export default {
if (hasSymbol) score *= 1.3;
if (score > 80) {
- strength = 'Very Strong';
+ strength = 'Sehr stark';
strengthEmoji = '🟢';
strengthColor = getColor('success');
} else if (score > 60) {
- strength = 'Strong';
+ strength = 'Stark';
strengthEmoji = '🟢';
strengthColor = getColor('success');
} else if (score > 40) {
- strength = 'Good';
+ strength = 'Gut';
strengthEmoji = '🟡';
strengthColor = getColor('warning');
} else if (score > 20) {
- strength = 'Weak';
+ strength = 'Schwach';
strengthEmoji = '🟠';
strengthColor = getColor('warning');
}
const embed = successEmbed(
- '🔑 Generated Password',
- `**Password:** ||\`${password}\`||\n` +
- `**Length:** ${password.length} characters\n` +
- `**Strength:** ${strengthEmoji} ${strength}\n` +
- `**Contains:** ${hasLower ? 'Lowercase' : ''}${hasUpper ? ', Uppercase' : ''}${hasNumber ? ', Numbers' : ''}${hasSymbol ? ', Symbols' : ''}`
+ '🔑 Generiertes Passwort',
+ `**Passwort:** ||\`${password}\`||\n` +
+ `**Länge:** ${password.length} Zeichen\n` +
+ `**Stärke:** ${strengthEmoji} ${strength}\n` +
+ `**Enthält:** ${hasLower ? 'Kleinbuchstaben' : ''}${hasUpper ? ', Großbuchstaben' : ''}${hasNumber ? ', Zahlen' : ''}${hasSymbol ? ', Sonderzeichen' : ''}`
).setColor(strengthColor);
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed],
});
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/hexcolor.js b/src/commands/Tools/modules/hexcolor.ts
similarity index 62%
rename from src/commands/Tools/hexcolor.js
rename to src/commands/Tools/modules/hexcolor.ts
index 36736e3a6e..ea6a854236 100644
--- a/src/commands/Tools/hexcolor.js
+++ b/src/commands/Tools/modules/hexcolor.ts
@@ -1,17 +1,18 @@
-import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { getColor } from '../../config/bot.js';
-
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
+import { SlashCommandBuilder, MessageFlags, type HexColorString } from 'discord.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { getColor } from '../../../config/bot.js';
+
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('hexcolor')
- .setDescription('Generate a random hex color with preview')
+ .setDescription('Erzeugt eine zufällige Hex-Farbe mit Vorschau')
.addStringOption(option =>
option.setName('color')
- .setDescription('Specific hex color (e.g., #FF5733 or FF5733)')
+ .setDescription('Bestimmte Hex-Farbe (z. B. #FF5733 oder FF5733)')
.setRequired(false)),
async execute(interaction) {
@@ -27,7 +28,7 @@ export default {
} else {
hexColor = hexColor.replace('#', '');
if (!/^[0-9A-Fa-f]{3,6}$/.test(hexColor)) {
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please provide a valid hex code.\n\n**Valid formats:**\n• `#FF5733` (with hash)\n• `FF5733` (without hash)\n• `F57` (3-digit shorthand)\n\n**Invalid:** `#GG5733` (G is not a hex digit)' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte gib einen gültigen Hex-Code an.\n\n**Gültige Formate:**\n• `#FF5733` (mit Raute)\n• `FF5733` (ohne Raute)\n• `F57` (3-stellige Kurzform)\n\n**Ungültig:** `#GG5733` (G ist keine Hex-Ziffer)' });
}
if (hexColor.length === 3) {
@@ -49,34 +50,34 @@ export default {
const colorName = getColorName(hexColor);
const embed = successEmbed(
- '🎨 Color Information',
+ '🎨 Farbinfo',
`**Hex:** \`${hexColor}\`\n` +
`**RGB:** \`rgb(${r}, ${g}, ${b})\`\n` +
`**HSL:** \`${rgbToHsl(r, g, b)}\`\n` +
- `**Name:** ${colorName || 'Custom Color'}`
+ `**Name:** ${colorName || 'Eigene Farbe'}`
)
- .setColor(hexColor)
+ .setColor(hexColor as HexColorString)
.setImage(colorPreviewUrl);
if (isRandom) {
- embed.setFooter({ text: 'Randomly generated color' });
+ embed.setFooter({ text: 'Zufällig erzeugte Farbe' });
}
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
},
- 'Failed to generate color information. Please try again.',
+ 'Die Farbinfo konnte nicht erzeugt werden. Versuch es noch mal.',
{
autoDefer: true,
deferOptions: { flags: MessageFlags.Ephemeral }
}
);
},
-};
+} satisfies Command;
-function rgbToHsl(r, g, b) {
+function rgbToHsl(r: number, g: number, b: number): string {
r /= 255, g /= 255, b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
- let h, s, l = (max + min) / 2;
+ let h: number | undefined, s: number, l = (max + min) / 2;
if (max === min) {
h = s = 0;
@@ -88,34 +89,34 @@ h = s = 0;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
- h /= 6;
+ h! /= 6;
}
- return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
+ return `hsl(${Math.round(h! * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
}
-function getColorName(hex) {
- const colors = {
- '#FF0000': 'Red',
- '#00FF00': 'Green',
- '#0000FF': 'Blue',
- '#FFFF00': 'Yellow',
+function getColorName(hex: string): string | null {
+ const colors: Record = {
+ '#FF0000': 'Rot',
+ '#00FF00': 'Grün',
+ '#0000FF': 'Blau',
+ '#FFFF00': 'Gelb',
'#FF00FF': 'Magenta',
'#00FFFF': 'Cyan',
- '#000000': 'Black',
- '#FFFFFF': 'White',
- '#808080': 'Gray',
+ '#000000': 'Schwarz',
+ '#FFFFFF': 'Weiß',
+ '#808080': 'Grau',
'#FFA500': 'Orange',
- '#800080': 'Purple',
- '#A52A2A': 'Brown',
- '#FFC0CB': 'Pink',
- '#008000': 'Dark Green',
- '#000080': 'Navy',
+ '#800080': 'Lila',
+ '#A52A2A': 'Braun',
+ '#FFC0CB': 'Rosa',
+ '#008000': 'Dunkelgrün',
+ '#000080': 'Marineblau',
'#FFD700': 'Gold',
- '#C0C0C0': 'Silver',
- '#FF6347': 'Tomato',
- '#40E0D0': 'Turquoise',
- '#E6E6FA': 'Lavender'
+ '#C0C0C0': 'Silber',
+ '#FF6347': 'Tomatenrot',
+ '#40E0D0': 'Türkis',
+ '#E6E6FA': 'Lavendel'
};
if (colors[hex.toUpperCase()]) {
@@ -136,5 +137,5 @@ function getColorName(hex) {
}
}
- return minDistance < 1000000 ? `Close to ${closestColor}` : null;
+ return minDistance < 1000000 ? `Ähnlich wie ${closestColor}` : null;
}
\ No newline at end of file
diff --git a/src/commands/Tools/poll.js b/src/commands/Tools/modules/poll.ts
similarity index 62%
rename from src/commands/Tools/poll.js
rename to src/commands/Tools/modules/poll.ts
index 772f1500c5..ff402af22a 100644
--- a/src/commands/Tools/poll.js
+++ b/src/commands/Tools/modules/poll.ts
@@ -1,61 +1,63 @@
-import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { getColor } from '../../config/bot.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { SlashCommandBuilder, MessageFlags, type GuildTextBasedChannel } from 'discord.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { getColor } from '../../../config/bot.js';
+import { createError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
const EMOJIS = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟'];
const MAX_OPTIONS = 10;
export default {
data: new SlashCommandBuilder()
.setName('poll')
- .setDescription('Create a simple poll with up to 10 options')
+ .setDescription('Erstelle eine einfache Umfrage mit bis zu 10 Optionen')
.addStringOption(option =>
option.setName('question')
- .setDescription('The poll question')
+ .setDescription('Die Frage der Umfrage')
.setRequired(true))
.addStringOption(option =>
option.setName('option1')
- .setDescription('First option')
+ .setDescription('Erste Option')
.setRequired(true))
.addStringOption(option =>
option.setName('option2')
- .setDescription('Second option')
+ .setDescription('Zweite Option')
.setRequired(true))
.addStringOption(option =>
option.setName('option3')
- .setDescription('Third option (optional)')
+ .setDescription('Dritte Option (optional)')
.setRequired(false))
.addStringOption(option =>
option.setName('option4')
- .setDescription('Fourth option (optional)')
+ .setDescription('Vierte Option (optional)')
.setRequired(false))
.addStringOption(option =>
option.setName('option5')
- .setDescription('Fifth option (optional)')
+ .setDescription('Fünfte Option (optional)')
.setRequired(false))
.addStringOption(option =>
option.setName('option6')
- .setDescription('Sixth option (optional)')
+ .setDescription('Sechste Option (optional)')
.setRequired(false))
.addStringOption(option =>
option.setName('option7')
- .setDescription('Seventh option (optional)')
+ .setDescription('Siebte Option (optional)')
.setRequired(false))
.addStringOption(option =>
option.setName('option8')
- .setDescription('Eighth option (optional)')
+ .setDescription('Achte Option (optional)')
.setRequired(false))
.addStringOption(option =>
option.setName('option9')
- .setDescription('Ninth option (optional)')
+ .setDescription('Neunte Option (optional)')
.setRequired(false))
.addStringOption(option =>
option.setName('option10')
- .setDescription('Tenth option (optional)')
+ .setDescription('Zehnte Option (optional)')
.setRequired(false))
.addBooleanOption(option =>
option.setName('anonymous')
- .setDescription('Make the poll anonymous (default: false)')
+ .setDescription('Umfrage anonym machen (Standard: nein)')
.setRequired(false)),
async execute(interaction) {
@@ -79,7 +81,7 @@ export default {
}
if (options.length < 2) {
- throw new Error("You must provide at least 2 options for the poll.");
+ throw createError("You must provide at least 2 options for the poll.", ErrorTypes.VALIDATION, "Gib mindestens 2 Optionen für die Umfrage an.", { expected: true });
}
let description = `**${question}**\n\n`;
@@ -88,17 +90,17 @@ export default {
});
if (isAnonymous) {
- description += '\n*This is an anonymous poll. Votes are not tracked to users.*';
+ description += '\n*Das ist eine anonyme Umfrage. Stimmen werden keinen Nutzern zugeordnet.*';
} else {
- description += '\n*React with the emoji to vote!*';
+ description += '\n*Reagiere mit dem Emoji, um abzustimmen!*';
}
const embed = successEmbed(
- `📊 ${isAnonymous ? 'Anonymous ' : ''}Poll`,
+ `📊 ${isAnonymous ? 'Anonyme ' : ''}Umfrage`,
description
);
- const message = await interaction.channel.send({ embeds: [embed] });
+ const message = await (interaction.channel as GuildTextBasedChannel).send({ embeds: [embed] });
for (let i = 0; i < options.length; i++) {
await message.react(EMOJIS[i]);
@@ -106,7 +108,7 @@ export default {
}
await InteractionHelper.safeEditReply(interaction, {
- content: '✅ Poll created successfully!',
+ content: '✅ Umfrage erstellt!',
});
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/modules/randomuser.ts
similarity index 61%
rename from src/commands/Tools/randomuser.js
rename to src/commands/Tools/modules/randomuser.ts
index 4dc15a6fd1..750f39adb5 100644
--- a/src/commands/Tools/randomuser.js
+++ b/src/commands/Tools/modules/randomuser.ts
@@ -1,29 +1,30 @@
-import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { getColor } from '../../config/bot.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, type ColorResolvable, type MessageComponentInteraction } from 'discord.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { getColor } from '../../../config/bot.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('randomuser')
- .setDescription('Select a random user from the server')
+ .setDescription('Wählt einen zufälligen Nutzer vom Server aus')
.addRoleOption(option =>
option.setName('role')
- .setDescription('Limit selection to users with this role')
+ .setDescription('Nur Nutzer mit dieser Rolle berücksichtigen')
.setRequired(false))
.addBooleanOption(option =>
option.setName('bots')
- .setDescription('Include bots in the selection (default: false)')
+ .setDescription('Bots einbeziehen (Standard: nein)')
.setRequired(false))
.addBooleanOption(option =>
option.setName('online')
- .setDescription('Only select from online users (default: false)')
+ .setDescription('Nur Nutzer, die online sind (Standard: nein)')
.setRequired(false))
.addBooleanOption(option =>
option.setName('mention')
- .setDescription('Mention the selected user (default: false)')
+ .setDescription('Ausgewählten Nutzer erwähnen (Standard: nein)')
.setRequired(false)),
async execute(interaction) {
@@ -40,7 +41,7 @@ export default {
if (!interaction.guild) {
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'This command can only be used in a server/guild.',
+ message: 'Dieser Command kann nur auf einem Server genutzt werden.',
});
}
@@ -66,14 +67,14 @@ export default {
}
if (memberArray.length === 0) {
- let errorMessage = 'Could not find any users matching your filters:';
- if (role) errorMessage = `No users have the **${role.name}** role.`;
- if (onlineOnly) errorMessage = 'No users are currently online.';
- if (role && onlineOnly) errorMessage = `No **${role.name}** members are online.`;
+ let errorMessage = 'Es wurden keine Nutzer gefunden, die zu deinen Filtern passen.';
+ if (role) errorMessage = `Kein Nutzer hat die Rolle **${role.name}**.`;
+ if (onlineOnly) errorMessage = 'Gerade ist kein Nutzer online.';
+ if (role && onlineOnly) errorMessage = `Kein Mitglied mit der Rolle **${role.name}** ist online.`;
return replyUserError(interaction, {
type: ErrorTypes.USER_INPUT,
- message: errorMessage + '\n\nTry adjusting your filters.',
+ message: errorMessage + '\n\nVersuch, deine Filter anzupassen.',
});
}
@@ -83,44 +84,45 @@ export default {
const user = selectedMember.user;
const joinDate = selectedMember.joinedAt;
const roles = selectedMember.roles.cache
- .filter(role => role.id !== interaction.guild.id)
+ .filter(role => role.id !== interaction.guild!.id)
.sort((a, b) => b.position - a.position)
.map(role => role.toString())
.slice(0, 10);
const embed = successEmbed(
- '🎲 Random User Selected',
+ '🎲 Zufälliger Nutzer ausgewählt',
shouldMention ? `${selectedMember}` : `**${user.username}**`
)
- .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 }))
+ .setThumbnail(user.displayAvatarURL({ size: 256 }))
.addFields(
- { name: 'Username', value: user.username, inline: true },
- { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true },
- { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false }
+ { name: 'Nutzername', value: user.username, inline: true },
+ { name: 'Bot', value: user.bot ? 'Ja' : 'Nein', inline: true },
+ { name: `Rollen (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} weitere` : '') : 'Keine Rollen', inline: false }
)
- .setColor('primary');
+ // NOTE: 'primary' is not a valid ColorResolvable (discord.js throws ColorConvert); kept as-is from JS
+ .setColor('primary' as ColorResolvable);
- const row = new ActionRowBuilder()
+ const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`randomuser_${interaction.user.id}_again`)
- .setLabel('🎲 Pick Another User')
+ .setLabel('🎲 Anderen Nutzer wählen')
.setStyle(ButtonStyle.Primary)
);
const response = await interaction.editReply({
- content: shouldMention ? `${selectedMember}, you've been chosen!` : null,
+ content: shouldMention ? `${selectedMember}, du wurdest ausgewählt!` : null,
embeds: [embed],
components: [row],
allowedMentions: { users: shouldMention ? [user.id] : [] }
});
- const filter = (i) => i.customId === `randomuser_${interaction.user.id}_again` && i.user.id === interaction.user.id;
+ const filter = (i: MessageComponentInteraction) => i.customId === `randomuser_${interaction.user.id}_again` && i.user.id === interaction.user.id;
const collector = response.createMessageComponentCollector({ filter, time: 300000 });
collector.on('collect', async (i) => {
try {
- let newMembers = interaction.guild.members.cache.filter(member => {
+ let newMembers = interaction.guild!.members.cache.filter(member => {
if (member.user.bot && !includeBots) return false;
if (onlineOnly && member.presence?.status === 'offline') return false;
@@ -139,7 +141,7 @@ export default {
if (newMemberArray.length === 0) {
await replyUserError(i, {
type: ErrorTypes.USER_INPUT,
- message: 'No users found matching the criteria.',
+ message: 'Keine Nutzer gefunden, die zu den Kriterien passen.',
});
return;
}
@@ -149,25 +151,25 @@ export default {
const newUser = newSelectedMember.user;
const newRoles = newSelectedMember.roles.cache
- .filter(r => r.id !== interaction.guild.id)
+ .filter(r => r.id !== interaction.guild!.id)
.sort((a, b) => b.position - a.position)
.map(r => r.toString())
.slice(0, 10);
const newEmbed = successEmbed(
- '🎲 Random User Selected',
+ '🎲 Zufälliger Nutzer ausgewählt',
shouldMention ? `${newSelectedMember}` : `**${newUser.username}**`
)
- .setThumbnail(newUser.displayAvatarURL({ dynamic: true, size: 256 }))
+ .setThumbnail(newUser.displayAvatarURL({ size: 256 }))
.addFields(
- { name: 'Username', value: newUser.username, inline: true },
- { name: 'Bot', value: newUser.bot ? 'Yes' : 'No', inline: true },
- { name: `Roles (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} more` : '') : 'No roles', inline: false }
+ { name: 'Nutzername', value: newUser.username, inline: true },
+ { name: 'Bot', value: newUser.bot ? 'Ja' : 'Nein', inline: true },
+ { name: `Rollen (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} weitere` : '') : 'Keine Rollen', inline: false }
)
.setColor(newSelectedMember.displayHexColor || '#3498db');
await i.update({
- content: shouldMention ? `${newSelectedMember}, you've been chosen!` : null,
+ content: shouldMention ? `${newSelectedMember}, du wurdest ausgewählt!` : null,
embeds: [newEmbed],
components: [row],
allowedMentions: { users: shouldMention ? [newUser.id] : [] }
@@ -176,18 +178,18 @@ export default {
} catch (error) {
logger.error('Button interaction error:', error);
await i.reply({
- content: 'An error occurred while selecting another user.',
+ content: 'Beim Auswählen eines anderen Nutzers ist ein Fehler aufgetreten.',
flags: ['Ephemeral']
});
}
});
collector.on('end', () => {
- const disabledRow = ActionRowBuilder.from(row).setComponents(
+ const disabledRow = ActionRowBuilder.from(row).setComponents(
ButtonBuilder.from(row.components[0]).setDisabled(true)
);
interaction.editReply({ components: [disabledRow] }).catch(console.error);
});
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/shorten.js b/src/commands/Tools/modules/shorten.ts
similarity index 65%
rename from src/commands/Tools/shorten.js
rename to src/commands/Tools/modules/shorten.ts
index 4b5b9af5f5..b1603dcaf1 100644
--- a/src/commands/Tools/shorten.js
+++ b/src/commands/Tools/modules/shorten.ts
@@ -1,24 +1,25 @@
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { getColor } from '../../config/bot.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { getColor } from '../../../config/bot.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName("shorten")
- .setDescription("Shorten a URL using is.gd")
+ .setDescription("Kürze eine URL mit is.gd")
.addStringOption(option =>
option
.setName("url")
- .setDescription("The URL to shorten")
+ .setDescription("Die URL, die gekürzt werden soll")
.setRequired(true)
)
.addStringOption(option =>
option
.setName("custom")
- .setDescription("Custom URL ending (optional)")
+ .setDescription("Eigenes URL-Ende (optional)")
.setRequired(false)
)
.setDMPermission(false),
@@ -37,7 +38,7 @@ export default {
return;
}
- const url = interaction.options.getString("url");
+ const url = interaction.options.getString("url")!;
const custom = interaction.options.getString("custom");
try {
@@ -45,14 +46,14 @@ export default {
} catch (e) {
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'Invalid URL format. Include http:// or https://',
+ message: 'Ungültiges URL-Format. Gib http:// oder https:// mit an.',
});
}
if (custom && !/^[a-zA-Z0-9_-]+$/.test(custom)) {
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'Custom URL can only contain letters, numbers, underscores, and hyphens.',
+ message: 'Das URL-Ende darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.',
});
}
@@ -69,13 +70,13 @@ export default {
response = await fetch(apiUrl, {
signal: controller.signal,
headers: {
- 'User-Agent': 'TitanBot URL Shortener/1.0'
+ 'User-Agent': 'NexoBot URL Shortener/1.0'
}
});
} catch (networkError) {
- const message = networkError?.name === 'AbortError'
- ? 'The URL shortener timed out. Please try again in a moment.'
- : 'Unable to reach the URL shortener service right now. Please try again later.';
+ const message = (networkError as { name?: string } | null | undefined)?.name === 'AbortError'
+ ? 'Der URL-Kürzer hat nicht rechtzeitig geantwortet. Versuch es gleich noch mal.'
+ : 'Der URL-Kürzer ist gerade nicht erreichbar. Versuch es später noch mal.';
return replyUserError(interaction, {
type: ErrorTypes.NETWORK,
message,
@@ -87,7 +88,7 @@ export default {
if (!response.ok) {
return replyUserError(interaction, {
type: ErrorTypes.UNKNOWN,
- message: `Shortener service returned HTTP ${response.status}. Please try again later.`,
+ message: `Der URL-Kürzer hat HTTP ${response.status} zurückgegeben. Versuch es später noch mal.`,
});
}
@@ -99,24 +100,24 @@ export default {
if (shortUrl.includes("already exists")) {
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'That custom URL is already taken. Try a different one.',
+ message: 'Dieses URL-Ende ist schon vergeben. Probier ein anderes.',
});
} else if (shortUrl.includes("invalid")) {
return replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'Invalid URL. Include http:// or https://',
+ message: 'Ungültige URL. Gib http:// oder https:// mit an.',
});
}
return replyUserError(interaction, {
type: ErrorTypes.UNKNOWN,
- message: `URL shortening failed: ${shortUrl}`,
+ message: `URL konnte nicht gekürzt werden: ${shortUrl}`,
});
}
- const embed = successEmbed('URL Shortened', `Here's your shortened URL: ${shortUrl}`);
+ const embed = successEmbed('URL gekürzt', `Hier ist deine gekürzte URL: ${shortUrl}`);
embed.setColor(getColor('success'));
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed],
});
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/time.js b/src/commands/Tools/modules/time.ts
similarity index 64%
rename from src/commands/Tools/time.js
rename to src/commands/Tools/modules/time.ts
index d26db76f23..100c8c0346 100644
--- a/src/commands/Tools/time.js
+++ b/src/commands/Tools/modules/time.ts
@@ -1,15 +1,16 @@
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('time')
- .setDescription('Get the current time in different timezones')
+ .setDescription('Zeigt die aktuelle Uhrzeit in verschiedenen Zeitzonen')
.addStringOption(option =>
option.setName('timezone')
- .setDescription('The timezone to display (e.g., UTC, America/New_York)')
+ .setDescription('Die anzuzeigende Zeitzone (z. B. UTC, Europe/Berlin)')
.setRequired(false)),
async execute(interaction) {
@@ -20,7 +21,7 @@ export default {
let timeString;
try {
- timeString = new Date().toLocaleString('en-US', {
+ timeString = new Date().toLocaleString('de-DE', {
timeZone: timezone,
weekday: 'long',
year: 'numeric',
@@ -35,7 +36,7 @@ export default {
logger.warn(`Invalid timezone requested: ${timezone}`);
await replyUserError(interaction, {
type: ErrorTypes.VALIDATION,
- message: 'Invalid timezone. Please use a valid timezone identifier (e.g., UTC, America/New_York, Europe/London)',
+ message: 'Ungültige Zeitzone. Bitte nutze eine gültige Zeitzonen-ID (z. B. UTC, Europe/Berlin, America/New_York)',
});
return;
}
@@ -44,19 +45,19 @@ export default {
const unixTimestamp = Math.floor(now.getTime() / 1000);
const embed = successEmbed(
- '🕒 Current Time',
+ '🕒 Aktuelle Uhrzeit',
`**${timezone}:** ${timeString}\n` +
- `**Unix Timestamp:** \`${unixTimestamp}\`\n` +
- `**ISO String:** \`${now.toISOString()}\``
+ `**Unix-Zeitstempel:** \`${unixTimestamp}\`\n` +
+ `**ISO-String:** \`${now.toISOString()}\``
);
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
},
- 'Failed to get current time. Please try again.',
+ 'Die aktuelle Uhrzeit konnte nicht abgerufen werden. Versuch es noch mal.',
{
autoDefer: true,
deferOptions: { flags: MessageFlags.Ephemeral }
}
);
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/unixtime.js b/src/commands/Tools/modules/unixtime.ts
similarity index 51%
rename from src/commands/Tools/unixtime.js
rename to src/commands/Tools/modules/unixtime.ts
index 7e8e7e7b91..fd17fa310a 100644
--- a/src/commands/Tools/unixtime.js
+++ b/src/commands/Tools/modules/unixtime.ts
@@ -1,12 +1,13 @@
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { getColor } from '../../config/bot.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { getColor } from '../../../config/bot.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('unixtime')
- .setDescription('Get the current Unix timestamp'),
+ .setDescription('Zeigt den aktuellen Unix-Zeitstempel'),
async execute(interaction) {
await InteractionHelper.safeExecute(
@@ -16,11 +17,11 @@ export default {
const unixTimestamp = Math.floor(now.getTime() / 1000);
const embed = successEmbed(
- '⏱️ Current Unix Timestamp',
- `**Seconds since Unix Epoch:** \`${unixTimestamp}\`\n` +
- `**Milliseconds since Unix Epoch:** \`${now.getTime()}\`\n\n` +
- `**Human-readable (UTC):** ${now.toUTCString()}\n` +
- `**ISO String:** ${now.toISOString()}`
+ '⏱️ Aktueller Unix-Zeitstempel',
+ `**Sekunden seit Unix-Epoche:** \`${unixTimestamp}\`\n` +
+ `**Millisekunden seit Unix-Epoche:** \`${now.getTime()}\`\n\n` +
+ `**Lesbar (UTC):** ${now.toUTCString()}\n` +
+ `**ISO-String:** ${now.toISOString()}`
);
embed.setColor(getColor('success'));
@@ -28,11 +29,11 @@ export default {
embeds: [embed],
});
},
- 'Failed to get unix timestamp. Please try again.',
+ 'Der Unix-Zeitstempel konnte nicht abgerufen werden. Versuch es noch mal.',
{
autoDefer: true,
deferOptions: { flags: MessageFlags.Ephemeral }
}
);
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Tools/tools.ts b/src/commands/Tools/tools.ts
new file mode 100644
index 0000000000..91432ba392
--- /dev/null
+++ b/src/commands/Tools/tools.ts
@@ -0,0 +1,33 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import baseconvertCommand from './modules/baseconvert.js';
+import calculateCommand from './modules/calculate.js';
+import countdownCommand from './modules/countdown.js';
+import generatepasswordCommand from './modules/generatepassword.js';
+import hexcolorCommand from './modules/hexcolor.js';
+import pollCommand from './modules/poll.js';
+import randomuserCommand from './modules/randomuser.js';
+import shortenCommand from './modules/shorten.js';
+import timeCommand from './modules/time.js';
+import unixtimeCommand from './modules/unixtime.js';
+import weatherCommand from '../Utility/modules/weather.js';
+import firstmsgCommand from '../Utility/modules/firstmsg.js';
+
+export default createRouterCommand({
+ name: 'tools',
+ description: 'Praktische Tools und Helfer',
+ category: 'Tools',
+ routes: [
+ { legacyName: 'baseconvert', module: baseconvertCommand },
+ { legacyName: 'calculate', module: calculateCommand },
+ { legacyName: 'countdown', module: countdownCommand },
+ { legacyName: 'generatepassword', module: generatepasswordCommand },
+ { legacyName: 'hexcolor', module: hexcolorCommand },
+ { legacyName: 'poll', module: pollCommand },
+ { legacyName: 'randomuser', module: randomuserCommand },
+ { legacyName: 'shorten', module: shortenCommand },
+ { legacyName: 'time', module: timeCommand },
+ { legacyName: 'unixtime', module: unixtimeCommand },
+ { legacyName: 'weather', module: weatherCommand },
+ { legacyName: 'firstmsg', module: firstmsgCommand },
+ ],
+});
diff --git a/src/commands/Utility/info.ts b/src/commands/Utility/info.ts
new file mode 100644
index 0000000000..9f6fa61820
--- /dev/null
+++ b/src/commands/Utility/info.ts
@@ -0,0 +1,15 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import avatarCommand from './modules/avatar.js';
+import serverinfoCommand from './modules/serverinfo.js';
+import userinfoCommand from './modules/userinfo.js';
+
+export default createRouterCommand({
+ name: 'info',
+ description: 'Infos über Nutzer und den Server',
+ category: 'Utility',
+ routes: [
+ { legacyName: 'avatar', module: avatarCommand },
+ { legacyName: 'serverinfo', module: serverinfoCommand },
+ { legacyName: 'userinfo', module: userinfoCommand },
+ ],
+});
diff --git a/src/commands/Utility/avatar.js b/src/commands/Utility/modules/avatar.ts
similarity index 55%
rename from src/commands/Utility/avatar.js
rename to src/commands/Utility/modules/avatar.ts
index b9caf87daf..5e4d157ea6 100644
--- a/src/commands/Utility/avatar.js
+++ b/src/commands/Utility/modules/avatar.ts
@@ -1,27 +1,28 @@
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName("avatar")
- .setDescription("Display a user's avatar image")
+ .setDescription("Zeigt den Avatar eines Nutzers an")
.addUserOption((option) =>
option
.setName("target")
.setDescription(
- "The user whose avatar you want to see (defaults to you)",
+ "Der Nutzer, dessen Avatar du sehen willst (Standard: du)",
),
),
async execute(interaction) {
const user = interaction.options.getUser("target") || interaction.user;
- const avatarUrl = user.displayAvatarURL({ size: 2048, dynamic: true });
+ const avatarUrl = user.displayAvatarURL({ size: 2048 });
const embed = createEmbed({
- title: `${user.username}'s Avatar`,
- description: `[Download Link](${avatarUrl})`
+ title: `Avatar von ${user.username}`,
+ description: `[Download-Link](${avatarUrl})`
})
.setImage(avatarUrl);
@@ -32,4 +33,4 @@ export default {
guildId: interaction.guildId
});
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Utility/firstmsg.js b/src/commands/Utility/modules/firstmsg.ts
similarity index 68%
rename from src/commands/Utility/firstmsg.js
rename to src/commands/Utility/modules/firstmsg.ts
index 1624921387..18b271e7ab 100644
--- a/src/commands/Utility/firstmsg.js
+++ b/src/commands/Utility/modules/firstmsg.ts
@@ -1,11 +1,12 @@
-import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType } from 'discord.js';
-import { createEmbed, errorEmbed, successEmbed } from '../../utils/embeds.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { logger } from '../../utils/logger.js';
+import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, type GuildTextBasedChannel } from 'discord.js';
+import { createEmbed, errorEmbed, successEmbed } from '../../../utils/embeds.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { logger } from '../../../utils/logger.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName("firstmsg")
- .setDescription("Get a link to the first message in this channel")
+ .setDescription("Link zur ersten Nachricht in diesem Kanal")
.setDMPermission(false)
.setDefaultMemberPermissions(PermissionFlagsBits.SendMessages),
category: "Utility",
@@ -21,7 +22,7 @@ export default {
return;
}
- const messages = await interaction.channel.messages.fetch({
+ const messages = await (interaction.channel as GuildTextBasedChannel).messages.fetch({
limit: 1,
after: '1',
cache: false
@@ -36,7 +37,7 @@ export default {
guildId: interaction.guildId
});
return await InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed('First Message', "No messages found in this channel!")],
+ embeds: [successEmbed('Erste Nachricht', "In diesem Kanal wurden keine Nachrichten gefunden!")],
});
}
@@ -45,8 +46,8 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- "First Message in #" + interaction.channel.name,
- `Message Link: ${messageLink}`
+ "Erste Nachricht in #" + (interaction.channel as GuildTextBasedChannel).name,
+ `Link zur Nachricht: ${messageLink}`
),
],
});
@@ -58,4 +59,4 @@ export default {
guildId: interaction.guildId
});
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Utility/modules/report.js b/src/commands/Utility/modules/report.ts
similarity index 61%
rename from src/commands/Utility/modules/report.js
rename to src/commands/Utility/modules/report.ts
index 86b34b7773..87ebff603d 100644
--- a/src/commands/Utility/modules/report.js
+++ b/src/commands/Utility/modules/report.ts
@@ -5,6 +5,7 @@ import { formatLogLine, resolveUserAuthor } from '../../../utils/logging/logEmbe
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
import { logger } from '../../../utils/logger.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
@@ -14,20 +15,20 @@ export default {
return;
}
- const targetUser = interaction.options.getUser('user');
- const reason = interaction.options.getString('reason');
- const guildId = interaction.guildId;
+ const targetUser = interaction.options.getUser('user')!;
+ const reason = interaction.options.getString('reason')!;
+ const guildId = interaction.guildId!;
const guildConfig = await getGuildConfig(client, guildId);
const reportChannelId = resolveLogChannel(guildConfig, 'reports');
if (!reportChannelId) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'The report channel has not been set up. Ask a moderator to use `/logging dashboard` or `/logging channel`.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Der Meldekanal ist noch nicht eingerichtet. Bitte einen Moderator, `/logging dashboard` oder `/logging channel` zu nutzen.' });
}
- const ownerMention = interaction.guild.ownerId
- ? `<@${interaction.guild.ownerId}> New report!`
- : 'New report!';
+ const ownerMention = interaction.guild!.ownerId
+ ? `<@${interaction.guild!.ownerId}> Neue Meldung!`
+ : 'Neue Meldung!';
await logEvent({
client,
@@ -35,13 +36,13 @@ export default {
eventType: EVENT_TYPES.REPORT_FILE,
content: ownerMention,
data: {
- title: 'User Report',
+ title: 'Nutzermeldung',
lines: [
- formatLogLine('Reported User', `${targetUser.tag} (\`${targetUser.id}\`)`),
- formatLogLine('Reported By', `${interaction.user.tag} (\`${interaction.user.id}\`)`),
- formatLogLine('Channel', interaction.channel.toString()),
+ formatLogLine('Gemeldeter Nutzer', `${targetUser.tag} (\`${targetUser.id}\`)`),
+ formatLogLine('Gemeldet von', `${interaction.user.tag} (\`${interaction.user.id}\`)`),
+ formatLogLine('Kanal', interaction.channel!.toString()),
],
- blockFields: [{ name: 'Reason', value: reason }],
+ blockFields: [{ name: 'Grund', value: reason }],
author: await resolveUserAuthor(client, targetUser.id),
thumbnail: targetUser.displayAvatarURL(),
},
@@ -49,8 +50,8 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [createEmbed({
- title: 'Report Submitted',
- description: `Your report against **${targetUser.tag}** has been successfully filed and sent to the moderation team. Thank you!`,
+ title: 'Meldung gesendet',
+ description: `Deine Meldung gegen **${targetUser.tag}** wurde an das Moderationsteam gesendet. Danke!`,
})],
});
@@ -61,4 +62,4 @@ export default {
reasonLength: reason.length,
});
},
-};
+} satisfies SubcommandModule;
diff --git a/src/commands/Utility/modules/report_setchannel.js b/src/commands/Utility/modules/report_setchannel.ts
similarity index 57%
rename from src/commands/Utility/modules/report_setchannel.js
rename to src/commands/Utility/modules/report_setchannel.ts
index 0da04de754..6a543acf3a 100644
--- a/src/commands/Utility/modules/report_setchannel.js
+++ b/src/commands/Utility/modules/report_setchannel.ts
@@ -1,32 +1,33 @@
-import { PermissionsBitField } from 'discord.js';
+import { PermissionsBitField, type GuildMember } from 'discord.js';
import { successEmbed } from '../../../utils/embeds.js';
import { setLogChannel } from '../../../services/loggingService.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { logger } from '../../../utils/logger.js';
import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import type { SubcommandModule } from '../../../types/index.js';
export default {
async execute(interaction, config, client) {
- if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need **Manage Server** permissions to set the report channel.' });
+ if (!(interaction.member as GuildMember).permissions.has(PermissionsBitField.Flags.ManageGuild)) {
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um den Meldekanal festzulegen.' });
}
- const channel = interaction.options.getChannel('channel');
- const guildId = interaction.guildId;
+ const channel = interaction.options.getChannel('channel')!;
+ const guildId = interaction.guildId!;
try {
await setLogChannel(client, guildId, 'reports', channel.id);
return InteractionHelper.safeReply(interaction, {
embeds: [successEmbed(
- 'Report Channel Set',
- `All new reports will now be sent to ${channel}.\nYou can also manage this from \`/logging dashboard\`.`,
+ 'Meldekanal festgelegt',
+ `Alle neuen Meldungen gehen ab jetzt an ${channel}.\nDu kannst das auch über \`/logging dashboard\` verwalten.`,
)],
ephemeral: true,
});
} catch (error) {
logger.error('report_setchannel error:', error);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Could not save the channel configuration.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Die Kanal-Einstellung konnte nicht gespeichert werden.' });
}
},
-};
+} satisfies SubcommandModule;
diff --git a/src/commands/Utility/serverinfo.js b/src/commands/Utility/modules/serverinfo.ts
similarity index 63%
rename from src/commands/Utility/serverinfo.js
rename to src/commands/Utility/modules/serverinfo.ts
index fd417d6845..50683ad22d 100644
--- a/src/commands/Utility/serverinfo.js
+++ b/src/commands/Utility/modules/serverinfo.ts
@@ -1,12 +1,13 @@
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName("serverinfo")
- .setDescription("Get detailed information about the server"),
+ .setDescription("Zeigt ausführliche Infos über den Server"),
async execute(interaction) {
const deferSuccess = await InteractionHelper.safeDefer(interaction);
@@ -19,29 +20,29 @@ export default {
return;
}
- const guild = interaction.guild;
+ const guild = interaction.guild!;
const owner = await guild.fetchOwner();
const createdTimestamp = Math.floor(guild.createdAt.getTime() / 1000);
- const embed = createEmbed({ title: `Server Info: ${guild.name}`, description: `Server ID: ${guild.id}` })
+ const embed = createEmbed({ title: `Server-Info: ${guild.name}`, description: `Server-ID: ${guild.id}` })
.setThumbnail(guild.iconURL({ size: 256 }))
.addFields(
- { name: "Owner", value: owner.user.tag, inline: true },
- { name: "Members", value: `${guild.memberCount}`, inline: true },
+ { name: "Inhaber", value: owner.user.tag, inline: true },
+ { name: "Mitglieder", value: `${guild.memberCount}`, inline: true },
{
- name: "Channels",
+ name: "Kanäle",
value: `${guild.channels.cache.size}`,
inline: true,
},
- { name: "Roles", value: `${guild.roles.cache.size}`, inline: true },
+ { name: "Rollen", value: `${guild.roles.cache.size}`, inline: true },
{
name: "Boosts",
value: `Level ${guild.premiumTier} (${guild.premiumSubscriptionCount})`,
inline: true,
},
{
- name: "Creation Date",
+ name: "Erstellt",
value: ``,
inline: true,
},
@@ -55,4 +56,4 @@ export default {
memberCount: guild.memberCount
});
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Utility/userinfo.js b/src/commands/Utility/modules/userinfo.ts
similarity index 65%
rename from src/commands/Utility/userinfo.js
rename to src/commands/Utility/modules/userinfo.ts
index 2455b0f4a9..5457306d64 100644
--- a/src/commands/Utility/userinfo.js
+++ b/src/commands/Utility/modules/userinfo.ts
@@ -1,15 +1,16 @@
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName("userinfo")
- .setDescription("Get detailed information about a user")
+ .setDescription("Zeigt ausführliche Infos über einen Nutzer")
.addUserOption((option) =>
option
.setName("target")
- .setDescription("The user to inspect (defaults to you)"),
+ .setDescription("Der Nutzer, den du ansehen willst (Standard: du)"),
),
async execute(interaction) {
@@ -24,40 +25,40 @@ export default {
}
const user = interaction.options.getUser("target") || interaction.user;
- const member = interaction.guild.members.cache.get(user.id);
+ const member = interaction.guild!.members.cache.get(user.id);
const createdTimestamp = Math.floor(user.createdAt.getTime() / 1000);
const joinedTimestamp = member?.joinedAt ? Math.floor(member.joinedAt.getTime() / 1000) : null;
- const embed = createEmbed({ title: `User Info: ${user.username}` })
+ const embed = createEmbed({ title: `Nutzer-Info: ${user.username}` })
.setThumbnail(user.displayAvatarURL({ size: 256 }))
.addFields(
{ name: "ID", value: user.id, inline: true },
- { name: "Bot", value: user.bot ? "Yes" : "No", inline: true },
+ { name: "Bot", value: user.bot ? "Ja" : "Nein", inline: true },
{
- name: "Roles",
+ name: "Rollen",
value:
member && member.roles.cache.size > 1
? member.roles.cache
.map((r) => r.name)
.slice(0, 5)
.join(",")
- : "None",
+ : "Keine",
inline: true,
},
{
- name: "Account Created",
+ name: "Account erstellt",
value: ``,
inline: false,
},
{
- name: "Joined Server",
- value: joinedTimestamp ? `` : "Not in server",
+ name: "Server beigetreten",
+ value: joinedTimestamp ? `` : "Nicht auf dem Server",
inline: false,
},
{
- name: "Highest Role",
- value: member?.roles?.highest?.name || "None",
+ name: "Höchste Rolle",
+ value: member?.roles?.highest?.name || "Keine",
inline: true,
},
);
@@ -69,4 +70,4 @@ export default {
guildId: interaction.guildId
});
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Utility/weather.js b/src/commands/Utility/modules/weather.ts
similarity index 56%
rename from src/commands/Utility/weather.js
rename to src/commands/Utility/modules/weather.ts
index a76c3eeb09..a15b8db99a 100644
--- a/src/commands/Utility/weather.js
+++ b/src/commands/Utility/modules/weather.ts
@@ -1,19 +1,47 @@
import { SlashCommandBuilder } from 'discord.js';
-import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js';
-import { logger } from '../../utils/logger.js';
-import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
+import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../../utils/embeds.js';
+import { logger } from '../../../utils/logger.js';
+import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import type { Command } from '../../../types/index.js';
const GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";
const WEATHER_URL = "https://api.open-meteo.com/v1/forecast";
+interface GeocodingResult {
+ latitude: number;
+ longitude: number;
+ name: string;
+ country: string;
+}
+
+interface GeocodingResponse {
+ results?: GeocodingResult[];
+}
+
+interface CurrentWeather {
+ temperature?: number | null;
+ relativehumidity?: number;
+ relative_humidity_2m?: number;
+ windspeed?: number | null;
+ weathercode?: number;
+ weather_code?: number;
+}
+
+interface WeatherResponse {
+ error?: boolean;
+ reason?: string;
+ current?: CurrentWeather;
+ current_weather?: CurrentWeather;
+}
+
export default {
data: new SlashCommandBuilder()
.setName("weather")
- .setDescription("Get real-time weather information for a location")
+ .setDescription("Aktuelles Wetter für einen Ort abrufen")
.addStringOption((option) =>
option
.setName("city")
- .setDescription("The city name, e.g., 'London' or 'Tokyo'")
+ .setDescription("Der Name der Stadt, z. B. 'Berlin' oder 'Tokio'")
.setRequired(true),
),
@@ -31,9 +59,9 @@ export default {
const city = interaction.options.getString("city");
const geoResponse = await fetch(
- `${GEOCODING_URL}?name=${encodeURIComponent(city)}`,
+ `${GEOCODING_URL}?name=${encodeURIComponent(city!)}`,
);
- const geoData = await geoResponse.json();
+ const geoData = await geoResponse.json() as GeocodingResponse;
if (!geoData.results || geoData.results.length === 0) {
logger.info(`Weather command - city not found`, {
@@ -41,7 +69,7 @@ export default {
city: city,
guildId: interaction.guildId
});
- await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Could not find a location for **${city}**. Please check the spelling.` });
+ await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Für **${city}** wurde kein Ort gefunden. Bitte prüf die Schreibweise.` });
return;
}
@@ -51,7 +79,7 @@ export default {
const weatherResponse = await fetch(
`${WEATHER_URL}?latitude=${latitude}&longitude=${longitude}¤t_weather=true`,
);
- const weatherData = await weatherResponse.json();
+ const weatherData = await weatherResponse.json() as WeatherResponse;
if (weatherData.error) {
logger.error(`Weather API error`, {
@@ -60,38 +88,38 @@ export default {
userId: interaction.user.id,
guildId: interaction.guildId
});
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'A weather service error occurred.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Wetterdienst ist ein Fehler aufgetreten.' });
return;
}
- const current = weatherData.current || weatherData.current_weather || {};
- const temperature = current.temperature != null ? Math.round(current.temperature) : "N/A";
- const humidity = current.relativehumidity ?? current.relative_humidity_2m ?? "N/A";
- const windSpeed = current.windspeed != null ? Math.round(current.windspeed) : "N/A";
+ const current: CurrentWeather = weatherData.current || weatherData.current_weather || {};
+ const temperature = current.temperature != null ? Math.round(current.temperature) : "–";
+ const humidity = current.relativehumidity ?? current.relative_humidity_2m ?? "–";
+ const windSpeed = current.windspeed != null ? Math.round(current.windspeed) : "–";
const weatherCode = current.weathercode ?? current.weather_code ?? null;
const condition = getWeatherDescription(weatherCode);
- const embed = createEmbed({ title: `Weather in ${cityDisplay}, ${country}`, description: condition.description })
+ const embed = createEmbed({ title: `Wetter in ${cityDisplay}, ${country}`, description: condition.description })
.addFields(
{
- name: "Temperature",
+ name: "Temperatur",
value: `${temperature}°C`,
inline: true,
},
{
- name: "Humidity",
+ name: "Luftfeuchtigkeit",
value: `${humidity}%`,
inline: true,
},
{
- name: "Wind Speed",
+ name: "Windgeschwindigkeit",
value: `${windSpeed} km/h`,
inline: true,
},
)
.setFooter({
- text: `Latitude: ${latitude.toFixed(2)} | Longitude: ${longitude.toFixed(2)}`,
+ text: `Breitengrad: ${latitude.toFixed(2)} | Längengrad: ${longitude.toFixed(2)}`,
});
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
@@ -103,21 +131,23 @@ export default {
guildId: interaction.guildId
});
},
-};
+} satisfies Command;
-function getWeatherDescription(code) {
+function getWeatherDescription(codeValue: number | null) {
+ // Number(null) === 0, same coercion the relational operators applied in JS
+ const code = Number(codeValue);
if (code >= 0 && code <= 3) {
- return { description: "Clear sky / Partly cloudy", emoji: "" };
+ return { description: "Klar / teilweise bewölkt", emoji: "" };
} else if (code >= 45 && code <= 48) {
- return { description: "Fog and Rime fog", emoji: "" };
+ return { description: "Nebel und Raureifnebel", emoji: "" };
} else if (code >= 51 && code <= 67) {
- return { description: "Drizzle or Rain", emoji: "" };
+ return { description: "Nieselregen oder Regen", emoji: "" };
} else if (code >= 71 && code <= 75) {
- return { description: "Snow fall", emoji: "" };
+ return { description: "Schneefall", emoji: "" };
} else if (code >= 80 && code <= 86) {
- return { description: "Showers (Rain/Snow)", emoji: "" };
+ return { description: "Schauer (Regen/Schnee)", emoji: "" };
} else if (code >= 95 && code <= 99) {
- return { description: "Thunderstorm", emoji: "" };
+ return { description: "Gewitter", emoji: "" };
}
- return { description: "Unknown conditions.", emoji: "" };
+ return { description: "Unbekannte Wetterlage.", emoji: "" };
}
\ No newline at end of file
diff --git a/src/commands/Utility/report.js b/src/commands/Utility/report.ts
similarity index 71%
rename from src/commands/Utility/report.js
rename to src/commands/Utility/report.ts
index 561aaf983c..1511d498d8 100644
--- a/src/commands/Utility/report.js
+++ b/src/commands/Utility/report.ts
@@ -4,26 +4,27 @@ import { InteractionHelper } from '../../utils/interactionHelper.js';
import report from './modules/report.js';
import reportSetchannel from './modules/report_setchannel.js';
+import type { Command } from '../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('report')
- .setDescription('Report a user to server staff, or configure where reports are sent.')
+ .setDescription('Melde einen Nutzer an das Server-Team oder lege fest, wohin Meldungen gehen.')
.setDMPermission(false)
.addSubcommand(subcommand =>
subcommand
.setName('file')
- .setDescription('Report a user to the server moderation team.')
+ .setDescription('Melde einen Nutzer an das Moderationsteam.')
.addUserOption(option =>
option
.setName('user')
- .setDescription('The user you want to report.')
+ .setDescription('Der Nutzer, den du melden möchtest.')
.setRequired(true),
)
.addStringOption(option =>
option
.setName('reason')
- .setDescription('The reason for the report (be detailed).')
+ .setDescription('Der Grund für die Meldung (bitte ausführlich).')
.setRequired(true)
.setMaxLength(500),
),
@@ -31,11 +32,11 @@ export default {
.addSubcommand(subcommand =>
subcommand
.setName('setchannel')
- .setDescription('Set the channel where user reports are sent. (Manage Server required)')
+ .setDescription('Lege den Kanal für Nutzermeldungen fest. („Server verwalten“ nötig)')
.addChannelOption(option =>
option
.setName('channel')
- .setDescription('The text channel to receive reports.')
+ .setDescription('Der Textkanal, der die Meldungen empfängt.')
.addChannelTypes(ChannelType.GuildText)
.setRequired(true),
),
@@ -53,6 +54,6 @@ export default {
return await reportSetchannel.execute(interaction, config, client);
}
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unbekannter Subcommand.' });
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Utility/todo.js b/src/commands/Utility/todo.ts
similarity index 69%
rename from src/commands/Utility/todo.js
rename to src/commands/Utility/todo.ts
index af82306340..184ac0e65e 100644
--- a/src/commands/Utility/todo.js
+++ b/src/commands/Utility/todo.ts
@@ -5,6 +5,40 @@ import { logger } from '../../utils/logger.js';
import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
import crypto from 'crypto';
+import type { Command } from '../../types/index.js';
+
+/** Personal task stored under `todo_{userId}`. */
+export interface TodoTask {
+ id: number;
+ text: string;
+ completed: boolean;
+ createdAt: string;
+}
+
+/** Stored under `todo_{userId}`. */
+export interface TodoList {
+ tasks: TodoTask[];
+ nextId: number;
+}
+
+/** Task in a shared list. */
+export interface SharedTodoTask extends TodoTask {
+ createdBy: string;
+ completedBy?: string;
+}
+
+/** Stored under `shared_todo_{listId}`. */
+export interface SharedTodoList {
+ id: string;
+ name: string | null;
+ creatorId: string;
+ members: string[];
+ tasks: SharedTodoTask[];
+ nextId: number;
+ createdAt: string;
+ ok?: boolean;
+ error?: unknown;
+}
function generateShareId() {
return crypto.randomBytes(16).toString('hex');
@@ -13,119 +47,119 @@ function generateShareId() {
export default {
data: new SlashCommandBuilder()
.setName("todo")
- .setDescription("Manage your personal to-do list")
+ .setDescription("Verwalte deine persönliche To-do-Liste")
.addSubcommand(subcommand =>
subcommand
.setName("add")
- .setDescription("Add a task to your to-do list")
+ .setDescription("Füge eine Aufgabe zu deiner To-do-Liste hinzu")
.addStringOption(option =>
option
.setName("task")
- .setDescription("The task to add")
+ .setDescription("Die Aufgabe, die du hinzufügen willst")
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("list")
- .setDescription("View your to-do list")
+ .setDescription("Zeigt deine To-do-Liste an")
)
.addSubcommand(subcommand =>
subcommand
.setName("complete")
- .setDescription("Mark a task as complete")
+ .setDescription("Markiere eine Aufgabe als erledigt")
.addIntegerOption(option =>
option
.setName("number")
- .setDescription("The number of the task to complete")
+ .setDescription("Die Nummer der erledigten Aufgabe")
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("remove")
- .setDescription("Remove a task from your to-do list")
+ .setDescription("Entferne eine Aufgabe von deiner To-do-Liste")
.addIntegerOption(option =>
option
.setName("number")
- .setDescription("The number of the task to remove")
+ .setDescription("Die Nummer der Aufgabe, die du entfernen willst")
.setRequired(true)
)
)
.addSubcommandGroup(group =>
group
.setName("share")
- .setDescription("Manage shared to-do lists")
+ .setDescription("Verwalte geteilte To-do-Listen")
.addSubcommand(subcommand =>
subcommand
.setName("create")
- .setDescription("Create a new shared to-do list")
+ .setDescription("Erstelle eine neue geteilte To-do-Liste")
.addStringOption(option =>
option
.setName("name")
- .setDescription("Name for the shared list")
+ .setDescription("Name der geteilten Liste")
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("add")
- .setDescription("Add a member to a shared list")
+ .setDescription("Füge ein Mitglied zu einer geteilten Liste hinzu")
.addStringOption(option =>
option
.setName("list_id")
- .setDescription("ID of the shared list")
+ .setDescription("ID der geteilten Liste")
.setRequired(true)
)
.addUserOption(option =>
option
.setName("user")
- .setDescription("User to add to the list")
+ .setDescription("Nutzer, den du zur Liste hinzufügen willst")
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("view")
- .setDescription("View a shared to-do list")
+ .setDescription("Zeigt eine geteilte To-do-Liste an")
.addStringOption(option =>
option
.setName("list_id")
- .setDescription("ID of the shared list")
+ .setDescription("ID der geteilten Liste")
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("addtask")
- .setDescription("Add a task to a shared to-do list")
+ .setDescription("Füge eine Aufgabe zu einer geteilten To-do-Liste hinzu")
.addStringOption(option =>
option
.setName("list_id")
- .setDescription("ID of the shared list")
+ .setDescription("ID der geteilten Liste")
.setRequired(true)
)
.addStringOption(option =>
option
.setName("task")
- .setDescription("The task to add")
+ .setDescription("Die Aufgabe, die du hinzufügen willst")
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("remove")
- .setDescription("Remove a task from a shared to-do list")
+ .setDescription("Entferne eine Aufgabe von einer geteilten To-do-Liste")
.addStringOption(option =>
option
.setName("list_id")
- .setDescription("ID of the shared list")
+ .setDescription("ID der geteilten Liste")
.setRequired(true)
)
.addIntegerOption(option =>
option
.setName("number")
- .setDescription("The number of the task to remove")
+ .setDescription("Die Nummer der Aufgabe, die du entfernen willst")
.setRequired(true)
)
)
@@ -139,9 +173,9 @@ export default {
const subcommand = interaction.options.getSubcommand();
const shareSubcommand = interaction.options.getSubcommandGroup() === 'share' ? interaction.options.getSubcommand() : null;
- async function getOrCreateSharedList(listId, creatorId = null, listName = null) {
+ async function getOrCreateSharedList(listId: string, creatorId: string | null = null, listName: string | null = null): Promise {
const listKey = `shared_todo_${listId}`;
- let listData = await getFromDb(listKey, null);
+ let listData = await getFromDb(listKey, null);
if (!listData || (listData.ok === false && listData.error)) {
if (creatorId) {
@@ -187,7 +221,7 @@ export default {
await getOrCreateSharedList(listId, userId, listName);
- const userSharedLists = await getFromDb(`user_shared_lists_${userId}`, []);
+ const userSharedLists = await getFromDb(`user_shared_lists_${userId}`, []);
const sharedListsArray = Array.isArray(userSharedLists) ? userSharedLists : [];
if (!sharedListsArray.includes(listId)) {
sharedListsArray.push(listId);
@@ -197,32 +231,32 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- "Shared List Created",
- `Created shared list "${listName}" with ID: \`${listId}\`\n` +
- `Use \`/todo share add list_id:${listId} user:@username\` to add members.`
+ "Geteilte Liste erstellt",
+ `Geteilte Liste „${listName}“ mit der ID \`${listId}\` erstellt.\n` +
+ `Nutze \`/todo share add list_id:${listId} user:@username\`, um Mitglieder hinzuzufügen.`
)
]
});
}
case 'add': {
- const listId = interaction.options.getString('list_id');
- const memberToAdd = interaction.options.getUser('user');
+ const listId = interaction.options.getString('list_id')!;
+ const memberToAdd = interaction.options.getUser('user')!;
const listData = await getOrCreateSharedList(listId);
if (!listData) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Shared list not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Geteilte Liste nicht gefunden.' });
}
if (listData.creatorId !== userId) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Only the list creator can add members.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Nur wer die Liste erstellt hat, kann Mitglieder hinzufügen.' });
}
if (!listData.members.includes(memberToAdd.id)) {
listData.members.push(memberToAdd.id);
await setInDb(`shared_todo_${listId}`, listData);
- const memberLists = await getFromDb(`user_shared_lists_${memberToAdd.id}`, []);
+ const memberLists = await getFromDb(`user_shared_lists_${memberToAdd.id}`, []);
const memberListsArray = Array.isArray(memberLists) ? memberLists : [];
if (!memberListsArray.includes(listId)) {
memberListsArray.push(listId);
@@ -231,60 +265,60 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
- successEmbed('Member Added',
- `Added ${memberToAdd.username} to the shared list "${listData.name}"`
+ successEmbed('Mitglied hinzugefügt',
+ `${memberToAdd.username} wurde zur geteilten Liste „${listData.name}“ hinzugefügt.`
)
]
});
} else {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'User is already a member of this list.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Der Nutzer ist bereits Mitglied dieser Liste.' });
}
}
case 'view': {
- const listId = interaction.options.getString('list_id');
+ const listId = interaction.options.getString('list_id')!;
const listData = await getOrCreateSharedList(listId);
if (!listData) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Shared list not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Geteilte Liste nicht gefunden.' });
}
if (!listData.members.includes(userId)) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You don\'t have access to this list.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du hast keinen Zugriff auf diese Liste.' });
}
if (listData.tasks.length === 0) {
const memberList = listData.members.map(memberId => {
- const member = interaction.guild.members.cache.get(memberId);
+ const member = interaction.guild!.members.cache.get(memberId);
return member ? member.user.username : `<@${memberId}>`;
}).join(',');
- const owner = interaction.guild.members.cache.get(listData.creatorId);
+ const owner = interaction.guild!.members.cache.get(listData.creatorId);
const ownerName = owner ? owner.user.username : `<@${listData.creatorId}>`;
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
`📋 **${listData.name}**\n\n` +
- `👑 **Owner:** ${ownerName}\n` +
- `👥 **Members:** ${memberList}\n\n` +
- `*This list is currently empty. Use the "Add Task" button to add tasks!*`,
- `Shared List (ID: \`${listId}\`)`
+ `👑 **Inhaber:** ${ownerName}\n` +
+ `👥 **Mitglieder:** ${memberList}\n\n` +
+ `*Diese Liste ist gerade leer. Nutze den Button „Aufgabe hinzufügen“, um Aufgaben hinzuzufügen!*`,
+ `Geteilte Liste (ID: \`${listId}\`)`
)
],
components: [
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`shared_todo_add_${listId}`)
- .setLabel('Add Task')
+ .setLabel('Aufgabe hinzufügen')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`shared_todo_complete_${listId}`)
- .setLabel('Complete Task')
+ .setLabel('Aufgabe erledigen')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`shared_todo_remove_${listId}`)
- .setLabel('Remove Task')
+ .setLabel('Aufgabe entfernen')
.setStyle(ButtonStyle.Danger)
)
]
@@ -295,40 +329,40 @@ export default {
.map(task =>
`${task.completed ? '✅' : '📝'} #${task.id} ${task.text}` +
`\`[${new Date(task.createdAt).toLocaleDateString()}]` +
- (task.completed ? `• Completed by ${task.completedBy}` : '') + '`'
+ (task.completed ? `• Erledigt von ${task.completedBy}` : '') + '`'
)
.join('\n');
const memberList = listData.members.map(memberId => {
- const member = interaction.guild.members.cache.get(memberId);
+ const member = interaction.guild!.members.cache.get(memberId);
return member ? member.user.username : `<@${memberId}>`;
}).join(',');
- const owner = interaction.guild.members.cache.get(listData.creatorId);
+ const owner = interaction.guild!.members.cache.get(listData.creatorId);
const ownerName = owner ? owner.user.username : `<@${listData.creatorId}>`;
const fullListDisplay = `📋 **${listData.name}**\n\n` +
- `👑 **Owner:** ${ownerName}\n` +
- `👥 **Members:** ${memberList}\n\n` +
- `**Tasks:**\n${taskList}`;
+ `👑 **Inhaber:** ${ownerName}\n` +
+ `👥 **Mitglieder:** ${memberList}\n\n` +
+ `**Aufgaben:**\n${taskList}`;
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
- successEmbed(`Shared List (ID: \`${listId}\`)`, fullListDisplay)
+ successEmbed(`Geteilte Liste (ID: \`${listId}\`)`, fullListDisplay)
],
components: [
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`shared_todo_add_${listId}`)
- .setLabel('Add Task')
+ .setLabel('Aufgabe hinzufügen')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`shared_todo_complete_${listId}`)
- .setLabel('Complete Task')
+ .setLabel('Aufgabe erledigen')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`shared_todo_remove_${listId}`)
- .setLabel('Remove Task')
+ .setLabel('Aufgabe entfernen')
.setStyle(ButtonStyle.Danger)
)
]
@@ -336,20 +370,20 @@ export default {
}
case 'addtask': {
- const listId = interaction.options.getString('list_id');
- const taskText = interaction.options.getString('task');
+ const listId = interaction.options.getString('list_id')!;
+ const taskText = interaction.options.getString('task')!;
const listData = await getOrCreateSharedList(listId);
if (!listData) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Shared list not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Geteilte Liste nicht gefunden.' });
}
if (!listData.members.includes(userId)) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You don\'t have access to this list.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du hast keinen Zugriff auf diese Liste.' });
}
- const newTask = {
+ const newTask: SharedTodoTask = {
id: listData.nextId++,
text: taskText,
completed: false,
@@ -362,28 +396,28 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
- successEmbed('Task Added', `Added "${taskText}" to the shared list "${listData.name}"`)
+ successEmbed('Aufgabe hinzugefügt', `„${taskText}“ wurde zur geteilten Liste „${listData.name}“ hinzugefügt.`)
]
});
}
case 'remove': {
- const listId = interaction.options.getString('list_id');
+ const listId = interaction.options.getString('list_id')!;
const taskNumber = interaction.options.getInteger('number');
const listData = await getOrCreateSharedList(listId);
if (!listData) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Shared list not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Geteilte Liste nicht gefunden.' });
}
if (!listData.members.includes(userId)) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You don\'t have access to this list.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du hast keinen Zugriff auf diese Liste.' });
}
const taskIndex = listData.tasks.findIndex(task => task.id === taskNumber);
if (taskIndex === -1) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Task not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Aufgabe nicht gefunden.' });
}
const [removedTask] = listData.tasks.splice(taskIndex, 1);
@@ -391,7 +425,7 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
- successEmbed('Task Removed', `Removed "${removedTask.text}" from the shared list "${listData.name}".`)
+ successEmbed('Aufgabe entfernt', `„${removedTask.text}“ wurde aus der geteilten Liste „${listData.name}“ entfernt.`)
]
});
}
@@ -401,7 +435,7 @@ export default {
const dbKey = `todo_${userId}`;
- const userData = await getFromDb(dbKey, {
+ const userData = await getFromDb(dbKey, {
tasks: [],
nextId: 1
});
@@ -411,9 +445,9 @@ export default {
switch (subcommand) {
case 'add': {
- const taskText = interaction.options.getString('task');
+ const taskText = interaction.options.getString('task')!;
- const newTask = {
+ const newTask: TodoTask = {
id: userData.nextId++,
text: taskText,
completed: false,
@@ -426,8 +460,8 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
successEmbed(
- "Task Added",
- `Added "${taskText}" to your to-do list.`
+ "Aufgabe hinzugefügt",
+ `„${taskText}“ wurde zu deiner To-do-Liste hinzugefügt.`
),
],
});
@@ -436,7 +470,7 @@ export default {
case 'list': {
if (userData.tasks.length === 0) {
return await InteractionHelper.safeEditReply(interaction, {
- embeds: [successEmbed('Your to-do list is empty!', "Your To-Do List")],
+ embeds: [successEmbed('Deine To-do-Liste ist leer!', "Deine To-do-Liste")],
});
}
@@ -449,7 +483,7 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
- successEmbed('Your To-Do List', taskList)
+ successEmbed('Deine To-do-Liste', taskList)
],
});
}
@@ -459,11 +493,11 @@ export default {
const task = userData.tasks.find(t => t.id === taskNumber);
if (!task) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Task not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Aufgabe nicht gefunden.' });
}
if (task.completed) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Task #${task.id} is already completed.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Aufgabe #${task.id} ist bereits erledigt.` });
}
task.completed = true;
@@ -471,7 +505,7 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
- successEmbed('Task Completed', `Marked "${task.text}" as complete!`)
+ successEmbed('Aufgabe erledigt', `„${task.text}“ ist als erledigt markiert!`)
],
});
}
@@ -481,7 +515,7 @@ export default {
const taskIndex = userData.tasks.findIndex(t => t.id === taskNumber);
if (taskIndex === -1) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Task not found.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Aufgabe nicht gefunden.' });
}
const [removedTask] = userData.tasks.splice(taskIndex, 1);
@@ -489,13 +523,13 @@ export default {
return await InteractionHelper.safeEditReply(interaction, {
embeds: [
- successEmbed('Task Removed', `Removed "${removedTask.text}" from your to-do list.`)
+ successEmbed('Aufgabe entfernt', `„${removedTask.text}“ wurde von deiner To-do-Liste entfernt.`)
],
});
}
default:
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Invalid subcommand.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Ungültiger Subcommand.' });
}
},
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Utility/wipedata.js b/src/commands/Utility/wipedata.ts
similarity index 52%
rename from src/commands/Utility/wipedata.js
rename to src/commands/Utility/wipedata.ts
index 941265dca8..792a830f6c 100644
--- a/src/commands/Utility/wipedata.js
+++ b/src/commands/Utility/wipedata.ts
@@ -4,26 +4,27 @@ import { getConfirmationButtons } from '../../utils/components.js';
import { logger } from '../../utils/logger.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../types/index.js';
export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName('wipedata')
- .setDescription('Delete all your personal data from the bot (irreversible)'),
+ .setDescription('Lösche alle deine persönlichen Daten aus dem Bot (unwiderruflich)'),
async execute(interaction, guildConfig, client) {
const warningMessage =
- `⚠️ **THIS ACTION IS IRREVERSIBLE!** ⚠️\n\n` +
- `This will permanently delete **ALL** your data from this server including:\n` +
- `• 💰 Economy balance (wallet & bank)\n` +
- `• 📊 Levels and XP\n` +
- `• 🎒 Inventory items\n` +
- `• 🛍️ Shop purchases\n` +
- `• 🎂 Birthday information\n` +
- `• 🔢 Counter data\n` +
- `• 📋 All other personal data\n\n` +
- `**This cannot be undone. Are you absolutely sure?**`;
+ `⚠️ **DIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN!** ⚠️\n\n` +
+ `Damit werden **ALLE** deine Daten auf diesem Server dauerhaft gelöscht, darunter:\n` +
+ `• 💰 Kontostand (Geldbörse & Bank)\n` +
+ `• 📊 Level und XP\n` +
+ `• 🎒 Items im Inventar\n` +
+ `• 🛍️ Shop-Käufe\n` +
+ `• 🎂 Geburtstagsdaten\n` +
+ `• 🔢 Zählerdaten\n` +
+ `• 📋 Alle weiteren persönlichen Daten\n\n` +
+ `**Das lässt sich nicht rückgängig machen. Bist du dir ganz sicher?**`;
- const embed = warningEmbed('Wipe All Data', warningMessage);
+ const embed = warningEmbed('Alle Daten löschen', warningMessage);
const confirmButtons = getConfirmationButtons('wipedata');
@@ -38,4 +39,4 @@ export default {
guildId: interaction.guildId
});
}
-};
\ No newline at end of file
+} satisfies Command;
\ No newline at end of file
diff --git a/src/commands/Verification/autoverify.js b/src/commands/Verification/autoverify.js
deleted file mode 100644
index 562f81758f..0000000000
--- a/src/commands/Verification/autoverify.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './modules/autoVerify.js';
\ No newline at end of file
diff --git a/src/commands/Verification/modules/autoVerify.js b/src/commands/Verification/modules/autoVerify.ts
similarity index 69%
rename from src/commands/Verification/modules/autoVerify.js
rename to src/commands/Verification/modules/autoVerify.ts
index 31ca0e881d..345b1ce1d3 100644
--- a/src/commands/Verification/modules/autoVerify.js
+++ b/src/commands/Verification/modules/autoVerify.ts
@@ -1,5 +1,6 @@
import { botConfig, getColor } from '../../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
+import type { ChatInputCommandInteraction, Guild } from 'discord.js';
import { createEmbed, errorEmbed, successEmbed, infoEmbed } from '../../../utils/embeds.js';
import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js';
import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js';
@@ -8,6 +9,8 @@ import { logger } from '../../../utils/logger.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { getWelcomeConfig } from '../../../utils/database.js';
import autoVerifyDashboard from './autoVerifyDashboard.js';
+import type { AutoVerifyConfig, VerificationConfig } from '../../../services/verificationService.js';
+import type { Command, NexoClient } from '../../../types/index.js';
const autoVerifyDefaults = botConfig.verification?.autoVerify || {};
const minAccountAgeDays = autoVerifyDefaults.minAccountAge ?? 1;
@@ -17,32 +20,32 @@ const defaultAccountAgeDays = autoVerifyDefaults.defaultAccountAgeDays ?? 7;
export default {
data: new SlashCommandBuilder()
.setName("autoverify")
- .setDescription("Configure automatic verification settings")
+ .setDescription('Einstellungen für die automatische Verifizierung')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand =>
subcommand
.setName("setup")
- .setDescription("Set up automatic verification")
+ .setDescription('Die automatische Verifizierung einrichten')
.addRoleOption(option =>
option
.setName("role")
- .setDescription("Role to assign to users who meet auto-verify criteria")
+ .setDescription('Rolle für Nutzer, die die AutoVerify-Kriterien erfüllen')
.setRequired(true)
)
.addStringOption(option =>
option
.setName("criteria")
- .setDescription("Criteria for automatic verification")
+ .setDescription('Kriterien für die automatische Verifizierung')
.addChoices(
- { name: "Account Age", value: "account_age" },
- { name: "No Criteria", value: "none" }
+ { name: 'Account-Alter', value: "account_age" },
+ { name: 'Keine Kriterien', value: "none" }
)
.setRequired(true)
)
.addIntegerOption(option =>
option
.setName("account_age_days")
- .setDescription("Minimum account age in days (required for account age criteria)")
+ .setDescription('Mindestalter des Accounts in Tagen (nötig für das Kriterium Account-Alter)')
.setMinValue(minAccountAgeDays)
.setMaxValue(maxAccountAgeDays)
.setRequired(false)
@@ -51,13 +54,13 @@ export default {
.addSubcommand(subcommand =>
subcommand
.setName("dashboard")
- .setDescription("Open the auto-verification dashboard for customization")
+ .setDescription('Das AutoVerify-Dashboard zum Anpassen öffnen')
),
async execute(interaction, config, client) {
- const wrappedExecute = withErrorHandling(async () => {
+ const wrappedExecute = withErrorHandling(async (..._args: unknown[]) => {
const subcommand = interaction.options.getSubcommand();
- const guild = interaction.guild;
+ const guild = interaction.guild!;
switch (subcommand) {
case "setup":
@@ -68,7 +71,7 @@ export default {
throw createError(
`Unknown subcommand: ${subcommand}`,
ErrorTypes.VALIDATION,
- "Invalid subcommand selected.",
+ 'Ungültiger Subcommand ausgewählt.',
{ subcommand }
);
}
@@ -76,12 +79,12 @@ export default {
return await wrappedExecute(interaction, config, client);
}
-};
+} satisfies Command;
-async function handleSetup(interaction, guild, client) {
- const criteria = interaction.options.getString("criteria");
+async function handleSetup(interaction: ChatInputCommandInteraction, guild: Guild, client: NexoClient) {
+ const criteria = interaction.options.getString("criteria")!;
const accountAgeDays = interaction.options.getInteger("account_age_days") || defaultAccountAgeDays;
- const targetRole = interaction.options.getRole("role");
+ const targetRole = interaction.options.getRole("role")!;
await InteractionHelper.safeDefer(interaction);
@@ -95,7 +98,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
'Auto-verify enable blocked by conflicting onboarding system',
ErrorTypes.CONFIGURATION,
- 'You cannot enable **AutoVerify** while the verification system or AutoRole is configured. Disable those first.',
+ 'Du kannst **AutoVerify** nicht aktivieren, solange die Verifizierung oder eine Auto-Rolle eingerichtet ist. Deaktiviere diese zuerst.',
{
guildId: guild.id,
verificationEnabled,
@@ -111,7 +114,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
'Bot member not found in guild cache',
ErrorTypes.CONFIGURATION,
- 'I could not verify my permissions in this server. Please try again in a moment.',
+ 'Ich konnte meine Berechtigungen auf diesem Server nicht prüfen. Bitte versuch es gleich noch einmal.',
{ guildId: guild.id }
);
}
@@ -120,7 +123,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
'Missing ManageRoles permission',
ErrorTypes.PERMISSION,
- "I need the 'Manage Roles' permission to assign auto-verify roles.",
+ 'Ich brauche die Berechtigung „Rollen verwalten“, um AutoVerify-Rollen zu vergeben.',
{ guildId: guild.id }
);
}
@@ -129,7 +132,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
'Invalid auto-verify role selected',
ErrorTypes.VALIDATION,
- 'Please choose a normal assignable role (not @everyone or an integration-managed role).',
+ 'Bitte wähle eine normale, vergebbare Rolle (nicht @everyone und keine von einer Integration verwaltete Rolle).',
{ guildId: guild.id, roleId: targetRole.id, managed: targetRole.managed }
);
}
@@ -138,7 +141,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
'Role hierarchy error for auto-verify setup',
ErrorTypes.PERMISSION,
- 'The selected auto-verify role must be below my highest role in the server role hierarchy.',
+ 'Die gewählte AutoVerify-Rolle muss in der Rollenhierarchie unter meiner höchsten Rolle liegen.',
{ guildId: guild.id, roleId: targetRole.id, rolePosition: targetRole.position, botRolePosition: botMember.roles.highest.position }
);
}
@@ -146,26 +149,28 @@ async function handleSetup(interaction, guild, client) {
validateAutoVerifyCriteria(criteria, criteria === 'account_age' ? accountAgeDays : 1);
if (!guildConfig.verification) {
- guildConfig.verification = {};
+ guildConfig.verification = {} as VerificationConfig;
}
- guildConfig.verification.autoVerify = {
+ // Stored record keeps the extra `configuredVia` key; the schema type does not list it.
+ const autoVerifyConfig = {
enabled: true,
- criteria: criteria,
+ criteria: criteria as AutoVerifyConfig["criteria"],
accountAgeDays: criteria === "account_age" ? accountAgeDays : null,
roleId: targetRole.id,
configuredVia: 'setup'
};
+ guildConfig.verification.autoVerify = autoVerifyConfig;
await setGuildConfig(client, guild.id, guildConfig);
let criteriaDescription = "";
switch (criteria) {
case "account_age":
- criteriaDescription = `\`${accountAgeDays} days\` old`;
+ criteriaDescription = `mindestens \`${accountAgeDays} Tage\` alt`;
break;
case "none":
- criteriaDescription = "All users immediately";
+ criteriaDescription = 'Alle Nutzer sofort';
break;
}
@@ -178,8 +183,8 @@ async function handleSetup(interaction, guild, client) {
await InteractionHelper.safeEditReply(interaction, {
embeds: [successEmbed(
- "Auto-Verification Configured",
- `Automatic verification has been configured!\n\n**Role:** ${targetRole}\n**Criteria:** ${criteriaDescription}\n\nUsers who meet these criteria will receive this role when they join the server.`
+ 'AutoVerify eingerichtet',
+ `Die automatische Verifizierung ist eingerichtet!\n\n**Rolle:** ${targetRole}\n**Kriterien:** ${criteriaDescription}\n\nNutzer, die diese Kriterien erfüllen, erhalten die Rolle beim Beitritt zum Server.`
)]
});
diff --git a/src/commands/Verification/modules/autoVerifyDashboard.js b/src/commands/Verification/modules/autoVerifyDashboard.ts
similarity index 61%
rename from src/commands/Verification/modules/autoVerifyDashboard.js
rename to src/commands/Verification/modules/autoVerifyDashboard.ts
index 7209da79fd..fae8f8daf3 100644
--- a/src/commands/Verification/modules/autoVerifyDashboard.js
+++ b/src/commands/Verification/modules/autoVerifyDashboard.ts
@@ -13,95 +13,98 @@ import {
ComponentType,
EmbedBuilder,
} from 'discord.js';
+import type { ButtonInteraction, ChatInputCommandInteraction, Guild, StringSelectMenuInteraction } from 'discord.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { successEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js';
import { getWelcomeConfig } from '../../../utils/database.js';
import { validateAutoVerifyCriteria } from '../../../services/verificationService.js';
import { botHasPermission } from '../../../utils/permissionGuard.js';
+import type { AutoVerifyConfig } from '../../../services/verificationService.js';
+import type { GuildConfig, NexoClient, SubcommandModule } from '../../../types/index.js';
const autoVerifyDefaults = botConfig.verification?.autoVerify || {};
const minAccountAgeDays = autoVerifyDefaults.minAccountAge ?? 1;
const maxAccountAgeDays = autoVerifyDefaults.maxAccountAge ?? 365;
const defaultAccountAgeDays = autoVerifyDefaults.defaultAccountAgeDays ?? 7;
-function buildDashboardEmbed(cfg, guild, conflictSummary = '') {
+function buildDashboardEmbed(cfg: GuildConfig, guild: Guild, conflictSummary = '') {
const autoVerify = cfg.verification?.autoVerify;
const autoVerifyRole = autoVerify?.roleId ? guild.roles.cache.get(autoVerify.roleId) : null;
- let criteriaDescription = "`Not configured`";
+ let criteriaDescription = '`Nicht eingerichtet`';
if (autoVerify?.criteria) {
switch (autoVerify.criteria) {
case "account_age":
- criteriaDescription = `\`Account Age\` - \`${autoVerify.accountAgeDays} days\``;
+ criteriaDescription = `\`Account-Alter\` – \`${autoVerify.accountAgeDays} Tage\``;
break;
case "none":
- criteriaDescription = `\`No Criteria\``;
+ criteriaDescription = `\`Keine Kriterien\``;
break;
}
}
const embed = new EmbedBuilder()
- .setTitle('🤖 Auto-Verification Dashboard')
- .setDescription(`Manage auto-verification settings for **${guild.name}**.\nSelect an option below to modify a setting.`)
+ .setTitle('🤖 AutoVerify-Dashboard')
+ .setDescription(`Verwalte die AutoVerify-Einstellungen für **${guild.name}**.\nWähle unten eine Option, um eine Einstellung zu ändern.`)
.setColor(getColor('info'))
.addFields(
- { name: 'System Status', value: autoVerify?.enabled ? 'Enabled' : 'Disabled', inline: true },
- { name: 'Target Role', value: autoVerifyRole ? autoVerifyRole.toString() : '`Not set`', inline: true },
- { name: 'Criteria', value: criteriaDescription, inline: true },
- { name: 'Account Age', value: autoVerify?.accountAgeDays ? `\`${autoVerify.accountAgeDays}\` days` : '`N/A`', inline: true },
+ { name: 'Systemstatus', value: autoVerify?.enabled ? 'Aktiviert' : 'Deaktiviert', inline: true },
+ { name: 'Zielrolle', value: autoVerifyRole ? autoVerifyRole.toString() : '`Nicht gesetzt`', inline: true },
+ { name: 'Kriterien', value: criteriaDescription, inline: true },
+ { name: 'Account-Alter', value: autoVerify?.accountAgeDays ? `\`${autoVerify.accountAgeDays}\` Tage` : '`–`', inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
);
if (conflictSummary) {
- embed.addFields({ name: 'Setup Conflicts', value: conflictSummary, inline: false });
+ embed.addFields({ name: 'Konflikte bei der Einrichtung', value: conflictSummary, inline: false });
}
return embed
- .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität' })
.setTimestamp();
}
-function buildSelectMenu(guildId) {
+function buildSelectMenu(guildId: string) {
return new StringSelectMenuBuilder()
.setCustomId(`autoverify_cfg_${guildId}`)
- .setPlaceholder('Select a setting to configure...')
+ .setPlaceholder('Wähle eine Einstellung...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Change Role')
- .setDescription('Select the role to assign automatically')
+ .setLabel('Rolle ändern')
+ .setDescription('Wähle die Rolle, die automatisch vergeben wird')
.setValue('role')
.setEmoji('🏷️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Edit Account Age Days')
- .setDescription('Set minimum account age in days')
+ .setLabel('Account-Alter bearbeiten')
+ .setDescription('Lege das Mindestalter des Accounts in Tagen fest')
.setValue('account_age')
.setEmoji('📅'),
);
}
-function buildButtonRow(cfg, guildId, disabled = false) {
+function buildButtonRow(cfg: GuildConfig, guildId: string, disabled = false) {
const autoVerifyOn = cfg.verification?.autoVerify?.enabled === true;
- return new ActionRowBuilder().addComponents(
+ return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`autoverify_cfg_criteria_${guildId}`)
- .setLabel('Change Criteria')
+ .setLabel('Kriterien ändern')
.setStyle(ButtonStyle.Primary)
.setEmoji('🎯')
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId(`autoverify_cfg_toggle_${guildId}`)
- .setLabel('Auto-Verification')
+ .setLabel('AutoVerify')
.setStyle(autoVerifyOn ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji('🤖')
.setDisabled(disabled),
);
}
-async function refreshDashboard(rootInteraction, cfg, guildId, client) {
+async function refreshDashboard(rootInteraction: ChatInputCommandInteraction, cfg: GuildConfig, guildId: string, client: NexoClient) {
try {
const selectMenu = buildSelectMenu(guildId);
@@ -112,27 +115,27 @@ async function refreshDashboard(rootInteraction, cfg, guildId, client) {
const autoRoleConfigured = Boolean(cfg.autoRole) || (Array.isArray(welcomeConfig.roleIds) && welcomeConfig.roleIds.length > 0);
const conflicts = [
- verificationEnabled ? 'Verification system is enabled' : null,
- autoRoleConfigured ? 'AutoRole is configured' : null
+ verificationEnabled ? 'Die Verifizierung ist aktiviert' : null,
+ autoRoleConfigured ? 'Eine Auto-Rolle ist eingerichtet' : null
].filter(Boolean);
if (conflicts.length > 0) {
conflictSummary = conflicts.join('\n');
}
} catch (error) {
- logger.warn('Could not fetch autoverify dashboard conflicts:', error.message);
+ logger.warn('Could not fetch autoverify dashboard conflicts:', (error as Error).message);
}
await InteractionHelper.safeEditReply(rootInteraction, {
- embeds: [buildDashboardEmbed(cfg, rootInteraction.guild, conflictSummary)],
+ embeds: [buildDashboardEmbed(cfg, rootInteraction.guild!, conflictSummary)],
components: [
buildButtonRow(cfg, guildId),
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
flags: MessageFlags.Ephemeral,
});
} catch (error) {
- logger.debug('Could not refresh autoverify dashboard (interaction may have expired):', error.message);
+ logger.debug('Could not refresh autoverify dashboard (interaction may have expired):', (error as Error).message);
}
}
@@ -140,7 +143,7 @@ export default {
prefixOnly: false,
async execute(interaction, config, client) {
try {
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
const guildConfig = await getGuildConfig(client, guildId);
if (!guildConfig.verification?.autoVerify?.enabled) {
@@ -149,21 +152,21 @@ export default {
const verificationEnabled = Boolean(guildConfig.verification?.enabled);
const autoRoleConfigured = Boolean(guildConfig.autoRole) || (Array.isArray(welcomeConfig.roleIds) && welcomeConfig.roleIds.length > 0);
- const blockingMessage = [];
- if (verificationEnabled) blockingMessage.push('Verification system is enabled');
- if (autoRoleConfigured) blockingMessage.push('AutoRole is configured');
+ const blockingMessage: string[] = [];
+ if (verificationEnabled) blockingMessage.push('Die Verifizierung ist aktiviert');
+ if (autoRoleConfigured) blockingMessage.push('Eine Auto-Rolle ist eingerichtet');
const blockingText = blockingMessage.length > 0
- ? `\n\n⚠️ **To enable AutoVerify, you must first disable:**\n${blockingMessage.map(msg =>`• ${msg}`).join('\n')}`
+ ? `\n\n⚠️ **Um AutoVerify zu aktivieren, musst du zuerst deaktivieren:**\n${blockingMessage.map(msg =>`• ${msg}`).join('\n')}`
: '';
return await InteractionHelper.safeReply(interaction, {
embeds: [
new EmbedBuilder()
- .setTitle('🤖 Auto-Verification Dashboard')
- .setDescription(`Auto-verification is not yet configured.${blockingText}\n\nUse \`/autoverify setup\` to configure it.`)
+ .setTitle('🤖 AutoVerify-Dashboard')
+ .setDescription(`AutoVerify ist noch nicht eingerichtet.${blockingText}\n\nNutze \`/verification autoverify setup\`, um es einzurichten.`)
.setColor(getColor('warning'))
- .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität' })
.setTimestamp()
],
flags: MessageFlags.Ephemeral
@@ -181,27 +184,27 @@ export default {
const autoRoleConfigured = Boolean(guildConfig.autoRole) || (Array.isArray(welcomeConfig.roleIds) && welcomeConfig.roleIds.length > 0);
const conflicts = [
- verificationEnabled ? 'Verification system is enabled' : null,
- autoRoleConfigured ? 'AutoRole is configured' : null
+ verificationEnabled ? 'Die Verifizierung ist aktiviert' : null,
+ autoRoleConfigured ? 'Eine Auto-Rolle ist eingerichtet' : null
].filter(Boolean);
if (conflicts.length > 0) {
conflictSummary = conflicts.join('\n');
}
} catch (error) {
- logger.warn('Could not fetch autoverify dashboard conflicts:', error.message);
+ logger.warn('Could not fetch autoverify dashboard conflicts:', (error as Error).message);
}
await InteractionHelper.safeEditReply(interaction, {
- embeds: [buildDashboardEmbed(guildConfig, interaction.guild, conflictSummary)],
+ embeds: [buildDashboardEmbed(guildConfig, interaction.guild!, conflictSummary)],
components: [
buildButtonRow(guildConfig, guildId),
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
flags: MessageFlags.Ephemeral,
});
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === interaction.user.id && i.customId === `autoverify_cfg_${guildId}`,
@@ -220,16 +223,16 @@ export default {
break;
}
} catch (error) {
- if (error instanceof TitanBotError) {
- logger.debug(`Autoverify config validation error: ${error.message}`);
+ if (error instanceof NexoBotError) {
+ logger.debug(`Autoverify config validation error: ${(error as Error).message}`);
} else {
logger.error('Unexpected autoverify dashboard error:', error);
}
const errorMessage =
- error instanceof TitanBotError
- ? error.userMessage || 'An error occurred while processing your selection.'
- : 'An unexpected error occurred while updating the configuration.';
+ error instanceof NexoBotError
+ ? error.userMessage || 'Beim Verarbeiten deiner Auswahl ist ein Fehler aufgetreten.'
+ : 'Beim Aktualisieren der Konfiguration ist ein unerwarteter Fehler aufgetreten.';
if (!selectInteraction.replied && !selectInteraction.deferred) {
await selectInteraction.deferUpdate().catch(() => {});
@@ -242,7 +245,7 @@ export default {
}
});
- const btnCollector = interaction.channel.createMessageComponentCollector({
+ const btnCollector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: i =>
i.user.id === interaction.user.id &&
@@ -256,14 +259,14 @@ export default {
await handleCriteria(btnInteraction, interaction, guildConfig, guildId, client);
} else if (btnInteraction.customId === `autoverify_cfg_toggle_${guildId}`) {
await btnInteraction.deferUpdate().catch(() => null);
- guildConfig.verification.autoVerify.enabled = !guildConfig.verification.autoVerify.enabled;
+ guildConfig.verification!.autoVerify!.enabled = !guildConfig.verification!.autoVerify!.enabled;
await setGuildConfig(client, guildId, guildConfig);
await btnInteraction.followUp({
embeds: [
successEmbed(
- '✅ Status Updated',
- `Auto-verification is now **${guildConfig.verification.autoVerify.enabled ? 'enabled' : 'disabled'}**.`,
+ '✅ Status aktualisiert',
+ `AutoVerify ist jetzt **${guildConfig.verification!.autoVerify!.enabled ? 'aktiviert' : 'deaktiviert'}**.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -272,7 +275,7 @@ export default {
await refreshDashboard(interaction, guildConfig, guildId, client);
}
} catch (err) {
- logger.debug('Button interaction error:', err.message);
+ logger.debug('Button interaction error:', (err as Error).message);
}
});
@@ -281,8 +284,8 @@ export default {
btnCollector.stop();
try {
const timeoutEmbed = new EmbedBuilder()
- .setTitle('Dashboard Timed Out')
- .setDescription('This dashboard has been closed due to inactivity. Please run the command again to continue.')
+ .setTitle('Dashboard abgelaufen')
+ .setDescription('Dieses Dashboard wurde wegen Inaktivität geschlossen. Führe den Command erneut aus, um weiterzumachen.')
.setColor(getColor('error'));
await InteractionHelper.safeEditReply(interaction, {
embeds: [timeoutEmbed],
@@ -290,54 +293,54 @@ export default {
flags: MessageFlags.Ephemeral,
});
} catch (error) {
- logger.debug('Could not update dashboard on timeout:', error.message);
+ logger.debug('Could not update dashboard on timeout:', (error as Error).message);
}
}
});
} catch (error) {
- if (error instanceof TitanBotError) throw error;
+ if (error instanceof NexoBotError) throw error;
logger.error('Unexpected error in autoverify_dashboard:', error);
- throw new TitanBotError(
- `Auto-verification dashboard failed: ${error.message}`,
+ throw new NexoBotError(
+ `Auto-verification dashboard failed: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'Failed to open the auto-verification dashboard.',
+ 'Das AutoVerify-Dashboard konnte nicht geöffnet werden.',
);
}
},
-};
+} satisfies SubcommandModule & { prefixOnly: boolean };
-async function handleCriteria(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleCriteria(selectInteraction: ButtonInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
if (!selectInteraction.deferred) {
await selectInteraction.deferUpdate().catch(() => null);
}
const criteriaEmbed = new EmbedBuilder()
- .setTitle('Select Verification Criteria')
- .setDescription('Choose the criteria for automatic verification')
+ .setTitle('Verifizierungskriterien wählen')
+ .setDescription('Wähle die Kriterien für die automatische Verifizierung')
.setColor(getColor('info'));
const criteriaMenu = new StringSelectMenuBuilder()
.setCustomId('autoverify_criteria_select')
- .setPlaceholder('Select criteria...')
+ .setPlaceholder('Wähle Kriterien...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel(`Account Age (older than ${defaultAccountAgeDays} days)`)
- .setDescription('Users with older accounts will be auto-verified')
+ .setLabel(`Account-Alter (älter als ${defaultAccountAgeDays} Tage)`)
+ .setDescription('Nutzer mit älteren Accounts werden automatisch verifiziert')
.setValue('account_age'),
new StringSelectMenuOptionBuilder()
- .setLabel('No Criteria (verify everyone)')
- .setDescription('All users gain the role immediately')
+ .setLabel('Keine Kriterien (alle verifizieren)')
+ .setDescription('Alle Nutzer erhalten die Rolle sofort')
.setValue('none'),
);
await selectInteraction.followUp({
embeds: [criteriaEmbed],
- components: [new ActionRowBuilder().addComponents(criteriaMenu)],
+ components: [new ActionRowBuilder().addComponents(criteriaMenu)],
flags: MessageFlags.Ephemeral,
});
- const criteriaCollector = rootInteraction.channel.createMessageComponentCollector({
+ const criteriaCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'autoverify_criteria_select',
@@ -349,12 +352,12 @@ async function handleCriteria(selectInteraction, rootInteraction, guildConfig, g
await criteriaInteraction.deferUpdate();
const newCriteria = criteriaInteraction.values[0];
- guildConfig.verification.autoVerify.criteria = newCriteria;
+ guildConfig.verification!.autoVerify!.criteria = newCriteria as AutoVerifyConfig['criteria'];
if (newCriteria !== 'account_age') {
- guildConfig.verification.autoVerify.accountAgeDays = null;
- } else if (!guildConfig.verification.autoVerify.accountAgeDays) {
- guildConfig.verification.autoVerify.accountAgeDays = defaultAccountAgeDays;
+ guildConfig.verification!.autoVerify!.accountAgeDays = null;
+ } else if (!guildConfig.verification!.autoVerify!.accountAgeDays) {
+ guildConfig.verification!.autoVerify!.accountAgeDays = defaultAccountAgeDays;
}
await setGuildConfig(client, guildId, guildConfig);
@@ -362,15 +365,15 @@ async function handleCriteria(selectInteraction, rootInteraction, guildConfig, g
let criteriaDisplay = '';
switch (newCriteria) {
case 'account_age':
- criteriaDisplay = `Account Age (${guildConfig.verification.autoVerify.accountAgeDays} days)`;
+ criteriaDisplay = `Account-Alter (${guildConfig.verification!.autoVerify!.accountAgeDays} Tage)`;
break;
case 'none':
- criteriaDisplay = 'No Criteria';
+ criteriaDisplay = 'Keine Kriterien';
break;
}
await criteriaInteraction.followUp({
- embeds: [successEmbed('Criteria Updated', `Auto-verification criteria changed to **${criteriaDisplay}**.`)],
+ embeds: [successEmbed('Kriterien aktualisiert', `AutoVerify-Kriterien geändert zu **${criteriaDisplay}**.`)],
flags: MessageFlags.Ephemeral,
});
@@ -381,32 +384,32 @@ async function handleCriteria(selectInteraction, rootInteraction, guildConfig, g
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No criteria selected. The setting was not changed.',
+ message: 'Es wurden keine Kriterien ausgewählt. Die Einstellung wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleRole(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleRole(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('autoverify_role_select')
- .setPlaceholder('Select a role...')
+ .setPlaceholder('Wähle eine Rolle...')
.setMaxValues(1);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Auto-Verification Role')
- .setDescription('Select the role to assign to auto-verified users.')
+ .setTitle('AutoVerify-Rolle')
+ .setDescription('Wähle die Rolle, die automatisch verifizierte Nutzer erhalten.')
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(roleSelect)],
+ components: [new ActionRowBuilder().addComponents(roleSelect)],
flags: MessageFlags.Ephemeral,
});
- const roleCollector = rootInteraction.channel.createMessageComponentCollector({
+ const roleCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.RoleSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'autoverify_role_select',
@@ -416,30 +419,30 @@ async function handleRole(selectInteraction, rootInteraction, guildConfig, guild
roleCollector.on('collect', async roleInteraction => {
await roleInteraction.deferUpdate();
- const role = roleInteraction.roles.first();
+ const role = roleInteraction.roles.first()!;
- if (role.id === rootInteraction.guild.id || role.managed) {
+ if (role.id === rootInteraction.guild!.id || role.managed) {
await replyUserError(roleInteraction, {
type: ErrorTypes.VALIDATION,
- message: 'Please choose a normal assignable role (not @everyone or a bot-managed role).',
+ message: 'Bitte wähle eine normale, vergebbare Rolle (nicht @everyone und keine von einem Bot verwaltete Rolle).',
});
return;
}
- const botMember = rootInteraction.guild.members.me;
- if (role.position >= botMember.roles.highest.position) {
+ const botMember = rootInteraction.guild!.members.me;
+ if (role.position >= botMember!.roles.highest.position) {
await replyUserError(roleInteraction, {
type: ErrorTypes.PERMISSION,
- message: 'The selected role must be below my highest role in the server role hierarchy.',
+ message: 'Die gewählte Rolle muss in der Rollenhierarchie unter meiner höchsten Rolle liegen.',
});
return;
}
- guildConfig.verification.autoVerify.roleId = role.id;
+ guildConfig.verification!.autoVerify!.roleId = role.id;
await setGuildConfig(client, guildId, guildConfig);
await roleInteraction.followUp({
- embeds: [successEmbed('Role Updated', `Auto-verification role set to ${role}.`)],
+ embeds: [successEmbed('Rolle aktualisiert', `AutoVerify-Rolle auf ${role} gesetzt.`)],
flags: MessageFlags.Ephemeral,
});
@@ -450,24 +453,24 @@ async function handleRole(selectInteraction, rootInteraction, guildConfig, guild
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No role was selected. The setting was not changed.',
+ message: 'Es wurde keine Rolle ausgewählt. Die Einstellung wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleAccountAge(selectInteraction, rootInteraction, guildConfig, guildId, client) {
+async function handleAccountAge(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, guildConfig: GuildConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('autoverify_account_age_modal')
- .setTitle('Set Account Age Requirement')
+ .setTitle('Mindestalter des Accounts festlegen')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('age_input')
- .setLabel('Minimum Account Age (days)')
+ .setLabel('Mindestalter des Accounts (Tage)')
.setStyle(TextInputStyle.Short)
- .setPlaceholder(`Between ${minAccountAgeDays} and ${maxAccountAgeDays}`)
- .setValue((guildConfig.verification.autoVerify.accountAgeDays || defaultAccountAgeDays).toString())
+ .setPlaceholder(`Zwischen ${minAccountAgeDays} und ${maxAccountAgeDays}`)
+ .setValue((guildConfig.verification!.autoVerify!.accountAgeDays || defaultAccountAgeDays).toString())
.setRequired(true),
),
);
@@ -488,15 +491,15 @@ async function handleAccountAge(selectInteraction, rootInteraction, guildConfig,
const days = parseInt(inputValue, 10);
if (isNaN(days) || days < minAccountAgeDays || days > maxAccountAgeDays) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: `Please enter a number between ${minAccountAgeDays} and ${maxAccountAgeDays}.` });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: `Bitte gib eine Zahl zwischen ${minAccountAgeDays} und ${maxAccountAgeDays} ein.` });
return;
}
- guildConfig.verification.autoVerify.accountAgeDays = days;
+ guildConfig.verification!.autoVerify!.accountAgeDays = days;
await setGuildConfig(client, guildId, guildConfig);
await submitted.reply({
- embeds: [successEmbed('Account Age Updated', `Minimum account age requirement set to **${days} days**.`)],
+ embeds: [successEmbed('Account-Alter aktualisiert', `Mindestalter des Accounts auf **${days} Tage** gesetzt.`)],
flags: MessageFlags.Ephemeral,
});
diff --git a/src/commands/Verification/verification.js b/src/commands/Verification/modules/verification.ts
similarity index 67%
rename from src/commands/Verification/verification.js
rename to src/commands/Verification/modules/verification.ts
index 8906809fe7..bf5f32d224 100644
--- a/src/commands/Verification/verification.js
+++ b/src/commands/Verification/modules/verification.ts
@@ -1,46 +1,48 @@
-import { botConfig, getColor } from '../../config/bot.js';
+import { botConfig, getColor } from '../../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js';
-import { createEmbed, infoEmbed, successEmbed } from '../../utils/embeds.js';
-import { getGuildConfig, setGuildConfig } from '../../services/config/guildConfig.js';
-import { withErrorHandling, createError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
-import { removeVerification, verifyUser } from '../../services/verificationService.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { getWelcomeConfig } from '../../utils/database.js';
-import verificationDashboard from './modules/verification_dashboard.js';
+import type { ChatInputCommandInteraction, Guild, TextChannel } from 'discord.js';
+import { createEmbed, infoEmbed, successEmbed } from '../../../utils/embeds.js';
+import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js';
+import { withErrorHandling, createError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { removeVerification, verifyUser } from '../../../services/verificationService.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { getWelcomeConfig } from '../../../utils/database.js';
+import verificationDashboard from './verification_dashboard.js';
+import type { Command, NexoClient } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName("verification")
- .setDescription("Manage the server verification system")
+ .setDescription('Das Verifizierungssystem des Servers verwalten')
.addSubcommand(subcommand =>
subcommand
.setName("setup")
- .setDescription("Set up the verification system")
+ .setDescription('Das Verifizierungssystem einrichten')
.addChannelOption(option =>
option
.setName("verification_channel")
- .setDescription("Channel where verification messages will be sent")
+ .setDescription('Kanal, in dem die Verifizierungsnachricht gesendet wird')
.addChannelTypes(ChannelType.GuildText)
.setRequired(true)
)
.addRoleOption(option =>
option
.setName("verified_role")
- .setDescription("Role to give to verified users")
+ .setDescription('Rolle, die verifizierte Nutzer erhalten')
.setRequired(true)
)
.addStringOption(option =>
option
.setName("message")
- .setDescription("Custom verification message")
+ .setDescription('Eigene Verifizierungsnachricht')
.setMaxLength(2000)
.setRequired(false)
)
.addStringOption(option =>
option
.setName("button_text")
- .setDescription("Text for the verification button")
+ .setDescription('Text für den Verifizierungs-Button')
.setMaxLength(80)
.setRequired(false)
)
@@ -48,30 +50,30 @@ export default {
.addSubcommand(subcommand =>
subcommand
.setName("remove")
- .setDescription("Remove verification from a user")
+ .setDescription('Die Verifizierung eines Nutzers entfernen')
.addUserOption(option =>
option
.setName("user")
- .setDescription("User to remove verification from")
+ .setDescription('Nutzer, dessen Verifizierung entfernt wird')
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("dashboard")
- .setDescription("Open the verification system configuration dashboard")
+ .setDescription('Das Dashboard für die Verifizierung öffnen')
),
async execute(interaction, config, client) {
- const wrappedExecute = withErrorHandling(async () => {
+ const wrappedExecute = withErrorHandling(async (..._args: unknown[]) => {
const subcommand = interaction.options.getSubcommand();
- const guild = interaction.guild;
+ const guild = interaction.guild!;
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
throw createError(
'Missing ManageGuild permission for verification admin subcommand',
ErrorTypes.PERMISSION,
- 'You need the **Manage Server** permission to use this verification subcommand.',
+ 'Du brauchst die Berechtigung **Server verwalten**, um diesen Verifizierungs-Subcommand zu nutzen.',
{ subcommand, requiredPermission: 'ManageGuild', userId: interaction.user.id }
);
}
@@ -87,7 +89,7 @@ export default {
throw createError(
`Unknown subcommand: ${subcommand}`,
ErrorTypes.VALIDATION,
- "Please select a valid subcommand.",
+ 'Bitte wähle einen gültigen Subcommand.',
{ subcommand }
);
}
@@ -95,11 +97,11 @@ export default {
return await wrappedExecute(interaction, config, client);
}
-};
+} satisfies Command;
-async function handleSetup(interaction, guild, client) {
- const verificationChannel = interaction.options.getChannel("verification_channel");
- const verifiedRole = interaction.options.getRole("verified_role");
+async function handleSetup(interaction: ChatInputCommandInteraction, guild: Guild, client: NexoClient) {
+ const verificationChannel = interaction.options.getChannel("verification_channel") as TextChannel;
+ const verifiedRole = interaction.options.getRole("verified_role")!;
const message = interaction.options.getString("message") || botConfig.verification.defaultMessage;
const buttonText = interaction.options.getString("button_text") || botConfig.verification.defaultButtonText;
const botMember = guild.members.me;
@@ -108,7 +110,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
'Bot member not found in guild cache',
ErrorTypes.CONFIGURATION,
- 'I could not verify my permissions in this server. Please try again in a moment.',
+ 'Ich konnte meine Berechtigungen auf diesem Server nicht prüfen. Bitte versuch es gleich noch einmal.',
{ guildId: guild.id }
);
}
@@ -126,7 +128,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
`Missing channel permissions: ${missingChannelPerms.join(', ')}`,
ErrorTypes.PERMISSION,
- 'I need **View Channel**, **Send Messages**, and **Embed Links** in the verification channel.',
+ 'Ich brauche **Kanal ansehen**, **Nachrichten senden** und **Links einbetten** im Verifizierungskanal.',
{ missingPermissions: missingChannelPerms, channel: verificationChannel.id }
);
}
@@ -135,7 +137,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
"Missing ManageRoles permission",
ErrorTypes.PERMISSION,
- "I need the 'Manage Roles' permission to give verified roles.",
+ 'Ich brauche die Berechtigung „Rollen verwalten“, um die Verifizierungsrolle zu vergeben.',
{ missingPermission: "ManageRoles" }
);
}
@@ -144,7 +146,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
'Invalid verified role selected',
ErrorTypes.VALIDATION,
- 'Please choose a normal assignable role (not @everyone or an integration-managed role).',
+ 'Bitte wähle eine normale, vergebbare Rolle (nicht @everyone und keine von einer Integration verwaltete Rolle).',
{ roleId: verifiedRole.id, managed: verifiedRole.managed }
);
}
@@ -154,7 +156,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
"Role hierarchy error",
ErrorTypes.PERMISSION,
- "The verified role must be below my highest role in the server role hierarchy.",
+ 'Die Verifizierungsrolle muss in der Rollenhierarchie unter meiner höchsten Rolle liegen.',
{ rolePosition: verifiedRole.position, botRolePosition: botRole.position }
);
}
@@ -168,7 +170,7 @@ async function handleSetup(interaction, guild, client) {
throw createError(
'Verification setup blocked by conflicting onboarding system',
ErrorTypes.CONFIGURATION,
- 'You cannot enable the verification system while **AutoVerify** or **AutoRole** is configured. Disable those first.',
+ 'Du kannst das Verifizierungssystem nicht aktivieren, solange **AutoVerify** oder eine **Auto-Rolle** eingerichtet ist. Deaktiviere diese zuerst.',
{
guildId: guild.id,
hasAutoVerifyEnabled,
@@ -182,12 +184,12 @@ async function handleSetup(interaction, guild, client) {
await InteractionHelper.safeDefer(interaction);
const verifyEmbed = createEmbed({
- title: "Server Verification",
+ title: 'Server-Verifizierung',
description: message,
color: getColor('success')
});
- const verifyButton = new ActionRowBuilder().addComponents(
+ const verifyButton = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("verify_user")
.setLabel(buttonText)
@@ -213,18 +215,18 @@ async function handleSetup(interaction, guild, client) {
await InteractionHelper.safeEditReply(interaction, {
embeds: [successEmbed(
- 'Verification System Updated',
+ 'Verifizierungssystem aktualisiert',
[
- `Channel: ${verificationChannel}`,
- `Verified Role: ${verifiedRole}`,
- `Button Text: ${buttonText}`
+ `Kanal: ${verificationChannel}`,
+ `Verifizierungsrolle: ${verifiedRole}`,
+ `Button-Text: ${buttonText}`
].join('\n')
)]
});
}
-async function handleRemove(interaction, guild, client) {
- const targetUser = interaction.options.getUser("user");
+async function handleRemove(interaction: ChatInputCommandInteraction, guild: Guild, client: NexoClient) {
+ const targetUser = interaction.options.getUser("user")!;
const result = await removeVerification(client, guild.id, targetUser.id, {
moderatorId: interaction.user.id,
@@ -233,7 +235,7 @@ async function handleRemove(interaction, guild, client) {
if (result.status === 'not_verified') {
return await InteractionHelper.safeReply(interaction, {
- embeds: [infoEmbed('Not Verified', `${targetUser.tag} does not currently have the verified role.`)],
+ embeds: [infoEmbed('Nicht verifiziert', `${targetUser.tag} hat die Verifizierungsrolle derzeit nicht.`)],
flags: MessageFlags.Ephemeral
});
}
@@ -245,6 +247,6 @@ async function handleRemove(interaction, guild, client) {
});
return await InteractionHelper.safeReply(interaction, {
- embeds: [successEmbed('Verification Removed', `Verification removed from ${targetUser.tag}.`)]
+ embeds: [successEmbed('Verifizierung entfernt', `Die Verifizierung von ${targetUser.tag} wurde entfernt.`)]
});
}
\ No newline at end of file
diff --git a/src/commands/Verification/modules/verification_dashboard.js b/src/commands/Verification/modules/verification_dashboard.ts
similarity index 65%
rename from src/commands/Verification/modules/verification_dashboard.js
rename to src/commands/Verification/modules/verification_dashboard.ts
index ab32689527..c1f6630533 100644
--- a/src/commands/Verification/modules/verification_dashboard.js
+++ b/src/commands/Verification/modules/verification_dashboard.ts
@@ -15,10 +15,11 @@ import {
ComponentType,
EmbedBuilder,
} from 'discord.js';
+import type { ChatInputCommandInteraction, Guild, StringSelectMenuInteraction, TextChannel } from 'discord.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { successEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js';
import { getWelcomeConfig } from '../../../utils/database.js';
import { botHasPermission } from '../../../utils/permissionGuard.js';
@@ -27,21 +28,24 @@ import {
formatPanelStatusField,
} from '../../../utils/panelStatus.js';
import { startDashboardSession } from '../../../utils/dashboardSession.js';
+import type { PanelStatus } from '../../../utils/panelStatus.js';
+import type { VerificationConfig } from '../../../services/verificationService.js';
+import type { NexoClient, SubcommandModule } from '../../../types/index.js';
-async function updateLivePanel(guild, cfg) {
+async function updateLivePanel(guild: Guild, cfg: VerificationConfig) {
if (!cfg.channelId || !cfg.messageId) return;
try {
- const channel = guild.channels.cache.get(cfg.channelId);
+ const channel = guild.channels.cache.get(cfg.channelId) as TextChannel | undefined;
if (!channel) return;
const msg = await channel.messages.fetch(cfg.messageId).catch(() => null);
if (!msg) return;
const verifyEmbed = new EmbedBuilder()
- .setTitle('Server Verification')
+ .setTitle('Server-Verifizierung')
.setDescription(cfg.message || botConfig.verification.defaultMessage)
.setColor(getColor('success'));
- const verifyButton = new ActionRowBuilder().addComponents(
+ const verifyButton = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('verify_user')
.setLabel(cfg.buttonText || botConfig.verification.defaultButtonText)
@@ -51,82 +55,82 @@ async function updateLivePanel(guild, cfg) {
await msg.edit({ embeds: [verifyEmbed], components: [verifyButton] });
} catch (error) {
- logger.warn('Could not update live verification panel:', error.message);
+ logger.warn('Could not update live verification panel:', (error as Error).message);
}
}
-function buildDashboardEmbed(cfg, guild, verifiedUserCount = 0, conflictSummary = '', panelStatus = null) {
- const channel = cfg.channelId ? `<#${cfg.channelId}>` : '`Not set`';
- const role = cfg.roleId ? `<@&${cfg.roleId}>` : '`Not set`';
+function buildDashboardEmbed(cfg: VerificationConfig, guild: Guild, verifiedUserCount = 0, conflictSummary = '', panelStatus: PanelStatus | null = null) {
+ const channel = cfg.channelId ? `<#${cfg.channelId}>` : '`Nicht gesetzt`';
+ const role = cfg.roleId ? `<@&${cfg.roleId}>` : '`Nicht gesetzt`';
const rawMsg = cfg.message || botConfig.verification.defaultMessage;
const msgPreview = `\`${rawMsg.length > 60 ? rawMsg.substring(0, 60) + '…' : rawMsg}\``;
const buttonText = cfg.buttonText || botConfig.verification.defaultButtonText;
- const panelStatusValue = cfg.channelId ? formatPanelStatusField(panelStatus) : '`Not configured`';
+ const panelStatusValue = cfg.channelId ? formatPanelStatusField(panelStatus) : '`Nicht eingerichtet`';
const embed = new EmbedBuilder()
- .setTitle('✅ Verification System Dashboard')
- .setDescription(`Manage verification settings for **${guild.name}**.\nSelect an option below to modify a setting.`)
+ .setTitle('✅ Verifizierungs-Dashboard')
+ .setDescription(`Verwalte die Verifizierungseinstellungen für **${guild.name}**.\nWähle unten eine Option, um eine Einstellung zu ändern.`)
.setColor(getColor('info'))
.addFields(
- { name: 'Panel Status', value: panelStatusValue, inline: false },
- { name: 'Verification Channel', value: channel, inline: true },
- { name: 'Verified Role', value: role, inline: true },
- { name: 'System Status', value: cfg.enabled !== false ? 'Enabled' : 'Disabled', inline: true },
- { name: 'Button Text', value: `\`${buttonText}\``, inline: true },
- { name: 'Verified Users', value: `${verifiedUserCount} users`, inline: true },
+ { name: 'Panel-Status', value: panelStatusValue, inline: false },
+ { name: 'Verifizierungskanal', value: channel, inline: true },
+ { name: 'Verifizierungsrolle', value: role, inline: true },
+ { name: 'Systemstatus', value: cfg.enabled !== false ? 'Aktiviert' : 'Deaktiviert', inline: true },
+ { name: 'Button-Text', value: `\`${buttonText}\``, inline: true },
+ { name: 'Verifizierte Nutzer', value: `${verifiedUserCount} Nutzer`, inline: true },
{ name: '\u200B', value: '\u200B', inline: true },
- { name: 'Verification Message', value: msgPreview, inline: false },
+ { name: 'Verifizierungsnachricht', value: msgPreview, inline: false },
);
if (conflictSummary) {
- embed.addFields({ name: 'Setup Conflicts', value: conflictSummary, inline: false });
+ embed.addFields({ name: 'Konflikte bei der Einrichtung', value: conflictSummary, inline: false });
}
return embed
- .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität' })
.setTimestamp();
}
-function buildSelectMenu(guildId) {
+function buildSelectMenu(guildId: string) {
return new StringSelectMenuBuilder()
.setCustomId(`verif_cfg_${guildId}`)
- .setPlaceholder('Select a setting to configure...')
+ .setPlaceholder('Wähle eine Einstellung...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Change Verification Channel')
- .setDescription('Set the channel where the verification panel is posted')
+ .setLabel('Verifizierungskanal ändern')
+ .setDescription('Lege fest, in welchem Kanal das Verifizierungs-Panel gepostet wird')
.setValue('channel')
.setEmoji('📢'),
new StringSelectMenuOptionBuilder()
- .setLabel('Change Verified Role')
- .setDescription('Set the role assigned when a user verifies')
+ .setLabel('Verifizierungsrolle ändern')
+ .setDescription('Lege die Rolle fest, die ein Nutzer bei der Verifizierung erhält')
.setValue('role')
.setEmoji('🏷️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Edit Verification Message')
- .setDescription('Customise the message shown on the verification panel embed')
+ .setLabel('Verifizierungsnachricht bearbeiten')
+ .setDescription('Passe die Nachricht im Embed des Verifizierungs-Panels an')
.setValue('message')
.setEmoji('💬'),
new StringSelectMenuOptionBuilder()
- .setLabel('Edit Button Text')
- .setDescription('Change the label on the verify button')
+ .setLabel('Button-Text bearbeiten')
+ .setDescription('Ändere die Beschriftung des Verifizierungs-Buttons')
.setValue('button_text')
.setEmoji('🔘'),
);
}
-function buildButtonRow(cfg, guildId, disabled = false, panelStatus = null) {
+function buildButtonRow(cfg: VerificationConfig, guildId: string, disabled = false, panelStatus: PanelStatus | null = null) {
const systemOn = cfg.enabled !== false;
const showRepost =
systemOn && panelStatus?.exists === false && panelStatus?.reason === 'panel_deleted';
- const buttons = [];
+ const buttons: ButtonBuilder[] = [];
if (showRepost) {
buttons.push(
new ButtonBuilder()
.setCustomId(`verif_cfg_repost_${guildId}`)
- .setLabel('Repost Panel')
+ .setLabel('Panel neu posten')
.setStyle(ButtonStyle.Primary)
.setEmoji('📌')
.setDisabled(disabled),
@@ -136,31 +140,31 @@ function buildButtonRow(cfg, guildId, disabled = false, panelStatus = null) {
buttons.push(
new ButtonBuilder()
.setCustomId(`verif_cfg_toggle_${guildId}`)
- .setLabel('Verification')
+ .setLabel('Verifizierung')
.setStyle(systemOn ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji('🔒')
.setDisabled(disabled),
);
- return new ActionRowBuilder().addComponents(buttons);
+ return new ActionRowBuilder().addComponents(buttons);
}
-async function repostVerificationPanel(guild, cfg) {
- const channel = await guild.channels.fetch(cfg.channelId).catch(() => null);
+async function repostVerificationPanel(guild: Guild, cfg: VerificationConfig) {
+ const channel = (await guild.channels.fetch(cfg.channelId!).catch(() => null)) as TextChannel | null;
if (!channel) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Panel channel missing',
ErrorTypes.CONFIGURATION,
- 'The configured verification channel no longer exists. Set a new channel from the dashboard.',
+ 'Der eingerichtete Verifizierungskanal existiert nicht mehr. Lege im Dashboard einen neuen Kanal fest.',
);
}
const verifyEmbed = new EmbedBuilder()
- .setTitle('Server Verification')
+ .setTitle('Server-Verifizierung')
.setDescription(cfg.message || botConfig.verification.defaultMessage)
.setColor(getColor('success'));
- const verifyButton = new ActionRowBuilder().addComponents(
+ const verifyButton = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('verify_user')
.setLabel(cfg.buttonText || botConfig.verification.defaultButtonText)
@@ -171,16 +175,16 @@ async function repostVerificationPanel(guild, cfg) {
return channel.send({ embeds: [verifyEmbed], components: [verifyButton] });
}
-async function refreshDashboard(rootInteraction, cfg, guildId, client) {
+async function refreshDashboard(rootInteraction: ChatInputCommandInteraction, cfg: VerificationConfig, guildId: string, client: NexoClient) {
try {
const selectMenu = buildSelectMenu(guildId);
let verifiedUserCount = 0;
let conflictSummary = '';
- let panelStatus = null;
+ let panelStatus: PanelStatus | null = null;
if (cfg.channelId && cfg.enabled !== false) {
- panelStatus = await getVerificationPanelStatus(client, rootInteraction.guild, cfg);
+ panelStatus = await getVerificationPanelStatus(client, rootInteraction.guild!, cfg);
if (panelStatus.recoveredId) {
cfg.messageId = panelStatus.recoveredId;
const latestConfig = await getGuildConfig(client, guildId);
@@ -190,7 +194,7 @@ async function refreshDashboard(rootInteraction, cfg, guildId, client) {
}
try {
- const verifiedRole = rootInteraction.guild.roles.cache.get(cfg.roleId);
+ const verifiedRole = rootInteraction.guild!.roles.cache.get(cfg.roleId!);
if (verifiedRole) {
verifiedUserCount = verifiedRole.members.size;
}
@@ -201,27 +205,27 @@ async function refreshDashboard(rootInteraction, cfg, guildId, client) {
const autoRoleConfigured = Boolean(guildConfig.autoRole) || (Array.isArray(welcomeConfig.roleIds) && welcomeConfig.roleIds.length > 0);
const conflicts = [
- autoVerifyEnabled ? 'AutoVerify is enabled' : null,
- autoRoleConfigured ? 'AutoRole is configured' : null
+ autoVerifyEnabled ? 'AutoVerify ist aktiviert' : null,
+ autoRoleConfigured ? 'Eine Auto-Rolle ist eingerichtet' : null
].filter(Boolean);
if (conflicts.length > 0) {
conflictSummary = conflicts.join('\n');
}
} catch (error) {
- logger.warn('Could not fetch verification dashboard details:', error.message);
+ logger.warn('Could not fetch verification dashboard details:', (error as Error).message);
}
await InteractionHelper.safeEditReply(rootInteraction, {
- embeds: [buildDashboardEmbed(cfg, rootInteraction.guild, verifiedUserCount, conflictSummary, panelStatus)],
+ embeds: [buildDashboardEmbed(cfg, rootInteraction.guild!, verifiedUserCount, conflictSummary, panelStatus)],
components: [
buildButtonRow(cfg, guildId, false, panelStatus),
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
flags: MessageFlags.Ephemeral,
});
} catch (error) {
- logger.debug('Could not refresh verification dashboard (interaction may have expired):', error.message);
+ logger.debug('Could not refresh verification dashboard (interaction may have expired):', (error as Error).message);
}
}
@@ -229,15 +233,15 @@ export default {
prefixOnly: false,
async execute(interaction, config, client) {
try {
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
const guildConfig = await getGuildConfig(client, guildId);
const cfg = guildConfig.verification;
if (!cfg?.channelId) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Verification not configured',
ErrorTypes.CONFIGURATION,
- 'The verification system has not been set up yet. Run `/verification setup` first.',
+ 'Das Verifizierungssystem ist noch nicht eingerichtet. Führe zuerst `/verification setup` aus.',
);
}
@@ -245,10 +249,10 @@ export default {
let verifiedUserCount = 0;
let conflictSummary = '';
- let panelStatus = null;
+ let panelStatus: PanelStatus | null = null;
if (cfg.channelId && cfg.enabled !== false) {
- panelStatus = await getVerificationPanelStatus(client, interaction.guild, cfg);
+ panelStatus = await getVerificationPanelStatus(client, interaction.guild!, cfg);
if (panelStatus.recoveredId) {
cfg.messageId = panelStatus.recoveredId;
guildConfig.verification = cfg;
@@ -257,7 +261,7 @@ export default {
}
try {
- const verifiedRole = interaction.guild.roles.cache.get(cfg.roleId);
+ const verifiedRole = interaction.guild!.roles.cache.get(cfg.roleId!);
if (verifiedRole) {
verifiedUserCount = verifiedRole.members.size;
}
@@ -267,23 +271,23 @@ export default {
const autoRoleConfigured = Boolean(guildConfig.autoRole) || (Array.isArray(welcomeConfig.roleIds) && welcomeConfig.roleIds.length > 0);
const conflicts = [
- autoVerifyEnabled ? 'AutoVerify is enabled' : null,
- autoRoleConfigured ? 'AutoRole is configured' : null
+ autoVerifyEnabled ? 'AutoVerify ist aktiviert' : null,
+ autoRoleConfigured ? 'Eine Auto-Rolle ist eingerichtet' : null
].filter(Boolean);
if (conflicts.length > 0) {
conflictSummary = conflicts.join('\n');
}
} catch (error) {
- logger.warn('Could not fetch verification dashboard details:', error.message);
+ logger.warn('Could not fetch verification dashboard details:', (error as Error).message);
}
await startDashboardSession({
interaction,
- embeds: [buildDashboardEmbed(cfg, interaction.guild, verifiedUserCount, conflictSummary, panelStatus)],
+ embeds: [buildDashboardEmbed(cfg, interaction.guild!, verifiedUserCount, conflictSummary, panelStatus)],
components: [
buildButtonRow(cfg, guildId, false, panelStatus),
- new ActionRowBuilder().addComponents(buildSelectMenu(guildId)),
+ new ActionRowBuilder().addComponents(buildSelectMenu(guildId)),
],
flags: MessageFlags.Ephemeral,
selectMenuId: `verif_cfg_${guildId}`,
@@ -309,13 +313,13 @@ export default {
onButton: async (btnInteraction) => {
if (btnInteraction.customId === `verif_cfg_repost_${guildId}`) {
await btnInteraction.deferUpdate();
- const newMsg = await repostVerificationPanel(interaction.guild, cfg);
+ const newMsg = await repostVerificationPanel(interaction.guild!, cfg);
cfg.messageId = newMsg.id;
const latestConfig = await getGuildConfig(client, guildId);
latestConfig.verification = cfg;
await setGuildConfig(client, guildId, latestConfig);
await btnInteraction.followUp({
- embeds: [successEmbed('Panel Reposted', `Verification panel restored in ${newMsg.channel}.`)],
+ embeds: [successEmbed('Panel neu gepostet', `Das Verifizierungs-Panel wurde in ${newMsg.channel} wiederhergestellt.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(interaction, cfg, guildId, client);
@@ -330,7 +334,7 @@ export default {
if (!wasEnabled && autoVerifyEnabled) {
await replyUserError(btnInteraction, {
type: ErrorTypes.CONFIGURATION,
- message: 'AutoVerify is currently enabled. Please disable AutoVerify first before enabling the manual Verification system.\n\nRun `/autoverify` to access the AutoVerify dashboard.',
+ message: 'AutoVerify ist derzeit aktiviert. Deaktiviere zuerst AutoVerify, bevor du die manuelle Verifizierung aktivierst.\n\nMit `/verification autoverify dashboard` öffnest du das AutoVerify-Dashboard.',
});
return;
}
@@ -338,7 +342,7 @@ export default {
cfg.enabled = !wasEnabled;
if (!cfg.enabled && cfg.channelId && cfg.messageId) {
- const channel = interaction.guild.channels.cache.get(cfg.channelId);
+ const channel = interaction.guild!.channels.cache.get(cfg.channelId) as TextChannel | undefined;
if (channel) {
const msg = await channel.messages.fetch(cfg.messageId).catch(() => null);
if (msg) await msg.delete().catch(() => {});
@@ -347,10 +351,10 @@ export default {
if (cfg.enabled && cfg.channelId) {
try {
- const newMsg = await repostVerificationPanel(interaction.guild, cfg);
+ const newMsg = await repostVerificationPanel(interaction.guild!, cfg);
cfg.messageId = newMsg.id;
} catch (error) {
- logger.warn('Could not re-post verification panel on re-enable:', error.message);
+ logger.warn('Could not re-post verification panel on re-enable:', (error as Error).message);
}
}
@@ -361,8 +365,8 @@ export default {
await btnInteraction.followUp({
embeds: [
successEmbed(
- '✅ System Updated',
- `The verification system is now **${cfg.enabled ? 'enabled' : 'disabled'}**.`,
+ '✅ System aktualisiert',
+ `Das Verifizierungssystem ist jetzt **${cfg.enabled ? 'aktiviert' : 'deaktiviert'}**.`,
),
],
flags: MessageFlags.Ephemeral,
@@ -374,8 +378,8 @@ export default {
await InteractionHelper.safeEditReply(rootInteraction, {
embeds: [
new EmbedBuilder()
- .setTitle('Dashboard Timed Out')
- .setDescription('This dashboard has been closed due to inactivity. Please run the command again to continue.')
+ .setTitle('Dashboard abgelaufen')
+ .setDescription('Dieses Dashboard wurde wegen Inaktivität geschlossen. Führe den Command erneut aus, um weiterzumachen.')
.setColor(getColor('error')),
],
components: [],
@@ -384,40 +388,40 @@ export default {
},
});
} catch (error) {
- if (error instanceof TitanBotError) throw error;
+ if (error instanceof NexoBotError) throw error;
logger.error('Unexpected error in verification_dashboard:', error);
- throw new TitanBotError(
- `Verification dashboard failed: ${error.message}`,
+ throw new NexoBotError(
+ `Verification dashboard failed: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'Failed to open the verification dashboard.',
+ 'Das Verifizierungs-Dashboard konnte nicht geöffnet werden.',
);
}
},
-};
+} satisfies SubcommandModule & { prefixOnly: boolean };
-async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleChannel(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: VerificationConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('verif_cfg_channel')
- .setPlaceholder('Select a text channel...')
+ .setPlaceholder('Wähle einen Textkanal...')
.addChannelTypes(ChannelType.GuildText)
.setMaxValues(1);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Change Verification Channel')
+ .setTitle('Verifizierungskanal ändern')
.setDescription(
- `**Current:** ${cfg.channelId ?`<#${cfg.channelId}>`: '`Not set`'}\n\nSelect the channel where the verification panel will be posted.\n\n> ⚠️ The existing panel will be deleted and re-posted in the new channel.`,
+ `**Aktuell:** ${cfg.channelId ?`<#${cfg.channelId}>`: '`Nicht gesetzt`'}\n\nWähle den Kanal, in dem das Verifizierungs-Panel gepostet wird.\n\n> ⚠️ Das bestehende Panel wird gelöscht und im neuen Kanal neu gepostet.`,
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(channelSelect)],
+ components: [new ActionRowBuilder().addComponents(channelSelect)],
flags: MessageFlags.Ephemeral,
});
- const chanCollector = rootInteraction.channel.createMessageComponentCollector({
+ const chanCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'verif_cfg_channel',
@@ -427,18 +431,18 @@ async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, c
chanCollector.on('collect', async chanInteraction => {
await chanInteraction.deferUpdate();
- const newChannel = chanInteraction.channels.first();
+ const newChannel = chanInteraction.channels.first() as TextChannel | undefined;
if (!botHasPermission(newChannel, ['ViewChannel', 'SendMessages', 'EmbedLinks'])) {
await replyUserError(chanInteraction, {
type: ErrorTypes.PERMISSION,
- message: `I need **View Channel**, **Send Messages**, and **Embed Links** permissions in ${newChannel}.`,
+ message: `Ich brauche die Berechtigungen **Kanal ansehen**, **Nachrichten senden** und **Links einbetten** in ${newChannel}.`,
});
return;
}
if (cfg.channelId && cfg.messageId) {
- const oldChannel = rootInteraction.guild.channels.cache.get(cfg.channelId);
+ const oldChannel = rootInteraction.guild!.channels.cache.get(cfg.channelId) as TextChannel | undefined;
if (oldChannel) {
try {
const oldMsg = await oldChannel.messages.fetch(cfg.messageId).catch(() => null);
@@ -452,11 +456,11 @@ async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, c
if (cfg.enabled !== false) {
try {
const verifyEmbed = new EmbedBuilder()
- .setTitle('Server Verification')
+ .setTitle('Server-Verifizierung')
.setDescription(cfg.message || botConfig.verification.defaultMessage)
.setColor(getColor('success'));
- const verifyButton = new ActionRowBuilder().addComponents(
+ const verifyButton = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('verify_user')
.setLabel(cfg.buttonText || botConfig.verification.defaultButtonText)
@@ -464,20 +468,20 @@ async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, c
.setEmoji('✅'),
);
- const newMsg = await newChannel.send({ embeds: [verifyEmbed], components: [verifyButton] });
+ const newMsg = await newChannel!.send({ embeds: [verifyEmbed], components: [verifyButton] });
cfg.messageId = newMsg.id;
} catch (error) {
- logger.warn('Could not post verification panel in new channel:', error.message);
+ logger.warn('Could not post verification panel in new channel:', (error as Error).message);
}
}
- cfg.channelId = newChannel.id;
+ cfg.channelId = newChannel!.id;
const latestConfig = await getGuildConfig(client, guildId);
latestConfig.verification = cfg;
await setGuildConfig(client, guildId, latestConfig);
await chanInteraction.followUp({
- embeds: [successEmbed('Channel Updated', `Verification panel moved to ${newChannel}.`)],
+ embeds: [successEmbed('Kanal aktualisiert', `Das Verifizierungs-Panel wurde nach ${newChannel} verschoben.`)],
flags: MessageFlags.Ephemeral,
});
@@ -488,34 +492,34 @@ async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, c
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No channel was selected. The setting was not changed.',
+ message: 'Es wurde kein Kanal ausgewählt. Die Einstellung wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleRole(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleRole(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: VerificationConfig, guildId: string, client: NexoClient) {
await selectInteraction.deferUpdate();
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('verif_cfg_role')
- .setPlaceholder('Select a role...')
+ .setPlaceholder('Wähle eine Rolle...')
.setMaxValues(1);
await selectInteraction.followUp({
embeds: [
new EmbedBuilder()
- .setTitle('Change Verified Role')
+ .setTitle('Verifizierungsrolle ändern')
.setDescription(
- `**Current:** ${cfg.roleId ?`<@&${cfg.roleId}>`: '`Not set`'}\n\nSelect the role to assign when a user verifies.`,
+ `**Aktuell:** ${cfg.roleId ?`<@&${cfg.roleId}>`: '`Nicht gesetzt`'}\n\nWähle die Rolle, die ein Nutzer bei der Verifizierung erhält.`,
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(roleSelect)],
+ components: [new ActionRowBuilder().addComponents(roleSelect)],
flags: MessageFlags.Ephemeral,
});
- const roleCollector = rootInteraction.channel.createMessageComponentCollector({
+ const roleCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.RoleSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'verif_cfg_role',
@@ -525,14 +529,14 @@ async function handleRole(selectInteraction, rootInteraction, cfg, guildId, clie
roleCollector.on('collect', async roleInteraction => {
await roleInteraction.deferUpdate();
- const role = roleInteraction.roles.first();
- const guild = rootInteraction.guild;
- const botMember = guild.members.me;
+ const role = roleInteraction.roles.first()!;
+ const guild = rootInteraction.guild!;
+ const botMember = guild.members.me!;
if (role.id === guild.id || role.managed) {
await replyUserError(roleInteraction, {
type: ErrorTypes.VALIDATION,
- message: 'Please choose a normal assignable role (not @everyone or a bot-managed role).',
+ message: 'Bitte wähle eine normale, vergebbare Rolle (nicht @everyone und keine von einem Bot verwaltete Rolle).',
});
return;
}
@@ -540,7 +544,7 @@ async function handleRole(selectInteraction, rootInteraction, cfg, guildId, clie
if (role.position >= botMember.roles.highest.position) {
await replyUserError(roleInteraction, {
type: ErrorTypes.PERMISSION,
- message: 'The verified role must be below my highest role in the server role hierarchy.',
+ message: 'Die Verifizierungsrolle muss in der Rollenhierarchie unter meiner höchsten Rolle liegen.',
});
return;
}
@@ -551,7 +555,7 @@ async function handleRole(selectInteraction, rootInteraction, cfg, guildId, clie
await setGuildConfig(client, guildId, latestConfig);
await roleInteraction.followUp({
- embeds: [successEmbed('Role Updated', `Verified role set to ${role}.`)],
+ embeds: [successEmbed('Rolle aktualisiert', `Verifizierungsrolle auf ${role} gesetzt.`)],
flags: MessageFlags.Ephemeral,
});
@@ -562,22 +566,22 @@ async function handleRole(selectInteraction, rootInteraction, cfg, guildId, clie
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No role was selected. The setting was not changed.',
+ message: 'Es wurde keine Rolle ausgewählt. Die Einstellung wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleMessage(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleMessage(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: VerificationConfig, guildId: string, client: NexoClient) {
try {
const modal = new ModalBuilder()
.setCustomId('verif_cfg_message')
- .setTitle('Edit Verification Message')
+ .setTitle('Verifizierungsnachricht bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('message_input')
- .setLabel('Message shown on the verification panel embed')
+ .setLabel('Nachricht im Verifizierungs-Panel')
.setStyle(TextInputStyle.Paragraph)
.setValue(cfg.message || botConfig.verification.defaultMessage)
.setMaxLength(2000)
@@ -604,10 +608,10 @@ async function handleMessage(selectInteraction, rootInteraction, cfg, guildId, c
latestConfig.verification = cfg;
await setGuildConfig(client, guildId, latestConfig);
- await updateLivePanel(rootInteraction.guild, cfg);
+ await updateLivePanel(rootInteraction.guild!, cfg);
await submitted.reply({
- embeds: [successEmbed('Message Updated', 'The verification panel has been updated with the new message.')],
+ embeds: [successEmbed('Nachricht aktualisiert', 'Das Verifizierungs-Panel zeigt jetzt die neue Nachricht.')],
flags: MessageFlags.Ephemeral,
});
@@ -618,16 +622,16 @@ async function handleMessage(selectInteraction, rootInteraction, cfg, guildId, c
}
}
-async function handleButtonText(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleButtonText(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: VerificationConfig, guildId: string, client: NexoClient) {
try {
const modal = new ModalBuilder()
.setCustomId('verif_cfg_button_text')
- .setTitle('Edit Button Text')
+ .setTitle('Button-Text bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('button_text_input')
- .setLabel('Button label (max 80 characters)')
+ .setLabel('Button-Beschriftung (max. 80 Zeichen)')
.setStyle(TextInputStyle.Short)
.setValue(cfg.buttonText || botConfig.verification.defaultButtonText)
.setMaxLength(80)
@@ -654,10 +658,10 @@ async function handleButtonText(selectInteraction, rootInteraction, cfg, guildId
latestConfig.verification = cfg;
await setGuildConfig(client, guildId, latestConfig);
- await updateLivePanel(rootInteraction.guild, cfg);
+ await updateLivePanel(rootInteraction.guild!, cfg);
await submitted.reply({
- embeds: [successEmbed('Button Text Updated', `The verify button now reads **${cfg.buttonText}**.`)],
+ embeds: [successEmbed('Button-Text aktualisiert', `Der Verifizierungs-Button zeigt jetzt **${cfg.buttonText}**.`)],
flags: MessageFlags.Ephemeral,
});
diff --git a/src/commands/Verification/verification.ts b/src/commands/Verification/verification.ts
new file mode 100644
index 0000000000..215e0668f9
--- /dev/null
+++ b/src/commands/Verification/verification.ts
@@ -0,0 +1,13 @@
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import verificationCommand from './modules/verification.js';
+import autoverifyCommand from './modules/autoVerify.js';
+
+export default createRouterCommand({
+ name: 'verification',
+ description: 'Verifizierung von Mitgliedern verwalten',
+ category: 'Verification',
+ routes: [
+ { legacyName: 'verification', module: verificationCommand, spread: true },
+ { legacyName: 'autoverify', module: autoverifyCommand },
+ ],
+});
diff --git a/src/commands/Verification/verify.js b/src/commands/Verification/verify.ts
similarity index 68%
rename from src/commands/Verification/verify.js
rename to src/commands/Verification/verify.ts
index 7c203ac8d6..645854f14d 100644
--- a/src/commands/Verification/verify.js
+++ b/src/commands/Verification/verify.ts
@@ -3,14 +3,15 @@ import { infoEmbed, successEmbed } from '../../utils/embeds.js';
import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js';
import { verifyUser } from '../../services/verificationService.js';
import { InteractionHelper } from '../../utils/interactionHelper.js';
+import type { Command } from '../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('verify')
- .setDescription('Verify yourself and gain access to the server'),
+ .setDescription('Verifiziere dich und erhalte Zugang zum Server'),
async execute(interaction, config, client) {
- const guild = interaction.guild;
+ const guild = interaction.guild!;
const result = await verifyUser(client, guild.id, interaction.user.id, {
source: 'command_self',
@@ -19,17 +20,17 @@ export default {
if (result.status === 'already_verified') {
return await InteractionHelper.safeReply(interaction, {
- embeds: [infoEmbed('Already Verified', "You are already verified.")],
+ embeds: [infoEmbed('Bereits verifiziert', 'Du bist bereits verifiziert.')],
flags: MessageFlags.Ephemeral
});
}
await InteractionHelper.safeReply(interaction, {
embeds: [successEmbed(
- "Verification Complete",
- `You have been verified and given the **${result.roleName}** role! Welcome to the server! 🎉`
+ 'Verifizierung abgeschlossen',
+ `Du wurdest verifiziert und hast die Rolle **${result.roleName}** erhalten! Willkommen auf dem Server! 🎉`
)],
flags: MessageFlags.Ephemeral
});
}
-};
+} satisfies Command;
diff --git a/src/commands/Welcome/autorole.js b/src/commands/Welcome/modules/autorole.ts
similarity index 68%
rename from src/commands/Welcome/autorole.js
rename to src/commands/Welcome/modules/autorole.ts
index 81764e4c46..52c42b59e8 100644
--- a/src/commands/Welcome/autorole.js
+++ b/src/commands/Welcome/modules/autorole.ts
@@ -1,12 +1,14 @@
-import { getColor } from '../../config/bot.js';
+import { getColor } from '../../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder, MessageFlags } from 'discord.js';
-import { getWelcomeConfig, updateWelcomeConfig } from '../../utils/database.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { getGuildConfig } from '../../services/config/guildConfig.js';
-import { ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
-
-function createAutoroleInfoEmbed(description) {
+import type { ChatInputCommandInteraction, Role } from 'discord.js';
+import { getWelcomeConfig, updateWelcomeConfig } from '../../../utils/database.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { getGuildConfig } from '../../../services/config/guildConfig.js';
+import { ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import type { Command } from '../../../types/index.js';
+
+function createAutoroleInfoEmbed(description: string) {
return new EmbedBuilder()
.setColor(getColor('primary'))
.setDescription(description)
@@ -16,28 +18,28 @@ function createAutoroleInfoEmbed(description) {
export default {
data: new SlashCommandBuilder()
.setName('autorole')
- .setDescription('Manage roles that are automatically assigned to new members')
+ .setDescription('Rollen verwalten, die neuen Mitgliedern automatisch vergeben werden')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand =>
subcommand
.setName('add')
- .setDescription('Add a role to be automatically assigned to new members')
+ .setDescription('Eine Rolle hinzufügen, die neue Mitglieder automatisch erhalten')
.addRoleOption(option =>
option.setName('role')
- .setDescription('The role to add')
+ .setDescription('Die Rolle, die hinzugefügt wird')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('remove')
- .setDescription('Remove a role from auto-assignment')
+ .setDescription('Eine Rolle aus der automatischen Vergabe entfernen')
.addRoleOption(option =>
option.setName('role')
- .setDescription('The role to remove')
+ .setDescription('Die Rolle, die entfernt wird')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('list')
- .setDescription('List all auto-assigned roles')),
+ .setDescription('Alle automatisch vergebenen Rollen anzeigen')),
async execute(interaction) {
const deferSuccess = await InteractionHelper.safeDefer(interaction);
@@ -51,26 +53,26 @@ export default {
}
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the **Manage Server** permission to use `/autorole`.' });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um `/welcome autorole` zu nutzen.' });
}
- const { options, guild, client } = interaction;
+ const { options, guild, client } = interaction as ChatInputCommandInteraction<'cached'>;
const subcommand = options.getSubcommand();
if (subcommand === 'add') {
- const role = options.getRole('role');
+ const role = options.getRole('role')!;
const guildConfig = await getGuildConfig(client, guild.id);
const verificationEnabled = Boolean(guildConfig.verification?.enabled);
const autoVerifyEnabled = Boolean(guildConfig.verification?.autoVerify?.enabled);
if (verificationEnabled || autoVerifyEnabled) {
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You cannot add AutoRole while the verification system or AutoVerify is enabled. Disable those first.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Du kannst keine Auto-Rolle hinzufügen, solange die Verifizierung oder AutoVerify aktiviert ist. Deaktiviere diese zuerst.' });
}
- if (role.position >= guild.members.me.roles.highest.position) {
+ if (role.position >= guild.members.me!.roles.highest.position) {
logger.warn(`[Autorole] User ${interaction.user.tag} tried to add role ${role.name} (${role.id}) higher than bot's highest role in ${guild.name}`);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'I can\'t assign roles that are higher than my highest role.' });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Ich kann keine Rollen vergeben, die höher sind als meine höchste Rolle.' });
}
try {
@@ -80,7 +82,7 @@ export default {
if (currentRoleId === role.id) {
logger.info(`[Autorole] User ${interaction.user.tag} tried to add duplicate role ${role.name} (${role.id}) in ${guild.name}`);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `The role ${role} is already set to be auto-assigned.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Die Rolle ${role} wird bereits automatisch vergeben.` });
}
await updateWelcomeConfig(client, guild.id, {
@@ -91,19 +93,19 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [createAutoroleInfoEmbed(
currentRoleId
- ? `✅ Auto-role updated to ${role}. Only one auto-role is allowed.`
- : `✅ Auto-role set to ${role}.`
+ ? `✅ Auto-Rolle auf ${role} geändert. Es ist nur eine Auto-Rolle erlaubt.`
+ : `✅ Auto-Rolle auf ${role} gesetzt.`
)],
flags: MessageFlags.Ephemeral
});
} catch (error) {
logger.error(`[Autorole] Failed to add role for guild ${guild.id}:`, error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while adding the role. Please try again.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Hinzufügen der Rolle ist ein Fehler aufgetreten. Bitte versuch es erneut.' });
}
}
else if (subcommand === 'remove') {
- const role = options.getRole('role');
+ const role = options.getRole('role')!;
try {
const config = await getWelcomeConfig(client, guild.id);
@@ -111,7 +113,7 @@ export default {
if (!existingRoles.includes(role.id)) {
logger.info(`[Autorole] User ${interaction.user.tag} tried to remove non-existent role ${role.name} (${role.id}) in ${guild.name}`);
- return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `The role ${role} is not set to be auto-assigned.` });
+ return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Die Rolle ${role} wird nicht automatisch vergeben.` });
}
const updatedRoles = existingRoles.filter(id => id !== role.id);
@@ -122,12 +124,12 @@ export default {
logger.info(`[Autorole] Removed role ${role.name} (${role.id}) from auto-assign in ${guild.name} by ${interaction.user.tag}`);
await InteractionHelper.safeEditReply(interaction, {
- embeds: [createAutoroleInfoEmbed(`✅ Removed ${role} from auto-assigned roles.`)],
+ embeds: [createAutoroleInfoEmbed(`✅ ${role} wurde aus den automatisch vergebenen Rollen entfernt.`)],
flags: MessageFlags.Ephemeral
});
} catch (error) {
logger.error(`[Autorole] Failed to remove role for guild ${guild.id}:`, error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while removing the role. Please try again.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Entfernen der Rolle ist ein Fehler aufgetreten. Bitte versuch es erneut.' });
}
}
@@ -137,8 +139,8 @@ export default {
const verificationEnabled = Boolean(guildConfig.verification?.enabled);
const autoVerifyEnabled = Boolean(guildConfig.verification?.autoVerify?.enabled);
const conflictSummary = [
- verificationEnabled ? 'Verification system is enabled' : null,
- autoVerifyEnabled ? 'AutoVerify is enabled' : null
+ verificationEnabled ? 'Die Verifizierung ist aktiviert' : null,
+ autoVerifyEnabled ? 'AutoVerify ist aktiviert' : null
].filter(Boolean).join('\n');
const config = await getWelcomeConfig(client, guild.id);
@@ -149,19 +151,19 @@ export default {
await updateWelcomeConfig(client, guild.id, {
roleIds: singleRoleIds
});
- logger.info(`[Autorole] Trimmed auto-role list to one role in ${interaction.guild.name}`);
+ logger.info(`[Autorole] Trimmed auto-role list to one role in ${interaction.guild!.name}`);
}
if (singleRoleIds.length === 0) {
return InteractionHelper.safeEditReply(interaction, {
- embeds: [createAutoroleInfoEmbed(`ℹ️ No role is set to be auto-assigned.${conflictSummary ?`\n\n⚠️ Setup blockers:\n${conflictSummary}`: ''}`)],
+ embeds: [createAutoroleInfoEmbed(`ℹ️ Es wird keine Rolle automatisch vergeben.${conflictSummary ?`\n\n⚠️ Hindernisse für die Einrichtung:\n${conflictSummary}`: ''}`)],
flags: MessageFlags.Ephemeral
});
}
const roles = await guild.roles.fetch();
- const validRoles = [];
- const invalidRoleIds = [];
+ const validRoles: Role[] = [];
+ const invalidRoleIds: string[] = [];
for (const roleId of singleRoleIds) {
const role = roles.get(roleId);
@@ -173,7 +175,7 @@ export default {
}
if (invalidRoleIds.length > 0) {
- logger.info(`[Autorole] Cleaning up ${invalidRoleIds.length} invalid role(s) from guild ${interaction.guild.name}`);
+ logger.info(`[Autorole] Cleaning up ${invalidRoleIds.length} invalid role(s) from guild ${interaction.guild!.name}`);
const updatedRoles = singleRoleIds.filter(id => !invalidRoleIds.includes(id));
await updateWelcomeConfig(client, guild.id, {
roleIds: updatedRoles
@@ -182,16 +184,16 @@ export default {
if (validRoles.length === 0) {
return InteractionHelper.safeEditReply(interaction, {
- embeds: [createAutoroleInfoEmbed(`ℹ️ No valid auto-role found. Any invalid role has been removed.${conflictSummary ?`\n\n⚠️ Setup blockers:\n${conflictSummary}`: ''}`)],
+ embeds: [createAutoroleInfoEmbed(`ℹ️ Keine gültige Auto-Rolle gefunden. Ungültige Rollen wurden entfernt.${conflictSummary ?`\n\n⚠️ Hindernisse für die Einrichtung:\n${conflictSummary}`: ''}`)],
flags: MessageFlags.Ephemeral
});
}
const embed = new EmbedBuilder()
.setColor(getColor('info'))
- .setTitle('Auto-Assigned Role')
- .setDescription(`${validRoles[0]}${conflictSummary ?`\n\n⚠️ Setup blockers:\n${conflictSummary}`: ''}`)
- .setFooter({ text: 'Only one auto-role can be configured.' });
+ .setTitle('Automatisch vergebene Rolle')
+ .setDescription(`${validRoles[0]}${conflictSummary ?`\n\n⚠️ Hindernisse für die Einrichtung:\n${conflictSummary}`: ''}`)
+ .setFooter({ text: 'Es kann nur eine Auto-Rolle eingerichtet werden.' });
await InteractionHelper.safeEditReply(interaction, {
embeds: [embed],
@@ -200,8 +202,8 @@ export default {
} catch (error) {
logger.error(`[Autorole] Failed to list roles for guild ${guild.id}:`, error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while listing auto-assigned roles. Please try again.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Anzeigen der automatisch vergebenen Rollen ist ein Fehler aufgetreten. Bitte versuch es erneut.' });
}
}
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Welcome/goodbye.js b/src/commands/Welcome/modules/goodbye.ts
similarity index 60%
rename from src/commands/Welcome/goodbye.js
rename to src/commands/Welcome/modules/goodbye.ts
index 3d45b93309..7af17d6096 100644
--- a/src/commands/Welcome/goodbye.js
+++ b/src/commands/Welcome/modules/goodbye.ts
@@ -1,36 +1,39 @@
-import { getColor } from '../../config/bot.js';
+import { getColor } from '../../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder, MessageFlags } from 'discord.js';
-import { getWelcomeConfig, updateWelcomeConfig } from '../../utils/database.js';
-import { formatWelcomeMessage, truncateForEmbedField } from '../../utils/welcome.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
+import type { ChatInputCommandInteraction } from 'discord.js';
+import { getWelcomeConfig, updateWelcomeConfig } from '../../../utils/database.js';
+import { formatWelcomeMessage, truncateForEmbedField } from '../../../utils/welcome.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import type { Command } from '../../../types/index.js';
+import type { WelcomeEmbedConfig } from '../../../utils/database.js';
export default {
data: new SlashCommandBuilder()
.setName('goodbye')
- .setDescription('Configure the goodbye message system')
+ .setDescription('Das Abschiedssystem konfigurieren')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand =>
subcommand
.setName('setup')
- .setDescription('Set up the goodbye message')
+ .setDescription('Die Abschiedsnachricht einrichten')
.addChannelOption(option =>
option.setName('channel')
- .setDescription('The channel to send goodbye messages to')
+ .setDescription('Der Kanal für Abschiedsnachrichten')
.addChannelTypes(ChannelType.GuildText)
.setRequired(true))
.addStringOption(option =>
option.setName('message')
- .setDescription('Goodbye message. Variables: {user}, {username}, {server}, {memberCount}')
+ .setDescription('Abschiedsnachricht. Variablen: {user}, {username}, {server}, {memberCount}')
.setRequired(true))
.addStringOption(option =>
option.setName('image')
- .setDescription('URL of the image to include in the goodbye message')
+ .setDescription('URL des Bildes für die Abschiedsnachricht')
.setRequired(false))
.addBooleanOption(option =>
option.setName('ping')
- .setDescription('Whether to ping the user in the goodbye message')
+ .setDescription('Ob der Nutzer in der Abschiedsnachricht gepingt wird')
.setRequired(false))),
async execute(interaction) {
@@ -44,16 +47,16 @@ export default {
return;
}
- const { options, guild, client } = interaction;
+ const { options, guild, client } = interaction as ChatInputCommandInteraction<'cached'>;
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the **Manage Server** permission to use `/goodbye`.' });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um `/welcome goodbye` zu nutzen.' });
}
const subcommand = options.getSubcommand();
if (subcommand === 'setup') {
- const channel = options.getChannel('channel');
+ const channel = options.getChannel('channel')!;
const message = options.getString('message');
const image = options.getString('image');
const ping = options.getBoolean('ping') ?? false;
@@ -61,12 +64,12 @@ export default {
const existingConfig = await getWelcomeConfig(client, guild.id);
if (existingConfig?.goodbyeChannelId) {
logger.info(`[Goodbye] Setup blocked because config already exists in channel ${existingConfig.goodbyeChannelId} for guild ${guild.id}`);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Goodbye is already configured for <#${existingConfig.goodbyeChannelId}>. Use **/greet dashboard** to customize channel, message, ping, or image.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Abschied ist bereits für <#${existingConfig.goodbyeChannelId}> eingerichtet. Nutze **/welcome greet dashboard**, um Kanal, Nachricht, Ping oder Bild anzupassen.` });
}
if (!message || message.trim().length === 0) {
logger.warn(`[Goodbye] Empty message provided by ${interaction.user.tag} in ${guild.name}`);
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Goodbye message cannot be empty' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Die Abschiedsnachricht darf nicht leer sein.' });
}
if (image) {
@@ -74,7 +77,7 @@ export default {
new URL(image);
} catch (e) {
logger.warn(`[Goodbye] Invalid image URL provided by ${interaction.user.tag}: ${image}`);
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please provide a valid image URL (must start with http:// or https://' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte gib eine gültige Bild-URL an (muss mit http:// oder https:// beginnen).' });
}
}
@@ -84,13 +87,14 @@ export default {
goodbyeChannelId: channel.id,
leaveMessage: message,
goodbyePing: ping,
+ // Stored color is numeric (getColor); WelcomeEmbedConfig.color is typed string-only.
leaveEmbed: {
- title: "Goodbye {user.tag}",
+ title: "Tschüss {user.tag}",
description: message,
color: getColor('error'),
- footer: `Goodbye from ${guild.name}!`,
+ footer: `Tschüss von ${guild.name}!`,
...(image && { image: { url: image } })
- }
+ } as Record as WelcomeEmbedConfig
});
logger.info(`[Goodbye] Setup configured by ${interaction.user.tag} for guild ${guild.name} (${guild.id})`);
@@ -102,14 +106,14 @@ export default {
const embed = new EmbedBuilder()
.setColor(getColor('success'))
- .setTitle('Goodbye System Configured')
- .setDescription(`Goodbye messages will now be sent to ${channel}`)
+ .setTitle('Abschiedssystem eingerichtet')
+ .setDescription(`Abschiedsnachrichten werden jetzt in ${channel} gesendet.`)
.addFields(
- { name: 'Message Preview', value: truncateForEmbedField(previewMessage) },
- { name: 'Ping User', value: ping ? 'Yes' : 'No' },
- { name: 'Status', value: 'Enabled' }
+ { name: 'Nachrichtenvorschau', value: truncateForEmbedField(previewMessage) },
+ { name: 'Nutzer pingen', value: ping ? 'Ja' : 'Nein' },
+ { name: 'Status', value: 'Aktiviert' }
)
- .setFooter({ text: 'Tip: Use /greet dashboard to customize goodbye settings' });
+ .setFooter({ text: 'Tipp: Mit /welcome greet dashboard passt du die Abschiedseinstellungen an' });
if (image) {
embed.setImage(image);
@@ -118,8 +122,8 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
} catch (error) {
logger.error(`[Goodbye] Failed to setup goodbye system for guild ${guild.id}:`, error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while configuring the goodbye system. Please try again.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Einrichten des Abschiedssystems ist ein Fehler aufgetreten. Bitte versuch es erneut.' });
}
}
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Welcome/greet.js b/src/commands/Welcome/modules/greet.ts
similarity index 54%
rename from src/commands/Welcome/greet.js
rename to src/commands/Welcome/modules/greet.ts
index aa823e14bb..ad20b0730d 100644
--- a/src/commands/Welcome/greet.js
+++ b/src/commands/Welcome/modules/greet.ts
@@ -1,25 +1,26 @@
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { logger } from '../../utils/logger.js';
-import { handleInteractionError, TitanBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
-import greetDashboard from './modules/greet_dashboard.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { logger } from '../../../utils/logger.js';
+import { handleInteractionError, NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import greetDashboard from './greet_dashboard.js';
+import type { Command } from '../../../types/index.js';
export default {
slashOnly: true,
data: new SlashCommandBuilder()
.setName('greet')
- .setDescription('Manage welcome & goodbye settings')
+ .setDescription('Willkommens- und Abschiedseinstellungen verwalten')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand =>
subcommand
.setName('dashboard')
- .setDescription('Open the welcome & goodbye configuration dashboard'),
+ .setDescription('Das Dashboard für Willkommen und Abschied öffnen'),
),
async execute(interaction, config, client) {
try {
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the **Manage Server** permission to use `/greet`.' });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um `/welcome greet` zu nutzen.' });
}
const subcommand = interaction.options.getSubcommand();
@@ -28,13 +29,13 @@ export default {
case 'dashboard':
return await greetDashboard.execute(interaction, config, client);
default:
- logger.warn(`Unknown /greet subcommand: ${subcommand}`);
+ logger.warn(`Unknown /welcome greet subcommand: ${subcommand}`);
}
} catch (error) {
- if (error instanceof TitanBotError) {
- return await replyUserError(interaction, { type: ErrorTypes.CONFIGURATION, message: error.userMessage || 'Something went wrong.' });
+ if (error instanceof NexoBotError) {
+ return await replyUserError(interaction, { type: ErrorTypes.CONFIGURATION, message: error.userMessage || 'Etwas ist schiefgelaufen.' });
}
await handleInteractionError(interaction, error, { command: 'greet' });
}
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Welcome/modules/greet_dashboard.js b/src/commands/Welcome/modules/greet_dashboard.ts
similarity index 64%
rename from src/commands/Welcome/modules/greet_dashboard.js
rename to src/commands/Welcome/modules/greet_dashboard.ts
index 97581e7cd5..ab0d2db8bc 100644
--- a/src/commands/Welcome/modules/greet_dashboard.js
+++ b/src/commands/Welcome/modules/greet_dashboard.ts
@@ -17,14 +17,24 @@ import {
FileUploadBuilder,
TextDisplayBuilder,
} from 'discord.js';
+import type {
+ ChatInputCommandInteraction,
+ Guild,
+ GuildBasedChannel,
+ InteractionReplyOptions,
+ MessageComponentInteraction,
+ StringSelectMenuInteraction,
+} from 'discord.js';
import { InteractionHelper } from '../../../utils/interactionHelper.js';
import { successEmbed } from '../../../utils/embeds.js';
import { logger } from '../../../utils/logger.js';
-import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import { NexoBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
import { getWelcomeConfig, saveWelcomeConfig } from '../../../utils/database.js';
import { botHasPermission } from '../../../utils/permissionGuard.js';
+import type { WelcomeConfig, WelcomeEmbedConfig } from '../../../utils/database.js';
+import type { NexoClient, SubcommandModule } from '../../../types/index.js';
-async function deferComponent(interaction) {
+async function deferComponent(interaction: MessageComponentInteraction) {
if (interaction.deferred || interaction.replied) {
return true;
}
@@ -33,120 +43,120 @@ async function deferComponent(interaction) {
await interaction.deferUpdate();
return true;
} catch (error) {
- logger.debug('Component interaction expired or already acknowledged:', error.message);
+ logger.debug('Component interaction expired or already acknowledged:', (error as Error).message);
return false;
}
}
-async function sendEphemeralFollowUp(interaction, payload) {
+async function sendEphemeralFollowUp(interaction: MessageComponentInteraction, payload: InteractionReplyOptions) {
try {
await interaction.followUp({
...payload,
flags: MessageFlags.Ephemeral,
});
} catch (error) {
- logger.debug('Failed to send ephemeral follow-up:', error.message);
+ logger.debug('Failed to send ephemeral follow-up:', (error as Error).message);
}
}
-function buildDashboardEmbed(cfg, guild) {
- const welcomeChannel = cfg.channelId ? `<#${cfg.channelId}>` : '`Not set`';
- const goodbyeChannel = cfg.goodbyeChannelId ? `<#${cfg.goodbyeChannelId}>` : '`Not set`';
+function buildDashboardEmbed(cfg: WelcomeConfig, guild: Guild) {
+ const welcomeChannel = cfg.channelId ? `<#${cfg.channelId}>` : '`Nicht gesetzt`';
+ const goodbyeChannel = cfg.goodbyeChannelId ? `<#${cfg.goodbyeChannelId}>` : '`Nicht gesetzt`';
- const rawWelcome = cfg.welcomeMessage || 'Welcome {user} to {server}!';
- const rawGoodbye = cfg.leaveMessage || '{user.tag} has left the server.';
+ const rawWelcome = cfg.welcomeMessage || 'Willkommen {user} auf {server}!';
+ const rawGoodbye = cfg.leaveMessage || '{user.tag} hat den Server verlassen.';
const welcomePreview = `\`${rawWelcome.length > 55 ? rawWelcome.substring(0, 55) + '…' : rawWelcome}\``;
const goodbyePreview = `\`${rawGoodbye.length > 55 ? rawGoodbye.substring(0, 55) + '…' : rawGoodbye}\``;
return new EmbedBuilder()
- .setTitle('👋 Greet System Dashboard')
+ .setTitle('👋 Willkommen & Abschied – Dashboard')
.setDescription(
- `Manage welcome & goodbye settings for **${guild.name}**.\nUse the toggles to enable/disable each side, then select an option to edit.`,
+ `Verwalte die Willkommens- und Abschiedseinstellungen für **${guild.name}**.\nAktiviere oder deaktiviere beide Seiten mit den Schaltern und wähle dann eine Option zum Bearbeiten.`,
)
.setColor(getColor('info'))
.addFields(
- { name: 'Welcome Channel', value: welcomeChannel, inline: true },
- { name: 'Welcome Status', value: cfg.enabled ? 'Enabled' : 'Disabled', inline: true },
- { name: 'Welcome Ping', value: cfg.welcomePing ? 'On' : 'Off', inline: true },
- { name: 'Goodbye Channel', value: goodbyeChannel, inline: true },
- { name: 'Goodbye Status', value: cfg.goodbyeEnabled ? 'Enabled' : 'Disabled', inline: true },
- { name: 'Goodbye Ping', value: cfg.goodbyePing ? 'On' : 'Off', inline: true },
- { name: 'Welcome Message', value: welcomePreview, inline: false },
- { name: 'Goodbye Message', value: goodbyePreview, inline: false },
+ { name: 'Willkommenskanal', value: welcomeChannel, inline: true },
+ { name: 'Willkommen-Status', value: cfg.enabled ? 'Aktiviert' : 'Deaktiviert', inline: true },
+ { name: 'Willkommen-Ping', value: cfg.welcomePing ? 'An' : 'Aus', inline: true },
+ { name: 'Abschiedskanal', value: goodbyeChannel, inline: true },
+ { name: 'Abschied-Status', value: cfg.goodbyeEnabled ? 'Aktiviert' : 'Deaktiviert', inline: true },
+ { name: 'Abschied-Ping', value: cfg.goodbyePing ? 'An' : 'Aus', inline: true },
+ { name: 'Willkommensnachricht', value: welcomePreview, inline: false },
+ { name: 'Abschiedsnachricht', value: goodbyePreview, inline: false },
)
- .setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' })
+ .setFooter({ text: 'Das Dashboard schließt sich nach 10 Minuten Inaktivität' })
.setTimestamp();
}
-function buildSelectMenu(guildId) {
+function buildSelectMenu(guildId: string) {
return new StringSelectMenuBuilder()
.setCustomId(`greet_cfg_${guildId}`)
- .setPlaceholder('Select a setting to configure...')
+ .setPlaceholder('Wähle eine Einstellung...')
.addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel('Welcome Channel')
- .setDescription('Set the channel where welcome messages are sent')
+ .setLabel('Willkommenskanal')
+ .setDescription('Lege fest, in welchem Kanal Willkommensnachrichten gesendet werden')
.setValue('welcome_channel')
.setEmoji('🟢'),
new StringSelectMenuOptionBuilder()
- .setLabel('Welcome Message')
- .setDescription('Edit the text shown when a member joins')
+ .setLabel('Willkommensnachricht')
+ .setDescription('Bearbeite den Text, der beim Beitritt eines Mitglieds erscheint')
.setValue('welcome_message')
.setEmoji('💬'),
new StringSelectMenuOptionBuilder()
- .setLabel('Welcome Image')
- .setDescription('Set the image for welcome messages')
+ .setLabel('Willkommensbild')
+ .setDescription('Lege das Bild für Willkommensnachrichten fest')
.setValue('welcome_image')
.setEmoji('🖼️'),
new StringSelectMenuOptionBuilder()
- .setLabel('Goodbye Channel')
- .setDescription('Set the channel where goodbye messages are sent')
+ .setLabel('Abschiedskanal')
+ .setDescription('Lege fest, in welchem Kanal Abschiedsnachrichten gesendet werden')
.setValue('goodbye_channel')
.setEmoji('🔴'),
new StringSelectMenuOptionBuilder()
- .setLabel('Goodbye Message')
- .setDescription('Edit the text shown when a member leaves')
+ .setLabel('Abschiedsnachricht')
+ .setDescription('Bearbeite den Text, der beim Verlassen eines Mitglieds erscheint')
.setValue('goodbye_message')
.setEmoji('💬'),
new StringSelectMenuOptionBuilder()
- .setLabel('Goodbye Image')
- .setDescription('Set the image for goodbye messages')
+ .setLabel('Abschiedsbild')
+ .setDescription('Lege das Bild für Abschiedsnachrichten fest')
.setValue('goodbye_image')
.setEmoji('🖼️'),
);
}
-function buildButtonRow(cfg, guildId, disabled = false) {
+function buildButtonRow(cfg: WelcomeConfig, guildId: string, disabled = false) {
const welcomeOn = cfg.enabled === true;
const goodbyeOn = cfg.goodbyeEnabled === true;
const welcomePingOn = cfg.welcomePing === true;
const goodbyePingOn = cfg.goodbyePing === true;
return [
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`greet_cfg_toggle_welcome_${guildId}`)
- .setLabel('Welcome')
+ .setLabel('Willkommen')
.setStyle(welcomeOn ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji('🟢')
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId(`greet_cfg_toggle_goodbye_${guildId}`)
- .setLabel('Goodbye')
+ .setLabel('Abschied')
.setStyle(goodbyeOn ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji('🔴')
.setDisabled(disabled),
),
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`greet_cfg_ping_welcome_${guildId}`)
- .setLabel('Ping Welcome')
+ .setLabel('Ping bei Willkommen')
.setStyle(welcomePingOn ? ButtonStyle.Primary : ButtonStyle.Secondary)
.setEmoji('🔔')
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId(`greet_cfg_ping_goodbye_${guildId}`)
- .setLabel('Ping Goodbye')
+ .setLabel('Ping bei Abschied')
.setStyle(goodbyePingOn ? ButtonStyle.Primary : ButtonStyle.Secondary)
.setEmoji('🔔')
.setDisabled(disabled),
@@ -154,18 +164,18 @@ function buildButtonRow(cfg, guildId, disabled = false) {
];
}
-async function refreshDashboard(rootInteraction, cfg, guildId) {
+async function refreshDashboard(rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string) {
try {
const selectMenu = buildSelectMenu(guildId);
await InteractionHelper.safeEditReply(rootInteraction, {
- embeds: [buildDashboardEmbed(cfg, rootInteraction.guild)],
+ embeds: [buildDashboardEmbed(cfg, rootInteraction.guild!)],
components: [
...buildButtonRow(cfg, guildId),
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
});
} catch (error) {
- logger.debug('Could not refresh greet dashboard (interaction may have expired):', error.message);
+ logger.debug('Could not refresh greet dashboard (interaction may have expired):', (error as Error).message);
}
}
@@ -173,14 +183,14 @@ export default {
prefixOnly: false,
async execute(interaction, config, client) {
try {
- const guildId = interaction.guild.id;
+ const guildId = interaction.guild!.id;
const cfg = await getWelcomeConfig(client, guildId);
if (!cfg.channelId && !cfg.goodbyeChannelId) {
- throw new TitanBotError(
+ throw new NexoBotError(
'Greet system not configured',
ErrorTypes.CONFIGURATION,
- 'Neither Welcome nor Goodbye has been set up yet. Run `/welcome setup` or `/goodbye setup` first.',
+ 'Weder Willkommen noch Abschied sind eingerichtet. Führe zuerst `/welcome message setup` oder `/welcome goodbye setup` aus.',
);
}
@@ -192,14 +202,14 @@ export default {
const selectMenu = buildSelectMenu(guildId);
await InteractionHelper.safeEditReply(interaction, {
- embeds: [buildDashboardEmbed(cfg, interaction.guild)],
+ embeds: [buildDashboardEmbed(cfg, interaction.guild!)],
components: [
...buildButtonRow(cfg, guildId),
- new ActionRowBuilder().addComponents(selectMenu),
+ new ActionRowBuilder().addComponents(selectMenu),
],
});
- const collector = interaction.channel.createMessageComponentCollector({
+ const collector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
filter: i =>
i.user.id === interaction.user.id && i.customId === `greet_cfg_${guildId}`,
@@ -230,16 +240,16 @@ export default {
break;
}
} catch (error) {
- if (error instanceof TitanBotError) {
- logger.debug(`Greet config validation error: ${error.message}`);
+ if (error instanceof NexoBotError) {
+ logger.debug(`Greet config validation error: ${(error as Error).message}`);
} else {
logger.error('Unexpected greet dashboard error:', error);
}
const errorMessage =
- error instanceof TitanBotError
- ? error.userMessage || 'An error occurred while processing your selection.'
- : 'An unexpected error occurred while updating the configuration.';
+ error instanceof NexoBotError
+ ? error.userMessage || 'Beim Verarbeiten deiner Auswahl ist ein Fehler aufgetreten.'
+ : 'Beim Aktualisieren der Konfiguration ist ein unerwarteter Fehler aufgetreten.';
if (!selectInteraction.replied && !selectInteraction.deferred) {
await selectInteraction.deferUpdate().catch(() => {});
@@ -252,7 +262,7 @@ export default {
}
});
- const btnCollector = interaction.channel.createMessageComponentCollector({
+ const btnCollector = interaction.channel!.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: i =>
i.user.id === interaction.user.id &&
@@ -277,8 +287,8 @@ export default {
await sendEphemeralFollowUp(btnInteraction, {
embeds: [
successEmbed(
- '✅ Welcome Updated',
- `Welcome messages are now **${cfg.enabled ? 'enabled' : 'disabled'}**.`,
+ '✅ Willkommen aktualisiert',
+ `Willkommensnachrichten sind jetzt **${cfg.enabled ? 'aktiviert' : 'deaktiviert'}**.`,
),
],
});
@@ -288,8 +298,8 @@ export default {
await sendEphemeralFollowUp(btnInteraction, {
embeds: [
successEmbed(
- '✅ Goodbye Updated',
- `Goodbye messages are now **${cfg.goodbyeEnabled ? 'enabled' : 'disabled'}**.`,
+ '✅ Abschied aktualisiert',
+ `Abschiedsnachrichten sind jetzt **${cfg.goodbyeEnabled ? 'aktiviert' : 'deaktiviert'}**.`,
),
],
});
@@ -299,8 +309,8 @@ export default {
await sendEphemeralFollowUp(btnInteraction, {
embeds: [
successEmbed(
- '✅ Welcome Ping Updated',
- `Joining users will${cfg.welcomePing ? '' : ' **not**'} be pinged in the welcome message.`,
+ '✅ Willkommen-Ping aktualisiert',
+ `Neue Nutzer werden in der Willkommensnachricht${cfg.welcomePing ? '' : ' **nicht**'} gepingt.`,
),
],
});
@@ -310,8 +320,8 @@ export default {
await sendEphemeralFollowUp(btnInteraction, {
embeds: [
successEmbed(
- '✅ Goodbye Ping Updated',
- `Leaving users will${cfg.goodbyePing ? '' : ' **not**'} be pinged in the goodbye message.`,
+ '✅ Abschied-Ping aktualisiert',
+ `Gehende Nutzer werden in der Abschiedsnachricht${cfg.goodbyePing ? '' : ' **nicht**'} gepingt.`,
),
],
});
@@ -330,53 +340,53 @@ export default {
await InteractionHelper.safeEditReply(interaction, {
embeds: [
new EmbedBuilder()
- .setTitle('Dashboard Timed Out')
- .setDescription('This dashboard has been closed due to inactivity. Please run the command again to continue.')
+ .setTitle('Dashboard abgelaufen')
+ .setDescription('Dieses Dashboard wurde wegen Inaktivität geschlossen. Führe den Command erneut aus, um weiterzumachen.')
.setColor(getColor('error'))
],
components: [],
});
} catch (error) {
- logger.debug('Could not update dashboard on timeout:', error.message);
+ logger.debug('Could not update dashboard on timeout:', (error as Error).message);
}
}
});
} catch (error) {
- if (error instanceof TitanBotError) throw error;
+ if (error instanceof NexoBotError) throw error;
logger.error('Unexpected error in greet_dashboard:', error);
- throw new TitanBotError(
- `Greet dashboard failed: ${error.message}`,
+ throw new NexoBotError(
+ `Greet dashboard failed: ${(error as Error).message}`,
ErrorTypes.UNKNOWN,
- 'Failed to open the greet dashboard.',
+ 'Das Willkommen-Dashboard konnte nicht geöffnet werden.',
);
}
},
-};
+} satisfies SubcommandModule & { prefixOnly: boolean };
-async function handleWelcomeChannel(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleWelcomeChannel(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string, client: NexoClient) {
if (!await deferComponent(selectInteraction)) {
return;
}
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('greet_cfg_welcome_channel')
- .setPlaceholder('Select a text channel...')
+ .setPlaceholder('Wähle einen Textkanal...')
.addChannelTypes(ChannelType.GuildText)
.setMaxValues(1);
await sendEphemeralFollowUp(selectInteraction, {
embeds: [
new EmbedBuilder()
- .setTitle('🟢 Welcome Channel')
+ .setTitle('🟢 Willkommenskanal')
.setDescription(
- `**Current:** ${cfg.channelId ?`<#${cfg.channelId}>`: '`Not set`'}\n\nSelect the channel where welcome messages will be sent.`,
+ `**Aktuell:** ${cfg.channelId ?`<#${cfg.channelId}>`: '`Nicht gesetzt`'}\n\nWähle den Kanal, in dem Willkommensnachrichten gesendet werden.`,
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(channelSelect)],
+ components: [new ActionRowBuilder().addComponents(channelSelect)],
});
- const chanCollector = rootInteraction.channel.createMessageComponentCollector({
+ const chanCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'greet_cfg_welcome_channel',
@@ -388,21 +398,21 @@ async function handleWelcomeChannel(selectInteraction, rootInteraction, cfg, gui
if (!await deferComponent(chanInteraction)) {
return;
}
- const channel = chanInteraction.channels.first();
+ const channel = chanInteraction.channels.first() as GuildBasedChannel | undefined;
if (!botHasPermission(channel, ['ViewChannel', 'SendMessages', 'EmbedLinks'])) {
await replyUserError(chanInteraction, {
type: ErrorTypes.PERMISSION,
- message: `I need **View Channel**, **Send Messages**, and **Embed Links** in ${channel}.`,
+ message: `Ich brauche **Kanal ansehen**, **Nachrichten senden** und **Links einbetten** in ${channel}.`,
});
return;
}
- cfg.channelId = channel.id;
+ cfg.channelId = channel!.id;
await saveWelcomeConfig(client, guildId, cfg);
await sendEphemeralFollowUp(chanInteraction, {
- embeds: [successEmbed('Channel Updated', `Welcome messages will now be sent in ${channel}.`)],
+ embeds: [successEmbed('Kanal aktualisiert', `Willkommensnachrichten werden jetzt in ${channel} gesendet.`)],
});
await refreshDashboard(rootInteraction, cfg, guildId);
@@ -412,23 +422,23 @@ async function handleWelcomeChannel(selectInteraction, rootInteraction, cfg, gui
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No channel was selected. The setting was not changed.',
+ message: 'Es wurde kein Kanal ausgewählt. Die Einstellung wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleWelcomeMessage(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleWelcomeMessage(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('greet_cfg_welcome_message')
- .setTitle('Edit Welcome Message')
+ .setTitle('Willkommensnachricht bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('message_input')
- .setLabel('Message (variables: {user}, {server}, etc)')
+ .setLabel('Nachricht (Variablen: {user}, {server} usw.)')
.setStyle(TextInputStyle.Paragraph)
- .setValue(cfg.welcomeMessage || 'Welcome {user} to {server}!')
+ .setValue(cfg.welcomeMessage || 'Willkommen {user} auf {server}!')
.setMaxLength(2000)
.setMinLength(1)
.setRequired(true),
@@ -455,23 +465,23 @@ async function handleWelcomeMessage(selectInteraction, rootInteraction, cfg, gui
await saveWelcomeConfig(client, guildId, cfg);
await submitted.reply({
- embeds: [successEmbed('Welcome Message Updated', 'The welcome message has been saved.')],
+ embeds: [successEmbed('Willkommensnachricht aktualisiert', 'Die Willkommensnachricht wurde gespeichert.')],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleWelcomeImage(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleWelcomeImage(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('greet_cfg_welcome_image')
- .setTitle('Set Welcome Image');
+ .setTitle('Willkommensbild festlegen');
const imageHint = new TextDisplayBuilder()
- .setContent('Provide a direct image URL **or** upload a file below. If both are given, the uploaded file takes priority. Leave the URL blank and skip the upload to remove the image.');
+ .setContent('Gib eine direkte Bild-URL an **oder** lade unten eine Datei hoch. Gibst du beides an, hat die hochgeladene Datei Vorrang. Lass die URL leer und überspring den Upload, um das Bild zu entfernen.');
const urlLabel = new LabelBuilder()
- .setLabel('Image URL (optional)')
+ .setLabel('Bild-URL (optional)')
.setTextInputComponent(
new TextInputBuilder()
.setCustomId('image_input')
@@ -482,7 +492,7 @@ async function handleWelcomeImage(selectInteraction, rootInteraction, cfg, guild
);
const uploadLabel = new LabelBuilder()
- .setLabel('Or upload an image file (optional)')
+ .setLabel('Oder lade eine Bilddatei hoch (optional)')
.setFileUploadComponent(
new FileUploadBuilder()
.setCustomId('image_upload')
@@ -516,11 +526,11 @@ async function handleWelcomeImage(selectInteraction, rootInteraction, cfg, guild
try {
new URL(imageUrl);
if (!['http:', 'https:'].includes(new URL(imageUrl).protocol)) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Image URL must start with `http://` or `https://`.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Die Bild-URL muss mit `http://` oder `https://` beginnen.' });
return;
}
} catch {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Please provide a valid image URL.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Bitte gib eine gültige Bild-URL an.' });
return;
}
}
@@ -529,14 +539,14 @@ async function handleWelcomeImage(selectInteraction, rootInteraction, cfg, guild
await saveWelcomeConfig(client, guildId, cfg);
await submitted.reply({
- embeds: [successEmbed('Welcome Image Updated', `Image ${imageUrl ? 'updated' : 'removed'} successfully.`)],
+ embeds: [successEmbed('Willkommensbild aktualisiert', `Bild erfolgreich ${imageUrl ? 'aktualisiert' : 'entfernt'}.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleWelcomePing(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleWelcomePing(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string, client: NexoClient) {
if (!await deferComponent(selectInteraction)) {
return;
}
@@ -547,8 +557,8 @@ async function handleWelcomePing(selectInteraction, rootInteraction, cfg, guildI
await sendEphemeralFollowUp(selectInteraction, {
embeds: [
successEmbed(
- '✅ Welcome Ping Updated',
- `Joining users will${cfg.welcomePing ? '' : ' **not**'} be pinged in the welcome message.`,
+ '✅ Willkommen-Ping aktualisiert',
+ `Neue Nutzer werden in der Willkommensnachricht${cfg.welcomePing ? '' : ' **nicht**'} gepingt.`,
),
],
});
@@ -556,30 +566,30 @@ async function handleWelcomePing(selectInteraction, rootInteraction, cfg, guildI
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleGoodbyeChannel(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleGoodbyeChannel(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string, client: NexoClient) {
if (!await deferComponent(selectInteraction)) {
return;
}
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('greet_cfg_goodbye_channel')
- .setPlaceholder('Select a text channel...')
+ .setPlaceholder('Wähle einen Textkanal...')
.addChannelTypes(ChannelType.GuildText)
.setMaxValues(1);
await sendEphemeralFollowUp(selectInteraction, {
embeds: [
new EmbedBuilder()
- .setTitle('🔴 Goodbye Channel')
+ .setTitle('🔴 Abschiedskanal')
.setDescription(
- `**Current:** ${cfg.goodbyeChannelId ?`<#${cfg.goodbyeChannelId}>`: '`Not set`'}\n\nSelect the channel where goodbye messages will be sent.`,
+ `**Aktuell:** ${cfg.goodbyeChannelId ?`<#${cfg.goodbyeChannelId}>`: '`Nicht gesetzt`'}\n\nWähle den Kanal, in dem Abschiedsnachrichten gesendet werden.`,
)
.setColor(getColor('info')),
],
- components: [new ActionRowBuilder().addComponents(channelSelect)],
+ components: [new ActionRowBuilder().addComponents(channelSelect)],
});
- const chanCollector = rootInteraction.channel.createMessageComponentCollector({
+ const chanCollector = rootInteraction.channel!.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect,
filter: i =>
i.user.id === selectInteraction.user.id && i.customId === 'greet_cfg_goodbye_channel',
@@ -591,21 +601,21 @@ async function handleGoodbyeChannel(selectInteraction, rootInteraction, cfg, gui
if (!await deferComponent(chanInteraction)) {
return;
}
- const channel = chanInteraction.channels.first();
+ const channel = chanInteraction.channels.first() as GuildBasedChannel | undefined;
if (!botHasPermission(channel, ['ViewChannel', 'SendMessages', 'EmbedLinks'])) {
await replyUserError(chanInteraction, {
type: ErrorTypes.PERMISSION,
- message: `I need **View Channel**, **Send Messages**, and **Embed Links** in ${channel}.`,
+ message: `Ich brauche **Kanal ansehen**, **Nachrichten senden** und **Links einbetten** in ${channel}.`,
});
return;
}
- cfg.goodbyeChannelId = channel.id;
+ cfg.goodbyeChannelId = channel!.id;
await saveWelcomeConfig(client, guildId, cfg);
await sendEphemeralFollowUp(chanInteraction, {
- embeds: [successEmbed('Channel Updated', `Goodbye messages will now be sent in ${channel}.`)],
+ embeds: [successEmbed('Kanal aktualisiert', `Abschiedsnachrichten werden jetzt in ${channel} gesendet.`)],
});
await refreshDashboard(rootInteraction, cfg, guildId);
@@ -615,23 +625,23 @@ async function handleGoodbyeChannel(selectInteraction, rootInteraction, cfg, gui
if (reason === 'time' && collected.size === 0) {
replyUserError(selectInteraction, {
type: ErrorTypes.RATE_LIMIT,
- message: 'No channel was selected. The setting was not changed.',
+ message: 'Es wurde kein Kanal ausgewählt. Die Einstellung wurde nicht geändert.',
}).catch(() => {});
}
});
}
-async function handleGoodbyeMessage(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleGoodbyeMessage(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('greet_cfg_goodbye_message')
- .setTitle('Edit Goodbye Message')
+ .setTitle('Abschiedsnachricht bearbeiten')
.addComponents(
- new ActionRowBuilder().addComponents(
+ new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('message_input')
- .setLabel('Message (variables: {user}, {server}, etc)')
+ .setLabel('Nachricht (Variablen: {user}, {server} usw.)')
.setStyle(TextInputStyle.Paragraph)
- .setValue(cfg.leaveMessage || '{user.tag} has left the server.')
+ .setValue(cfg.leaveMessage || '{user.tag} hat den Server verlassen.')
.setMaxLength(2000)
.setMinLength(1)
.setRequired(true),
@@ -658,23 +668,23 @@ async function handleGoodbyeMessage(selectInteraction, rootInteraction, cfg, gui
await saveWelcomeConfig(client, guildId, cfg);
await submitted.reply({
- embeds: [successEmbed('Goodbye Message Updated', 'The goodbye message has been saved.')],
+ embeds: [successEmbed('Abschiedsnachricht aktualisiert', 'Die Abschiedsnachricht wurde gespeichert.')],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleGoodbyeImage(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleGoodbyeImage(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string, client: NexoClient) {
const modal = new ModalBuilder()
.setCustomId('greet_cfg_goodbye_image')
- .setTitle('Set Goodbye Image');
+ .setTitle('Abschiedsbild festlegen');
const imageHint = new TextDisplayBuilder()
- .setContent('Provide a direct image URL **or** upload a file below. If both are given, the uploaded file takes priority. Leave the URL blank and skip the upload to remove the image.');
+ .setContent('Gib eine direkte Bild-URL an **oder** lade unten eine Datei hoch. Gibst du beides an, hat die hochgeladene Datei Vorrang. Lass die URL leer und überspring den Upload, um das Bild zu entfernen.');
const urlLabel = new LabelBuilder()
- .setLabel('Image URL (optional)')
+ .setLabel('Bild-URL (optional)')
.setTextInputComponent(
new TextInputBuilder()
.setCustomId('image_input')
@@ -683,13 +693,13 @@ async function handleGoodbyeImage(selectInteraction, rootInteraction, cfg, guild
.setValue(
typeof cfg.leaveEmbed?.image === 'string'
? cfg.leaveEmbed.image
- : cfg.leaveEmbed?.image?.url || ''
+ : (cfg.leaveEmbed?.image as { url?: string } | undefined)?.url || ''
)
.setRequired(false),
);
const uploadLabel = new LabelBuilder()
- .setLabel('Or upload an image file (optional)')
+ .setLabel('Oder lade eine Bilddatei hoch (optional)')
.setFileUploadComponent(
new FileUploadBuilder()
.setCustomId('image_upload')
@@ -723,16 +733,16 @@ async function handleGoodbyeImage(selectInteraction, rootInteraction, cfg, guild
try {
new URL(imageUrl);
if (!['http:', 'https:'].includes(new URL(imageUrl).protocol)) {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Image URL must start with `http://` or `https://`.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Die Bild-URL muss mit `http://` oder `https://` beginnen.' });
return;
}
} catch {
- await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Please provide a valid image URL.' });
+ await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Bitte gib eine gültige Bild-URL an.' });
return;
}
}
- const nextLeaveEmbed = { ...(cfg.leaveEmbed || {}) };
+ const nextLeaveEmbed: WelcomeEmbedConfig = { ...(cfg.leaveEmbed || {}) };
if (imageUrl) {
nextLeaveEmbed.image = imageUrl;
} else {
@@ -743,14 +753,14 @@ async function handleGoodbyeImage(selectInteraction, rootInteraction, cfg, guild
await saveWelcomeConfig(client, guildId, cfg);
await submitted.reply({
- embeds: [successEmbed('Goodbye Image Updated', `Image ${imageUrl ? 'updated' : 'removed'} successfully.`)],
+ embeds: [successEmbed('Abschiedsbild aktualisiert', `Bild erfolgreich ${imageUrl ? 'aktualisiert' : 'entfernt'}.`)],
flags: MessageFlags.Ephemeral,
});
await refreshDashboard(rootInteraction, cfg, guildId);
}
-async function handleGoodbyePing(selectInteraction, rootInteraction, cfg, guildId, client) {
+async function handleGoodbyePing(selectInteraction: StringSelectMenuInteraction, rootInteraction: ChatInputCommandInteraction, cfg: WelcomeConfig, guildId: string, client: NexoClient) {
if (!await deferComponent(selectInteraction)) {
return;
}
@@ -761,8 +771,8 @@ async function handleGoodbyePing(selectInteraction, rootInteraction, cfg, guildI
await sendEphemeralFollowUp(selectInteraction, {
embeds: [
successEmbed(
- '✅ Goodbye Ping Updated',
- `Leaving users will${cfg.goodbyePing ? '' : ' **not**'} be pinged in the goodbye message.`,
+ '✅ Abschied-Ping aktualisiert',
+ `Gehende Nutzer werden in der Abschiedsnachricht${cfg.goodbyePing ? '' : ' **nicht**'} gepingt.`,
),
],
});
diff --git a/src/commands/Welcome/welcome.js b/src/commands/Welcome/modules/welcome.ts
similarity index 62%
rename from src/commands/Welcome/welcome.js
rename to src/commands/Welcome/modules/welcome.ts
index 840ba7bdd3..d084722d70 100644
--- a/src/commands/Welcome/welcome.js
+++ b/src/commands/Welcome/modules/welcome.ts
@@ -1,36 +1,38 @@
-import { getColor } from '../../config/bot.js';
+import { getColor } from '../../../config/bot.js';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder, MessageFlags } from 'discord.js';
-import { getWelcomeConfig, updateWelcomeConfig } from '../../utils/database.js';
-import { formatWelcomeMessage, truncateForEmbedField } from '../../utils/welcome.js';
-import { logger } from '../../utils/logger.js';
-import { InteractionHelper } from '../../utils/interactionHelper.js';
-import { ErrorTypes, replyUserError } from '../../utils/errorHandler.js';
+import type { ChatInputCommandInteraction } from 'discord.js';
+import { getWelcomeConfig, updateWelcomeConfig } from '../../../utils/database.js';
+import { formatWelcomeMessage, truncateForEmbedField } from '../../../utils/welcome.js';
+import { logger } from '../../../utils/logger.js';
+import { InteractionHelper } from '../../../utils/interactionHelper.js';
+import { ErrorTypes, replyUserError } from '../../../utils/errorHandler.js';
+import type { Command } from '../../../types/index.js';
export default {
data: new SlashCommandBuilder()
.setName('welcome')
- .setDescription('Configure the welcome system')
+ .setDescription('Das Willkommenssystem konfigurieren')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand =>
subcommand
.setName('setup')
- .setDescription('Set up the welcome message')
+ .setDescription('Die Willkommensnachricht einrichten')
.addChannelOption(option =>
option.setName('channel')
- .setDescription('The channel to send welcome messages to')
+ .setDescription('Der Kanal für Willkommensnachrichten')
.addChannelTypes(ChannelType.GuildText)
.setRequired(true))
.addStringOption(option =>
option.setName('message')
- .setDescription('Welcome message. Variables: {user}, {username}, {server}, {memberCount}')
+ .setDescription('Willkommensnachricht. Variablen: {user}, {username}, {server}, {memberCount}')
.setRequired(true))
.addStringOption(option =>
option.setName('image')
- .setDescription('URL of the image to include in the welcome message')
+ .setDescription('URL des Bildes für die Willkommensnachricht')
.setRequired(false))
.addBooleanOption(option =>
option.setName('ping')
- .setDescription('Whether to ping the user in the welcome message')
+ .setDescription('Ob der Nutzer in der Willkommensnachricht gepingt wird')
.setRequired(false))),
async execute(interaction) {
@@ -45,20 +47,20 @@ export default {
return;
}
} catch (deferError) {
- logger.error(`Welcome defer error`, { error: deferError.message });
+ logger.error(`Welcome defer error`, { error: deferError instanceof Error ? deferError.message : String(deferError) });
return;
}
- const { options, guild, client } = interaction;
+ const { options, guild, client } = interaction as ChatInputCommandInteraction<'cached'>;
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
- return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the **Manage Server** permission to use `/welcome`.' });
+ return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'Du brauchst die Berechtigung **Server verwalten**, um `/welcome message` zu nutzen.' });
}
const subcommand = options.getSubcommand();
if (subcommand === 'setup') {
- const channel = options.getChannel('channel');
+ const channel = options.getChannel('channel')!;
const message = options.getString('message');
const image = options.getString('image');
const ping = options.getBoolean('ping') ?? false;
@@ -66,12 +68,12 @@ export default {
const existingConfig = await getWelcomeConfig(client, guild.id);
if (existingConfig?.channelId) {
logger.info(`[Welcome] Setup blocked because config already exists in channel ${existingConfig.channelId} for guild ${guild.id}`);
- return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Welcome is already configured for <#${existingConfig.channelId}>. Use **/greet dashboard** to customize channel, message, ping, or image.` });
+ return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Willkommen ist bereits für <#${existingConfig.channelId}> eingerichtet. Nutze **/welcome greet dashboard**, um Kanal, Nachricht, Ping oder Bild anzupassen.` });
}
if (!message || message.trim().length === 0) {
logger.warn(`[Welcome] Empty message provided by ${interaction.user.tag} in ${guild.name}`);
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Welcome message cannot be empty' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Die Willkommensnachricht darf nicht leer sein.' });
}
if (image) {
@@ -79,7 +81,7 @@ export default {
new URL(image);
} catch (e) {
logger.warn(`[Welcome] Invalid image URL provided by ${interaction.user.tag}: ${image}`);
- return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please provide a valid image URL (must start with http:// or https://' });
+ return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Bitte gib eine gültige Bild-URL an (muss mit http:// oder https:// beginnen).' });
}
}
@@ -101,14 +103,14 @@ export default {
const embed = new EmbedBuilder()
.setColor(getColor('success'))
- .setTitle('Welcome System Configured')
- .setDescription(`Welcome messages will now be sent to ${channel}`)
+ .setTitle('Willkommenssystem eingerichtet')
+ .setDescription(`Willkommensnachrichten werden jetzt in ${channel} gesendet.`)
.addFields(
- { name: 'Message Preview', value: truncateForEmbedField(previewMessage) },
- { name: 'Ping User', value: ping ? 'Yes' : 'No' },
- { name: 'Status', value: 'Enabled' }
+ { name: 'Nachrichtenvorschau', value: truncateForEmbedField(previewMessage) },
+ { name: 'Nutzer pingen', value: ping ? 'Ja' : 'Nein' },
+ { name: 'Status', value: 'Aktiviert' }
)
- .setFooter({ text: 'Tip: Use /greet dashboard to customize welcome settings' });
+ .setFooter({ text: 'Tipp: Mit /welcome greet dashboard passt du die Willkommenseinstellungen an' });
if (image) {
embed.setImage(image);
@@ -117,8 +119,8 @@ export default {
await InteractionHelper.safeEditReply(interaction, { embeds: [embed] });
} catch (error) {
logger.error(`[Welcome] Failed to setup welcome system for guild ${guild.id}:`, error);
- await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while configuring the welcome system. Please try again.' });
+ await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Beim Einrichten des Willkommenssystems ist ein Fehler aufgetreten. Bitte versuch es erneut.' });
}
}
},
-};
\ No newline at end of file
+} satisfies Command;
diff --git a/src/commands/Welcome/welcome.ts b/src/commands/Welcome/welcome.ts
new file mode 100644
index 0000000000..61a87c993c
--- /dev/null
+++ b/src/commands/Welcome/welcome.ts
@@ -0,0 +1,19 @@
+import { PermissionFlagsBits } from 'discord.js';
+import { createRouterCommand } from '../../utils/commandRouter.js';
+import autoroleCommand from './modules/autorole.js';
+import goodbyeCommand from './modules/goodbye.js';
+import greetCommand from './modules/greet.js';
+import welcomeCommand from './modules/welcome.js';
+
+export default createRouterCommand({
+ name: 'welcome',
+ description: 'Einstellungen für Willkommen, Abschied und Auto-Rolle',
+ category: 'Welcome',
+ defaultMemberPermissions: PermissionFlagsBits.ManageGuild,
+ routes: [
+ { legacyName: 'autorole', module: autoroleCommand },
+ { legacyName: 'goodbye', module: goodbyeCommand },
+ { legacyName: 'greet', module: greetCommand },
+ { legacyName: 'welcome', module: welcomeCommand },
+ ],
+});
diff --git a/src/config/application.js b/src/config/application.ts
similarity index 72%
rename from src/config/application.js
rename to src/config/application.ts
index 159b9eb295..5caca52651 100644
--- a/src/config/application.js
+++ b/src/config/application.ts
@@ -3,8 +3,10 @@ import path from "path";
import botConfig, { validateConfig } from "./bot.js";
import { shopConfig as shop } from "./shop/index.js";
import { pgConfig } from "./database/postgres.js";
+import { getEnv } from "./env.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
+const env = getEnv();
const appConfig = {
paths: {
@@ -20,10 +22,10 @@ const appConfig = {
bot: {
...botConfig,
- token: process.env.DISCORD_TOKEN || process.env.TOKEN,
- clientId: process.env.CLIENT_ID,
+ token: env.DISCORD_TOKEN || env.TOKEN,
+ clientId: env.CLIENT_ID,
// Retained for tutorial/setup compatibility; not used for command registration.
- guildId: process.env.GUILD_ID,
+ guildId: env.GUILD_ID,
shop: {
...botConfig.shop,
@@ -37,9 +39,9 @@ const appConfig = {
},
logging: {
- level: process.env.LOG_LEVEL || "info",
+ level: env.LOG_LEVEL || "info",
file: {
- enabled: process.env.LOG_TO_FILE === "true",
+ enabled: env.LOG_TO_FILE === "true",
path: path.join(__dirname, "../../logs"),
maxSize: "20m",
maxFiles: "14d",
@@ -51,16 +53,16 @@ const appConfig = {
timestamp: true,
},
sentry: {
- enabled: process.env.SENTRY_DSN ? true : false,
- dsn: process.env.SENTRY_DSN,
- environment: process.env.NODE_ENV || "development",
+ enabled: env.SENTRY_DSN ? true : false,
+ dsn: env.SENTRY_DSN,
+ environment: env.NODE_ENV || "development",
},
},
api: {
- port: process.env.PORT || 3000,
+ port: env.PORT || 3000,
cors: {
- origin: process.env.CORS_ORIGIN?.split(",") || "*",
+ origin: env.CORS_ORIGIN?.split(",") || "*",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
},
@@ -77,9 +79,9 @@ const appConfig = {
music: botConfig.features?.music ?? true,
},
- env: process.env.NODE_ENV || "development",
- isProduction: process.env.NODE_ENV === "production",
- isDevelopment: process.env.NODE_ENV !== "production",
+ env: env.NODE_ENV || "development",
+ isProduction: env.NODE_ENV === "production",
+ isDevelopment: env.NODE_ENV !== "production",
};
Object.freeze(appConfig);
diff --git a/src/config/bot.js b/src/config/bot.ts
similarity index 71%
rename from src/config/bot.js
rename to src/config/bot.ts
index 86dc861cc3..1e8e705c9e 100644
--- a/src/config/bot.js
+++ b/src/config/bot.ts
@@ -1,4 +1,8 @@
+import type { Guild, PresenceData } from 'discord.js';
import { logger } from '../utils/logger.js';
+import { getEnv } from './env.js';
+
+const env = getEnv();
export const botConfig = {
// =========================
@@ -24,11 +28,11 @@ export const botConfig = {
activities: [
{
name: "Custom Status", // required by Discord API, not shown in the client
- state: "stalking", // this is what people actually see
+ state: "auf der Lauer", // this is what people actually see
type: 4, // Custom
},
],
- },
+ } satisfies PresenceData,
// =========================
// COMMAND BEHAVIOR
@@ -36,7 +40,7 @@ export const botConfig = {
commands: {
// Bot owner user IDs (comma-separated in OWNER_IDS env var).
// Owners can access owner/admin-level bot commands.
- owners: process.env.OWNER_IDS?.split(",").map((id) => id.trim()).filter(Boolean) || [],
+ owners: env.OWNER_IDS?.split(",").map((id) => id.trim()).filter(Boolean) || [],
// Default wait time between command uses (in seconds).
defaultCooldown: 3,
@@ -45,14 +49,14 @@ export const botConfig = {
deleteCommands: false,
// Optional server ID retained for tutorial compatibility; not used for command registration.
- testGuildId: process.env.TEST_GUILD_ID,
+ testGuildId: env.TEST_GUILD_ID,
// When true (or MAINTENANCE_MODE=true), only bot owners can run commands.
- maintenanceMode: process.env.MAINTENANCE_MODE === "true",
+ maintenanceMode: env.MAINTENANCE_MODE === "true",
// Command prefix for text-based commands (e.g., "!" for "!ping").
// Supports both slash commands and prefix commands.
- prefix: process.env.PREFIX || "!",
+ prefix: env.PREFIX || "!",
},
// =========================
@@ -61,9 +65,9 @@ export const botConfig = {
applications: {
// Default questions shown when someone fills out an application.
defaultQuestions: [
- { question: "What is your name?", required: true },
- { question: "How old are you?", required: true },
- { question: "Why do you want to join?", required: true },
+ { question: "Wie heißt du?", required: true },
+ { question: "Wie alt bist du?", required: true },
+ { question: "Warum möchtest du beitreten?", required: true },
],
// Embed colors by application status.
@@ -83,7 +87,7 @@ export const botConfig = {
deleteApprovedAfter: 30,
// Role IDs allowed to manage applications.
- managerRoles: [], // Will be populated from environment or database
+ managerRoles: [] as string[], // Will be populated from environment or database
},
// =========================
@@ -93,8 +97,8 @@ export const botConfig = {
embeds: {
colors: {
// Main brand colors.
- primary: "#336699",
- secondary: "#2F3136",
+ primary: "#7C5CFF",
+ secondary: "#22D3EE",
// Standard status colors for success/error/warning/info messages.
success: "#57F287",
@@ -141,17 +145,17 @@ export const botConfig = {
},
footer: {
// Default footer text used in bot embeds.
- text: "Titan Bot",
+ text: "Nexo",
// Footer icon URL (null = no icon).
- icon: null,
+ icon: null as string | null,
},
// Default thumbnail URL for embeds (null = no thumbnail).
- thumbnail: null,
+ thumbnail: null as string | null,
author: {
// Optional default embed author block.
- name: null,
- icon: null,
- url: null,
+ name: null as string | null,
+ icon: null as string | null,
+ url: null as string | null,
},
},
@@ -161,9 +165,9 @@ export const botConfig = {
economy: {
currency: {
// Currency display name.
- name: "coins",
+ name: "Münzen",
// Plural display name.
- namePlural: "coins",
+ namePlural: "Münzen",
// Currency symbol shown in balances.
symbol: "$",
},
@@ -214,37 +218,37 @@ export const botConfig = {
// =========================
tickets: {
// Category ID where new tickets are created (null = no forced category).
- defaultCategory: null,
+ defaultCategory: null as string | null,
// Role IDs allowed to manage/support tickets.
- supportRoles: [],
+ supportRoles: [] as string[],
// Priority options users/staff can assign.
priorities: {
none: {
emoji: "⚪",
color: "#95A5A6",
- label: "None",
+ label: "Keine",
},
low: {
emoji: "🟢",
color: "#2ECC71",
- label: "Low",
+ label: "Niedrig",
},
medium: {
emoji: "🟡",
color: "#F1C40F",
- label: "Medium",
+ label: "Mittel",
},
high: {
emoji: "🔴",
color: "#E74C3C",
- label: "High",
+ label: "Hoch",
},
urgent: {
emoji: "🚨",
color: "#E91E63",
- label: "Urgent",
+ label: "Dringend",
},
},
@@ -252,10 +256,10 @@ export const botConfig = {
defaultPriority: "none",
// Category ID where closed tickets are archived.
- archiveCategory: null,
+ archiveCategory: null as string | null,
// Channel ID where ticket logs are sent.
- logChannel: null,
+ logChannel: null as string | null,
},
// =========================
@@ -277,10 +281,10 @@ export const botConfig = {
maximumDuration: 2592000000,
// Role IDs allowed to host giveaways.
- allowedRoles: [],
+ allowedRoles: [] as string[],
// Role IDs that bypass giveaway restrictions.
- bypassRoles: [],
+ bypassRoles: [] as string[],
},
// =========================
@@ -288,10 +292,10 @@ export const botConfig = {
// =========================
birthday: {
// Role ID given to users on their birthday.
- defaultRole: null,
+ defaultRole: null as string | null,
// Channel ID where birthday announcements are posted.
- announcementChannel: null,
+ announcementChannel: null as string | null,
// Timezone used to calculate birthday dates.
timezone: "UTC",
@@ -302,10 +306,10 @@ export const botConfig = {
// =========================
verification: {
// Message shown when posting the verification panel.
- defaultMessage: "Click the button below to verify yourself and gain access to the server!",
+ defaultMessage: "Klicke auf den Button unten, um dich zu verifizieren und Zugang zum Server zu erhalten!",
// Text on the verification button.
- defaultButtonText: "Verify",
+ defaultButtonText: "Verifizieren",
// Automatic verification behavior.
autoVerify: {
@@ -332,9 +336,9 @@ export const botConfig = {
// Human-readable descriptions for each criteria mode.
criteria: {
- account_age: "Account must be older than specified days",
- server_size: "All users if server has less than 1000 members",
- none: "All users immediately"
+ account_age: "Account muss älter als die angegebene Anzahl Tage sein",
+ server_size: "Alle Nutzer, wenn der Server weniger als 1000 Mitglieder hat",
+ none: "Alle Nutzer sofort"
}
},
@@ -372,15 +376,15 @@ export const botConfig = {
// Welcome template posted when a user joins.
// Placeholders: {user}, {server}, {memberCount}
defaultWelcomeMessage:
- "Welcome {user} to {server}! We now have {memberCount} members!",
+ "Willkommen {user} auf {server}! Wir sind jetzt {memberCount} Mitglieder!",
// Goodbye template posted when a user leaves.
// Placeholders: {user}, {memberCount}
defaultGoodbyeMessage:
- "{user} has left the server. We now have {memberCount} members.",
+ "{user} hat den Server verlassen. Wir sind jetzt {memberCount} Mitglieder.",
// Channel ID for welcome messages.
- defaultWelcomeChannel: null,
+ defaultWelcomeChannel: null as string | null,
// Channel ID for goodbye messages.
- defaultGoodbyeChannel: null,
+ defaultGoodbyeChannel: null as string | null,
},
// =========================
@@ -389,8 +393,8 @@ export const botConfig = {
counters: {
defaults: {
// Default naming/description templates for counter entries.
- name: "{name} Counter",
- description: "Server {name} counter",
+ name: "{name}-Zähler",
+ description: "Server-Zähler: {name}",
// Channel type used for counters (typically "voice").
type: "voice",
// Channel name format. `{count}` is replaced automatically.
@@ -404,27 +408,27 @@ export const botConfig = {
},
messages: {
// Default response messages for counter actions.
- created: "✅ Created counter **{name}**",
- deleted: "🗑️ Deleted counter **{name}**",
- updated: "🔄 Updated counter **{name}**",
+ created: "✅ Zähler **{name}** erstellt",
+ deleted: "🗑️ Zähler **{name}** gelöscht",
+ updated: "🔄 Zähler **{name}** aktualisiert",
},
types: {
// Built-in counter types and how each count is calculated.
members: {
- name: "👥 Members",
- description: "Total members in the server",
- getCount: (guild) => guild.memberCount.toString(),
+ name: "👥 Mitglieder",
+ description: "Mitglieder insgesamt auf dem Server",
+ getCount: (guild: Guild) => guild.memberCount.toString(),
},
bots: {
name: "🤖 Bots",
- description: "Total bot accounts in the server",
- getCount: (guild) =>
+ description: "Bot-Accounts insgesamt auf dem Server",
+ getCount: (guild: Guild) =>
guild.members.cache.filter((m) => m.user.bot).size.toString(),
},
members_only: {
- name: "👤 Humans",
- description: "Total human members (non-bots)",
- getCount: (guild) =>
+ name: "👤 Menschen",
+ description: "Menschliche Mitglieder insgesamt (ohne Bots)",
+ getCount: (guild: Guild) =>
guild.members.cache.filter((m) => !m.user.bot).size.toString(),
},
},
@@ -434,13 +438,13 @@ export const botConfig = {
// GENERIC BOT MESSAGES
// =========================
messages: {
- noPermission: "You do not have permission to use this command.",
- cooldownActive: "Please wait {time} before using this command again.",
- errorOccurred: "An error occurred while executing this command.",
+ noPermission: "Du hast keine Berechtigung, diesen Command zu nutzen.",
+ cooldownActive: "Bitte warte {time}, bevor du diesen Command erneut nutzt.",
+ errorOccurred: "Beim Ausführen dieses Commands ist ein Fehler aufgetreten.",
missingPermissions:
- "I am missing required permissions to perform this action.",
- commandDisabled: "This command has been disabled.",
- maintenanceMode: "The bot is currently in maintenance mode.",
+ "Mir fehlen die nötigen Berechtigungen für diese Aktion.",
+ commandDisabled: "Dieser Command wurde deaktiviert.",
+ maintenanceMode: "Der Bot befindet sich gerade im Wartungsmodus.",
},
// =========================
@@ -477,40 +481,44 @@ export const botConfig = {
},
};
-export function validateConfig(config) {
- const errors = [];
+export type BotConfigShape = typeof botConfig;
+export type FeatureKey = keyof BotConfigShape["features"];
+
+export function validateConfig(config: BotConfigShape): string[] {
+ const errors: string[] = [];
+ const env = getEnv();
- if (process.env.NODE_ENV !== 'production') {
+ if (env.NODE_ENV !== 'production') {
logger.debug('Environment variables check:');
- logger.debug('DISCORD_TOKEN exists:', !!process.env.DISCORD_TOKEN);
- logger.debug('TOKEN exists:', !!process.env.TOKEN);
- logger.debug('CLIENT_ID exists:', !!process.env.CLIENT_ID);
- logger.debug('GUILD_ID exists:', !!process.env.GUILD_ID);
- logger.debug('POSTGRES_HOST exists:', !!process.env.POSTGRES_HOST);
- logger.debug('NODE_ENV:', process.env.NODE_ENV);
+ logger.debug('DISCORD_TOKEN exists:', !!env.DISCORD_TOKEN);
+ logger.debug('TOKEN exists:', !!env.TOKEN);
+ logger.debug('CLIENT_ID exists:', !!env.CLIENT_ID);
+ logger.debug('GUILD_ID exists:', !!env.GUILD_ID);
+ logger.debug('POSTGRES_HOST exists:', !!env.POSTGRES_HOST);
+ logger.debug('NODE_ENV:', env.NODE_ENV);
}
- if (!process.env.DISCORD_TOKEN && !process.env.TOKEN) {
+ if (!env.DISCORD_TOKEN && !env.TOKEN) {
errors.push("Bot token is required (DISCORD_TOKEN or TOKEN environment variable)");
}
- if (!process.env.CLIENT_ID) {
+ if (!env.CLIENT_ID) {
errors.push("Client ID is required (CLIENT_ID environment variable)");
}
- if (process.env.NODE_ENV === 'production') {
+ if (env.NODE_ENV === 'production') {
// A full connection URL (DATABASE_URL / POSTGRES_URL) satisfies all Postgres
// requirements, matching how src/config/database/postgres.js resolves the pool config.
- const hasConnectionUrl = Boolean(process.env.POSTGRES_URL || process.env.DATABASE_URL);
+ const hasConnectionUrl = Boolean(env.POSTGRES_URL || env.DATABASE_URL);
if (!hasConnectionUrl) {
- if (!process.env.POSTGRES_HOST) {
+ if (!env.POSTGRES_HOST) {
errors.push("PostgreSQL connection is required in production (set DATABASE_URL/POSTGRES_URL, or POSTGRES_HOST)");
}
- if (!process.env.POSTGRES_USER) {
+ if (!env.POSTGRES_USER) {
errors.push("PostgreSQL user is required in production (set DATABASE_URL/POSTGRES_URL, or POSTGRES_USER)");
}
- if (!process.env.POSTGRES_PASSWORD) {
+ if (!env.POSTGRES_PASSWORD) {
errors.push("PostgreSQL password is required in production (set DATABASE_URL/POSTGRES_URL, or POSTGRES_PASSWORD)");
}
}
@@ -522,14 +530,14 @@ export function validateConfig(config) {
const configErrors = validateConfig(botConfig);
if (configErrors.length > 0) {
logger.error("Bot configuration errors:", configErrors.join("\n"));
- if (process.env.NODE_ENV === "production") {
+ if (env.NODE_ENV === "production") {
process.exit(1);
}
}
export const BotConfig = botConfig;
-const COMMAND_CATEGORY_FEATURE_MAP = {
+const COMMAND_CATEGORY_FEATURE_MAP: Record = {
birthday: "birthday",
community: "community",
economy: "economy",
@@ -550,21 +558,21 @@ const COMMAND_CATEGORY_FEATURE_MAP = {
welcome: "welcome",
};
-function normalizeCategoryKey(category) {
+function normalizeCategoryKey(category: string | null | undefined): string {
return String(category || "").trim().toLowerCase().replace(/\s+/g, "_");
}
-export function getCommandPrefix() {
+export function getCommandPrefix(): string {
return botConfig.commands?.prefix ?? "!";
}
-export function getBotOwners() {
+export function getBotOwners(): string[] {
return (botConfig.commands?.owners ?? [])
.map((id) => String(id).trim())
.filter(Boolean);
}
-export function isBotOwner(userId) {
+export function isBotOwner(userId: string | null | undefined): boolean {
if (!userId) {
return false;
}
@@ -572,12 +580,13 @@ export function isBotOwner(userId) {
return getBotOwners().includes(String(userId));
}
-export function isMaintenanceMode() {
+export function isMaintenanceMode(): boolean {
return botConfig.commands?.maintenanceMode === true;
}
-export function getBotMessage(key, replacements = {}) {
- let message = botConfig.messages?.[key] || key;
+export function getBotMessage(key: string, replacements: Record = {}): string {
+ const messages: Partial> = botConfig.messages;
+ let message = messages?.[key] || key;
for (const [placeholder, value] of Object.entries(replacements)) {
message = message.replace(new RegExp(`\\{${placeholder}\\}`, "g"), String(value));
@@ -586,22 +595,23 @@ export function getBotMessage(key, replacements = {}) {
return message;
}
-export function isFeatureEnabled(featureKey) {
+export function isFeatureEnabled(featureKey: string | null | undefined): boolean {
if (!featureKey) {
return true;
}
- return botConfig.features?.[featureKey] !== false;
+ const features: Partial> = botConfig.features;
+ return features?.[featureKey] !== false;
}
-export function isCommandCategoryEnabled(category) {
+export function isCommandCategoryEnabled(category: string | null | undefined): boolean {
const normalized = normalizeCategoryKey(category);
if (!normalized || normalized === "core") {
return true;
}
- const featureKey = COMMAND_CATEGORY_FEATURE_MAP[normalized];
+ const featureKey: FeatureKey | undefined = COMMAND_CATEGORY_FEATURE_MAP[normalized];
if (!featureKey) {
return true;
}
@@ -609,19 +619,19 @@ export function isCommandCategoryEnabled(category) {
return isFeatureEnabled(featureKey);
}
-export function getApplicationStatusColor(status) {
- const colors = botConfig.applications?.statusColors || {};
+export function getApplicationStatusColor(status: string): number {
+ const colors: Partial> = botConfig.applications?.statusColors || {};
const hex = colors[status];
return hex ? getColor(hex) : getColor(status === "approved" ? "success" : status === "denied" ? "error" : "warning");
}
-export function getDefaultApplicationQuestions() {
+export function getDefaultApplicationQuestions(): string[] {
return (botConfig.applications?.defaultQuestions || []).map((entry) =>
typeof entry === "string" ? entry : entry.question,
).filter(Boolean);
}
-export function getColor(path, fallback = "#99AAB5") {
+export function getColor(path: string | number, fallback: string = "#99AAB5"): number {
if (typeof path === "number") return path;
if (typeof path === "string" && path.startsWith("#")) {
@@ -630,18 +640,19 @@ export function getColor(path, fallback = "#99AAB5") {
}
const result = path
.split(".")
- .reduce(
- (obj, key) => (obj && obj[key] !== undefined ? obj[key] : fallback),
+ .reduce(
+ (obj, key) => (obj && (obj as Record)[key] !== undefined ? (obj as Record)[key] : fallback),
botConfig.embeds.colors,
);
if (typeof result === "string" && result.startsWith("#")) {
return parseInt(result.replace("#", ""), 16);
}
- return result;
+ // Non-hex results (a color group or a non-hex fallback) are passed through unchanged, as before.
+ return result as number;
}
-export function getRandomColor() {
+export function getRandomColor(): string {
const colors = Object.values(botConfig.embeds.colors).flatMap((color) =>
typeof color === "string" ? color : Object.values(color),
);
diff --git a/src/config/commands/commandAliases.js b/src/config/commands/commandAliases.ts
similarity index 92%
rename from src/config/commands/commandAliases.js
rename to src/config/commands/commandAliases.ts
index b23023a143..99d656924f 100644
--- a/src/config/commands/commandAliases.js
+++ b/src/config/commands/commandAliases.ts
@@ -3,7 +3,7 @@
* Maps shortened command names to their full command names
*/
-export const commandAliases = {
+export const commandAliases: Record = {
'bal': 'balance',
'money': 'balance',
'cash': 'balance',
@@ -23,7 +23,6 @@ export const commandAliases = {
'ping': 'ping',
'help': 'help',
'h': 'help',
- 'info': 'help',
'ban': 'ban',
'kick': 'kick',
@@ -106,7 +105,7 @@ export const commandAliases = {
'now': 'nowplaying',
};
-export const subcommandAliases = {
+export const subcommandAliases: Record = {
'l': 'list',
'ls': 'list',
's': 'set',
@@ -136,7 +135,7 @@ export const subcommandAliases = {
* @param {string} commandName - The command name (could be an alias)
* @returns {string} - The full command name, or the original if not an alias
*/
-export function resolveCommandAlias(commandName) {
+export function resolveCommandAlias(commandName: string): string {
const normalized = commandName.toLowerCase();
return commandAliases[normalized] || commandName;
}
@@ -146,7 +145,7 @@ export function resolveCommandAlias(commandName) {
* @param {string} subcommandName - The subcommand name (could be an alias)
* @returns {string} - The full subcommand name, or the original if not an alias
*/
-export function resolveSubcommandAlias(subcommandName) {
+export function resolveSubcommandAlias(subcommandName: string): string {
const normalized = subcommandName.toLowerCase();
return subcommandAliases[normalized] || subcommandName;
}
diff --git a/src/config/commands/commandCategories.js b/src/config/commands/commandCategories.js
deleted file mode 100644
index 3d3ec1b5ee..0000000000
--- a/src/config/commands/commandCategories.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Command category metadata for the command access manager.
- */
-
-export const CATEGORY_ICONS = {
- Birthday: '🎂',
- Community: '👥',
- Core: 'ℹ️',
- Economy: '💰',
- Fun: '🎮',
- Giveaway: '🎉',
- JoinToCreate: '🔌',
- Leveling: '📊',
- Logging: '📝',
- Moderation: '🛡️',
- Music: '🎵',
- Reaction_roles: '🎭',
- Search: '🔍',
- ServerStats: '📈',
- Ticket: '🎫',
- Tools: '🛠️',
- Utility: '🔧',
- Verification: '✅',
- Welcome: '👋',
-};
-
-/** Commands that always stay available so admins can recover access. */
-export const PROTECTED_COMMANDS = new Set(['commands', 'configwizard']);
-
-export function normalizeCategoryKey(category) {
- return String(category || '')
- .trim()
- .toLowerCase()
- .replace(/\s+/g, '_');
-}
-
-export function formatCategoryName(rawCategory) {
- return String(rawCategory || '')
- .replace(/_/g, ' ')
- .replace(/([a-z])([A-Z])/g, '$1 $2')
- .replace(/\b\w/g, (char) => char.toUpperCase());
-}
-
-export function getCategoryIcon(category) {
- return CATEGORY_ICONS[category] || CATEGORY_ICONS[formatCategoryName(category)] || '📁';
-}
diff --git a/src/config/commands/commandCategories.ts b/src/config/commands/commandCategories.ts
new file mode 100644
index 0000000000..990046f26d
--- /dev/null
+++ b/src/config/commands/commandCategories.ts
@@ -0,0 +1,75 @@
+/**
+ * Command category metadata for the command access manager.
+ */
+
+export const CATEGORY_ICONS: Record = {
+ Birthday: '🎂',
+ Community: '👥',
+ Core: 'ℹ️',
+ Economy: '💰',
+ Fun: '🎮',
+ Giveaway: '🎉',
+ JoinToCreate: '🔌',
+ Leveling: '📊',
+ Logging: '📝',
+ Moderation: '🛡️',
+ Music: '🎵',
+ Reaction_roles: '🎭',
+ Search: '🔍',
+ ServerStats: '📈',
+ Ticket: '🎫',
+ Tools: '🛠️',
+ Utility: '🔧',
+ Verification: '✅',
+ Welcome: '👋',
+};
+
+/** German display labels by command folder name (folder keys stay English: they are stored as disabledCategories keys). */
+export const CATEGORY_LABELS: Record = {
+ Birthday: 'Geburtstag',
+ Community: 'Community',
+ Core: 'Allgemein',
+ Economy: 'Wirtschaft',
+ Fun: 'Spaß',
+ Giveaway: 'Giveaways',
+ JoinToCreate: 'Join-to-Create',
+ Leveling: 'Leveling',
+ Logging: 'Logging',
+ Moderation: 'Moderation',
+ Music: 'Musik',
+ Reaction_roles: 'Reaktionsrollen',
+ Search: 'Suche',
+ ServerStats: 'Server-Statistiken',
+ Ticket: 'Tickets',
+ Tools: 'Tools',
+ Utility: 'Nützliches',
+ Verification: 'Verifizierung',
+ Welcome: 'Willkommen',
+};
+
+/** Commands that always stay available so admins can recover access. */
+export const PROTECTED_COMMANDS = new Set(['commands', 'configwizard']);
+
+export function normalizeCategoryKey(category: string | null | undefined): string {
+ return String(category || '')
+ .trim()
+ .toLowerCase()
+ .replace(/\s+/g, '_');
+}
+
+/** English title-cased folder name (e.g. "Reaction_roles" -> "Reaction Roles"). */
+function formatFolderName(rawCategory: string | null | undefined): string {
+ return String(rawCategory || '')
+ .replace(/_/g, ' ')
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
+ .replace(/\b\w/g, (char) => char.toUpperCase());
+}
+
+/** German display label for a category folder; falls back to the title-cased folder name. */
+export function formatCategoryName(rawCategory: string | null | undefined): string {
+ return CATEGORY_LABELS[String(rawCategory || '')] ?? formatFolderName(rawCategory);
+}
+
+export function getCategoryIcon(category: string): string {
+ return CATEGORY_ICONS[category] || CATEGORY_ICONS[formatFolderName(category)] || '📁';
+}
diff --git a/src/config/commands/legacyCommandMap.ts b/src/config/commands/legacyCommandMap.ts
new file mode 100644
index 0000000000..ee3dfbf4be
--- /dev/null
+++ b/src/config/commands/legacyCommandMap.ts
@@ -0,0 +1,219 @@
+/**
+ * Legacy command map: pre-consolidation top-level commands -> their new location.
+ *
+ * Single source of truth for everything that still knows the old names:
+ * - command access checks (stored `disabledCommands` keys use old names, no data migration)
+ * - prefix routing (`!bal` -> `/eco balance`) and aliases
+ * - help menus ("formerly /balance")
+ * - docs/COMMAND_MIGRATION.md
+ *
+ * An entry with `subcommand` maps an old command without subcommands to one subcommand.
+ * An entry with only `group` maps an old command with subcommands to a subcommand group;
+ * the old subcommand names are kept inside that group.
+ */
+
+export interface LegacyCommandTarget {
+ command: string;
+ group?: string;
+ subcommand?: string;
+ /** Category folder the old command lived in (for disabledCategories). */
+ category: string;
+}
+
+export const LEGACY_COMMAND_MAP: Readonly> = {
+ // Economy
+ balance: { command: 'eco', subcommand: 'balance', category: 'Economy' },
+ beg: { command: 'eco', subcommand: 'beg', category: 'Economy' },
+ buy: { command: 'eco', subcommand: 'buy', category: 'Economy' },
+ crime: { command: 'eco', subcommand: 'crime', category: 'Economy' },
+ daily: { command: 'eco', subcommand: 'daily', category: 'Economy' },
+ deposit: { command: 'eco', subcommand: 'deposit', category: 'Economy' },
+ eleaderboard: { command: 'eco', subcommand: 'leaderboard', category: 'Economy' },
+ fish: { command: 'eco', subcommand: 'fish', category: 'Economy' },
+ gamble: { command: 'eco', subcommand: 'gamble', category: 'Economy' },
+ inventory: { command: 'eco', subcommand: 'inventory', category: 'Economy' },
+ mine: { command: 'eco', subcommand: 'mine', category: 'Economy' },
+ pay: { command: 'eco', subcommand: 'pay', category: 'Economy' },
+ rob: { command: 'eco', subcommand: 'rob', category: 'Economy' },
+ shop: { command: 'eco', subcommand: 'shop', category: 'Economy' },
+ slut: { command: 'eco', subcommand: 'slut', category: 'Economy' },
+ withdraw: { command: 'eco', subcommand: 'withdraw', category: 'Economy' },
+ work: { command: 'eco', subcommand: 'work', category: 'Economy' },
+ 'shop-config': { command: 'economy', group: 'shop', category: 'Economy' },
+
+ // Music
+ join: { command: 'music', subcommand: 'join', category: 'Music' },
+ play: { command: 'music', subcommand: 'play', category: 'Music' },
+ queue: { command: 'music', subcommand: 'queue', category: 'Music' },
+ nowplaying: { command: 'music', subcommand: 'nowplaying', category: 'Music' },
+
+ // Giveaway
+ gcreate: { command: 'giveaway', subcommand: 'create', category: 'Giveaway' },
+ gdelete: { command: 'giveaway', subcommand: 'delete', category: 'Giveaway' },
+ gend: { command: 'giveaway', subcommand: 'end', category: 'Giveaway' },
+ greroll: { command: 'giveaway', subcommand: 'reroll', category: 'Giveaway' },
+
+ // Leveling
+ leveladd: { command: 'level', subcommand: 'add', category: 'Leveling' },
+ levelremove: { command: 'level', subcommand: 'remove', category: 'Leveling' },
+ levelset: { command: 'level', subcommand: 'set', category: 'Leveling' },
+ rank: { command: 'rank', subcommand: 'view', category: 'Leveling' },
+ leaderboard: { command: 'rank', subcommand: 'leaderboard', category: 'Leveling' },
+
+ // Tools
+ baseconvert: { command: 'tools', subcommand: 'baseconvert', category: 'Tools' },
+ calculate: { command: 'tools', subcommand: 'calculate', category: 'Tools' },
+ countdown: { command: 'tools', subcommand: 'countdown', category: 'Tools' },
+ generatepassword: { command: 'tools', subcommand: 'generatepassword', category: 'Tools' },
+ hexcolor: { command: 'tools', subcommand: 'hexcolor', category: 'Tools' },
+ poll: { command: 'tools', subcommand: 'poll', category: 'Tools' },
+ randomuser: { command: 'tools', subcommand: 'randomuser', category: 'Tools' },
+ shorten: { command: 'tools', subcommand: 'shorten', category: 'Tools' },
+ time: { command: 'tools', subcommand: 'time', category: 'Tools' },
+ unixtime: { command: 'tools', subcommand: 'unixtime', category: 'Tools' },
+ weather: { command: 'tools', subcommand: 'weather', category: 'Utility' },
+ firstmsg: { command: 'tools', subcommand: 'firstmsg', category: 'Utility' },
+
+ // Info
+ avatar: { command: 'info', subcommand: 'avatar', category: 'Utility' },
+ serverinfo: { command: 'info', subcommand: 'server', category: 'Utility' },
+ userinfo: { command: 'info', subcommand: 'user', category: 'Utility' },
+
+ // Bot
+ ping: { command: 'bot', subcommand: 'ping', category: 'Core' },
+ stats: { command: 'bot', subcommand: 'stats', category: 'Core' },
+ support: { command: 'bot', subcommand: 'support', category: 'Core' },
+ uptime: { command: 'bot', subcommand: 'uptime', category: 'Core' },
+
+ // Fun
+ fight: { command: 'fun', subcommand: 'fight', category: 'Fun' },
+ flip: { command: 'fun', subcommand: 'flip', category: 'Fun' },
+ roll: { command: 'fun', subcommand: 'roll', category: 'Fun' },
+
+ // Welcome
+ autorole: { command: 'welcome', group: 'autorole', category: 'Welcome' },
+ goodbye: { command: 'welcome', group: 'goodbye', category: 'Welcome' },
+ greet: { command: 'welcome', group: 'greet', category: 'Welcome' },
+ welcome: { command: 'welcome', group: 'message', category: 'Welcome' },
+
+ // Verification
+ autoverify: { command: 'verification', group: 'autoverify', category: 'Verification' },
+
+ // Tickets
+ claim: { command: 'tickets', subcommand: 'claim', category: 'Ticket' },
+ close: { command: 'tickets', subcommand: 'close', category: 'Ticket' },
+ priority: { command: 'tickets', subcommand: 'priority', category: 'Ticket' },
+
+ // Moderation
+ ban: { command: 'ban', subcommand: 'user', category: 'Moderation' },
+ unban: { command: 'ban', subcommand: 'remove', category: 'Moderation' },
+ massban: { command: 'ban', subcommand: 'mass', category: 'Moderation' },
+ kick: { command: 'kick', subcommand: 'user', category: 'Moderation' },
+ masskick: { command: 'kick', subcommand: 'mass', category: 'Moderation' },
+ timeout: { command: 'mod', subcommand: 'timeout', category: 'Moderation' },
+ untimeout: { command: 'mod', subcommand: 'untimeout', category: 'Moderation' },
+ warn: { command: 'mod', subcommand: 'warn', category: 'Moderation' },
+ warnings: { command: 'mod', subcommand: 'warnings', category: 'Moderation' },
+ dm: { command: 'mod', subcommand: 'dm', category: 'Moderation' },
+ lock: { command: 'channel', subcommand: 'lock', category: 'Moderation' },
+ unlock: { command: 'channel', subcommand: 'unlock', category: 'Moderation' },
+ purge: { command: 'msg', subcommand: 'purge', category: 'Moderation' },
+ say: { command: 'msg', subcommand: 'say', category: 'Moderation' },
+ embedbuilder: { command: 'msg', subcommand: 'embed', category: 'Tools' },
+ usernotes: { command: 'msg', group: 'notes', category: 'Moderation' },
+};
+
+/** First-level names (subcommands or groups) a legacy entry occupies in its new command. */
+function firstLevelName(target: LegacyCommandTarget): string {
+ return (target.group ?? target.subcommand)!;
+}
+
+const NEW_FIRST_LEVEL_NAMES = new Map>();
+for (const target of Object.values(LEGACY_COMMAND_MAP)) {
+ const names = NEW_FIRST_LEVEL_NAMES.get(target.command) ?? new Set();
+ names.add(firstLevelName(target));
+ NEW_FIRST_LEVEL_NAMES.set(target.command, names);
+}
+
+/** New path tokens for a legacy target, e.g. ['eco', 'balance'] or ['msg', 'notes']. */
+export function getLegacyPath(target: LegacyCommandTarget): string[] {
+ return [target.command, firstLevelName(target)];
+}
+
+/** `/eco balance` style display string for a legacy command name. */
+
+/**
+ * Whether `firstArg` is already a valid first-level name of the new command.
+ * Used for old names that equal a new top-level name (ban, kick, rank, welcome).
+ */
+function isNewFirstLevelName(command: string, firstArg: string | undefined): boolean {
+ return Boolean(firstArg && NEW_FIRST_LEVEL_NAMES.get(command)?.has(firstArg.toLowerCase()));
+}
+
+/** Suffix marking a stored "whole command" disable for names that existed before and after consolidation. */
+export const WHOLE_COMMAND_MARKER = '*';
+
+/**
+ * Translate a stored access key (old or new naming) to the new naming.
+ * `balance` -> `eco balance`, `usernotes add` -> `msg notes add`, `welcome setup` -> `welcome message setup`.
+ * A bare old name keeps its old meaning (`ban` -> `ban user`); disabling a whole new command with such a
+ * name is stored as `ban *`.
+ */
+export function translateLegacyAccessKey(key: string): string {
+ const tokens = key.toLowerCase().trim().split(/\s+/);
+ if (tokens[1] === WHOLE_COMMAND_MARKER) {
+ return tokens[0];
+ }
+
+ const target = LEGACY_COMMAND_MAP[tokens[0]];
+ if (!target || (target.command === tokens[0] && isNewFirstLevelName(target.command, tokens[1]))) {
+ return tokens.join(' ');
+ }
+
+ return [...getLegacyPath(target), ...tokens.slice(1)].join(' ');
+}
+
+/** Key to store when disabling `name` (new naming), so translateLegacyAccessKey reads it back unchanged. */
+export function toStoredAccessKey(name: string): string {
+ return LEGACY_COMMAND_MAP[name]?.command === name ? `${name} ${WHOLE_COMMAND_MARKER}` : name;
+}
+
+/**
+ * Rewrite a prefix invocation that uses an old command name.
+ * Returns null when the name is not a legacy name or the args already address the new structure.
+ */
+export function routeLegacyPrefixCommand(
+ commandName: string,
+ args: string[],
+): { commandName: string; args: string[] } | null {
+ const target = LEGACY_COMMAND_MAP[commandName.toLowerCase()];
+ if (!target) {
+ return null;
+ }
+
+ if (target.command === commandName.toLowerCase() && isNewFirstLevelName(target.command, args[0])) {
+ return null;
+ }
+
+ return { commandName: target.command, args: [firstLevelName(target), ...args] };
+}
+
+/**
+ * Old command name for a new path, e.g. ('mod', null, 'warn') -> 'warn', ('msg', 'notes', 'add') -> 'usernotes'.
+ * Returns null when the path is not a moved legacy command.
+ */
+export function resolveLegacyName(command: string, group: string | null, subcommand: string | null): string | null {
+ const firstLevel = group ?? subcommand;
+ if (!firstLevel) {
+ return null;
+ }
+
+ for (const [legacyName, target] of Object.entries(LEGACY_COMMAND_MAP)) {
+ if (target.command === command && firstLevelName(target) === firstLevel && Boolean(target.group) === Boolean(group)) {
+ return legacyName;
+ }
+ }
+
+ return null;
+}
+
diff --git a/src/config/commands/prefixRestrictions.js b/src/config/commands/prefixRestrictions.js
deleted file mode 100644
index 2b57f7bd60..0000000000
--- a/src/config/commands/prefixRestrictions.js
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * Prefix command restrictions — dashboard and advanced setup flows stay slash-only.
- */
-
-/** Top-level commands that cannot be invoked via prefix at all. */
-export const SLASH_ONLY_COMMANDS = new Set([
- 'configwizard',
- 'help',
- 'embedbuilder',
- 'wipedata',
- 'apply',
-]);
-
-/** Subcommands blocked for every command when invoked via prefix. */
-export const GLOBAL_BLOCKED_SUBCOMMANDS = new Set([
- 'dashboard',
- 'setup',
-]);
-
-/** Subcommand groups blocked for every command when invoked via prefix. */
-export const GLOBAL_BLOCKED_SUBCOMMAND_GROUPS = new Set([
- 'config',
-]);
-
-/** Per-command subcommands that stay slash-only (beyond the global block list). */
-export const COMMAND_BLOCKED_SUBCOMMANDS = {
- music: new Set([
- 'shuffle',
- 'loop',
- 'seek',
- 'remove',
- 'move',
- 'clear',
- '247',
- ]),
- birthday: new Set(['setchannel']),
- report: new Set(['setchannel']),
-};
-
-function collectSubcommandNames(commandJson) {
- const subcommandGroup = commandJson.options?.find((opt) => opt.type === 2);
-
- if (subcommandGroup) {
- const names = [];
- for (const group of subcommandGroup.options || []) {
- names.push(...(group.options?.map((opt) => opt.name) || []));
- }
- return names;
- }
-
- return (commandJson.options?.filter((opt) => opt.type === 1) || []).map((sub) => sub.name);
-}
-
-function isSubcommandBlocked(commandName, subcommandName) {
- if (!subcommandName) {
- return false;
- }
-
- if (GLOBAL_BLOCKED_SUBCOMMANDS.has(subcommandName)) {
- return true;
- }
-
- const commandBlocked = COMMAND_BLOCKED_SUBCOMMANDS[commandName];
- return commandBlocked?.has(subcommandName) ?? false;
-}
-
-/**
- * Returns whether a prefix invocation should be rejected.
- * @param {object} command - Loaded command module
- * @param {string[]} args - Parsed prefix arguments (after command name)
- * @param {(name: string) => string} resolveSubcommandAlias
- * @returns {{ blocked: boolean, reason?: string }}
- */
-export function getPrefixRestriction(command, args, resolveSubcommandAlias) {
- if (!command?.data?.toJSON) {
- return { blocked: false };
- }
-
- const commandJson = command.data.toJSON();
- const commandName = commandJson.name?.toLowerCase();
-
- if (command.prefixOnly === false || command.slashOnly === true) {
- return { blocked: true, reason: 'This command is only available as a slash command.' };
- }
-
- if (SLASH_ONLY_COMMANDS.has(commandName)) {
- return { blocked: true, reason: 'This command is only available as a slash command.' };
- }
-
- const [firstArg, secondArg] = args.map((arg) => arg?.toLowerCase?.() || null);
- const resolvedFirstArg = firstArg ? resolveSubcommandAlias(firstArg) : null;
- const resolvedSecondArg = secondArg ? resolveSubcommandAlias(secondArg) : null;
-
- const subcommandGroup = commandJson.options?.find((opt) => opt.type === 2);
-
- const allSubcommandNames = collectSubcommandNames(commandJson);
- const allSubcommandsBlocked =
- allSubcommandNames.length > 0 &&
- allSubcommandNames.every((name) => isSubcommandBlocked(commandName, name));
-
- if (allSubcommandsBlocked) {
- return { blocked: true, reason: 'This command is only available as a slash command.' };
- }
-
- if (firstArg && GLOBAL_BLOCKED_SUBCOMMAND_GROUPS.has(firstArg)) {
- return {
- blocked: true,
- reason: 'This configuration flow is only available as a slash command.',
- };
- }
-
- if (resolvedFirstArg && isSubcommandBlocked(commandName, resolvedFirstArg)) {
- return {
- blocked: true,
- reason: 'This subcommand is only available as a slash command.',
- };
- }
-
- if (subcommandGroup && resolvedSecondArg && isSubcommandBlocked(commandName, resolvedSecondArg)) {
- return {
- blocked: true,
- reason: 'This subcommand is only available as a slash command.',
- };
- }
-
- return { blocked: false };
-}
-
-export function isPrefixRestrictedCommand(command, args, resolveSubcommandAlias) {
- return getPrefixRestriction(command, args, resolveSubcommandAlias).blocked;
-}
diff --git a/src/config/commands/prefixRestrictions.ts b/src/config/commands/prefixRestrictions.ts
new file mode 100644
index 0000000000..e83f26e207
--- /dev/null
+++ b/src/config/commands/prefixRestrictions.ts
@@ -0,0 +1,168 @@
+/**
+ * Prefix command restrictions — dashboard and advanced setup flows stay slash-only.
+ */
+
+import type {
+ APIApplicationCommandOption,
+ APIApplicationCommandSubcommandGroupOption,
+ APIApplicationCommandSubcommandOption,
+ RESTPostAPIChatInputApplicationCommandsJSONBody,
+} from 'discord.js';
+import type { Command } from '../../types/index.js';
+
+export interface PrefixRestriction {
+ blocked: boolean;
+ reason?: string;
+}
+
+type PrefixCheckedCommand = Pick;
+
+function isSubcommandGroupOption(opt: APIApplicationCommandOption): opt is APIApplicationCommandSubcommandGroupOption {
+ return opt.type === 2;
+}
+
+function isSubcommandOption(opt: APIApplicationCommandOption): opt is APIApplicationCommandSubcommandOption {
+ return opt.type === 1;
+}
+
+/** Top-level commands that cannot be invoked via prefix at all. */
+export const SLASH_ONLY_COMMANDS = new Set([
+ 'configwizard',
+ 'help',
+ 'wipedata',
+ 'apply',
+]);
+
+/** Subcommands blocked for every command when invoked via prefix. */
+export const GLOBAL_BLOCKED_SUBCOMMANDS = new Set([
+ 'dashboard',
+ 'setup',
+]);
+
+/** Subcommand groups blocked for every command when invoked via prefix. */
+export const GLOBAL_BLOCKED_SUBCOMMAND_GROUPS = new Set([
+ 'config',
+]);
+
+/** Per-command subcommands that stay slash-only (beyond the global block list). */
+export const COMMAND_BLOCKED_SUBCOMMANDS: Record | undefined> = {
+ music: new Set([
+ 'shuffle',
+ 'loop',
+ 'seek',
+ 'remove',
+ 'move',
+ 'clear',
+ '247',
+ ]),
+ birthday: new Set(['setchannel']),
+ report: new Set(['setchannel']),
+};
+
+function collectSubcommandNames(commandJson: RESTPostAPIChatInputApplicationCommandsJSONBody): string[] {
+ const names = (commandJson.options?.filter(isSubcommandOption) || []).map((sub) => sub.name);
+ for (const group of commandJson.options?.filter(isSubcommandGroupOption) || []) {
+ names.push(...(group.options?.map((sub) => sub.name) || []));
+ }
+ return names;
+}
+
+function isSubcommandBlocked(commandName: string, subcommandName: string | null | undefined): boolean {
+ if (!subcommandName) {
+ return false;
+ }
+
+ if (GLOBAL_BLOCKED_SUBCOMMANDS.has(subcommandName)) {
+ return true;
+ }
+
+ const commandBlocked = COMMAND_BLOCKED_SUBCOMMANDS[commandName];
+ return commandBlocked?.has(subcommandName) ?? false;
+}
+
+/**
+ * Returns whether a prefix invocation should be rejected.
+ * @param {object} command - Loaded command module
+ * @param {string[]} args - Parsed prefix arguments (after command name)
+ * @param {(name: string) => string} resolveSubcommandAlias
+ * @returns {{ blocked: boolean, reason?: string }}
+ */
+export function getPrefixRestriction(
+ command: PrefixCheckedCommand | null | undefined,
+ args: string[],
+ resolveSubcommandAlias: (name: string) => string,
+): PrefixRestriction {
+ if (!command?.data?.toJSON) {
+ return { blocked: false };
+ }
+
+ const commandJson = command.data.toJSON();
+ const commandName = commandJson.name?.toLowerCase();
+
+ if (command.prefixOnly === false || command.slashOnly === true) {
+ return { blocked: true, reason: 'Dieser Command ist nur als Slash-Command verfügbar.' };
+ }
+
+ if (SLASH_ONLY_COMMANDS.has(commandName)) {
+ return { blocked: true, reason: 'Dieser Command ist nur als Slash-Command verfügbar.' };
+ }
+
+ const [firstArg, secondArg] = args.map((arg) => arg?.toLowerCase?.() || null);
+ const resolvedFirstArg = firstArg ? resolveSubcommandAlias(firstArg) : null;
+ const resolvedSecondArg = secondArg ? resolveSubcommandAlias(secondArg) : null;
+
+ const matchedGroup = commandJson.options?.find(
+ (opt): opt is APIApplicationCommandSubcommandGroupOption => isSubcommandGroupOption(opt) && opt.name === firstArg,
+ );
+
+ const allSubcommandNames = collectSubcommandNames(commandJson);
+ const allSubcommandsBlocked =
+ allSubcommandNames.length > 0 &&
+ allSubcommandNames.every((name) => isSubcommandBlocked(commandName, name));
+
+ if (allSubcommandsBlocked) {
+ return { blocked: true, reason: 'Dieser Command ist nur als Slash-Command verfügbar.' };
+ }
+
+ if (firstArg && GLOBAL_BLOCKED_SUBCOMMAND_GROUPS.has(firstArg)) {
+ return {
+ blocked: true,
+ reason: 'Diese Konfiguration ist nur als Slash-Command verfügbar.',
+ };
+ }
+
+ // Routed subcommands keep the slash-only flag of the old command they come from (e.g. /eco shop).
+ const route = matchedGroup
+ ? command.resolveRoute?.(matchedGroup.name, null)
+ : firstArg && (command.resolveRoute?.(null, firstArg) ?? command.resolveRoute?.(null, resolvedFirstArg));
+ if (route && route.module.slashOnly) {
+ return {
+ blocked: true,
+ reason: 'Dieser Subcommand ist nur als Slash-Command verfügbar.',
+ };
+ }
+
+ if (firstArg && (isSubcommandBlocked(commandName, firstArg) || isSubcommandBlocked(commandName, resolvedFirstArg))) {
+ return {
+ blocked: true,
+ reason: 'Dieser Subcommand ist nur als Slash-Command verfügbar.',
+ };
+ }
+
+ if (matchedGroup && secondArg && (isSubcommandBlocked(commandName, secondArg) || isSubcommandBlocked(commandName, resolvedSecondArg))) {
+ return {
+ blocked: true,
+ reason: 'Dieser Subcommand ist nur als Slash-Command verfügbar.',
+ };
+ }
+
+ return { blocked: false };
+}
+
+export function isPrefixRestrictedCommand(
+ command: PrefixCheckedCommand | null | undefined,
+ args: string[],
+ resolveSubcommandAlias: (name: string) => string,
+): boolean {
+ return getPrefixRestriction(command, args, resolveSubcommandAlias).blocked;
+}
diff --git a/src/config/database/postgres.js b/src/config/database/postgres.ts
similarity index 86%
rename from src/config/database/postgres.js
rename to src/config/database/postgres.ts
index 79d92dbdc1..e38ed4bb64 100644
--- a/src/config/database/postgres.js
+++ b/src/config/database/postgres.ts
@@ -44,7 +44,7 @@ const validatedTables = Object.fromEntries(
key,
assertAllowlistedIdentifier(value, allowedTableIdentifiers, `PostgreSQL table identifier (${key})`),
])
-);
+) as Record;
const DEFAULT_POSTGRES_URL = 'postgresql://localhost:5432/titanbot';
@@ -77,10 +77,10 @@ export function resolvePostgresPoolConfig() {
const ssl = resolveSslConfig();
const url = (process.env.POSTGRES_URL || process.env.DATABASE_URL || '').trim();
const sharedOptions = {
- max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS) || 20,
- min: parseInt(process.env.POSTGRES_MIN_CONNECTIONS) || 2,
- idleTimeoutMillis: parseInt(process.env.POSTGRES_IDLE_TIMEOUT) || 30000,
- connectionTimeoutMillis: parseInt(process.env.POSTGRES_CONNECTION_TIMEOUT) || 10000,
+ max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS ?? '') || 20,
+ min: parseInt(process.env.POSTGRES_MIN_CONNECTIONS ?? '') || 2,
+ idleTimeoutMillis: parseInt(process.env.POSTGRES_IDLE_TIMEOUT ?? '') || 30000,
+ connectionTimeoutMillis: parseInt(process.env.POSTGRES_CONNECTION_TIMEOUT ?? '') || 10000,
application_name: 'titanbot',
statement_timeout: process.env.NODE_ENV === 'production' ? 30000 : 0,
keepalives: 1,
@@ -94,7 +94,7 @@ export function resolvePostgresPoolConfig() {
return {
host: process.env.POSTGRES_HOST || 'localhost',
- port: parseInt(process.env.POSTGRES_PORT) || 5432,
+ port: parseInt(process.env.POSTGRES_PORT ?? '') || 5432,
database: process.env.POSTGRES_DB || 'titanbot',
user: process.env.POSTGRES_USER || 'postgres',
password: (process.env.POSTGRES_PASSWORD || '').toString(),
@@ -108,25 +108,25 @@ export const pgConfig = {
options: {
host: process.env.POSTGRES_HOST || 'localhost',
- port: parseInt(process.env.POSTGRES_PORT) || 5432,
+ port: parseInt(process.env.POSTGRES_PORT ?? '') || 5432,
database: process.env.POSTGRES_DB || 'titanbot',
user: process.env.POSTGRES_USER || 'postgres',
password: (process.env.POSTGRES_PASSWORD || '').toString(),
ssl: resolveSslConfig(),
- max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS) || 20,
- min: parseInt(process.env.POSTGRES_MIN_CONNECTIONS) || 2,
- idleTimeoutMillis: parseInt(process.env.POSTGRES_IDLE_TIMEOUT) || 30000,
- connectionTimeoutMillis: parseInt(process.env.POSTGRES_CONNECTION_TIMEOUT) || 10000,
+ max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS ?? '') || 20,
+ min: parseInt(process.env.POSTGRES_MIN_CONNECTIONS ?? '') || 2,
+ idleTimeoutMillis: parseInt(process.env.POSTGRES_IDLE_TIMEOUT ?? '') || 30000,
+ connectionTimeoutMillis: parseInt(process.env.POSTGRES_CONNECTION_TIMEOUT ?? '') || 10000,
application_name: 'titanbot',
statement_timeout: process.env.NODE_ENV === 'production' ? 30000 : 0,
keepalives: 1,
keepalives_idle: 30,
- retries: parseInt(process.env.POSTGRES_RETRIES) || 3,
- backoffBase: parseInt(process.env.POSTGRES_BACKOFF_BASE) || 100,
- backoffMultiplier: parseInt(process.env.POSTGRES_BACKOFF_MULTIPLIER) || 2,
+ retries: parseInt(process.env.POSTGRES_RETRIES ?? '') || 3,
+ backoffBase: parseInt(process.env.POSTGRES_BACKOFF_BASE ?? '') || 100,
+ backoffMultiplier: parseInt(process.env.POSTGRES_BACKOFF_MULTIPLIER ?? '') || 2,
},
tables: validatedTables,
diff --git a/src/config/database/schemaVersion.js b/src/config/database/schemaVersion.ts
similarity index 100%
rename from src/config/database/schemaVersion.js
rename to src/config/database/schemaVersion.ts
diff --git a/src/config/env.ts b/src/config/env.ts
new file mode 100644
index 0000000000..e2c6ec6f89
--- /dev/null
+++ b/src/config/env.ts
@@ -0,0 +1,117 @@
+import { z } from 'zod';
+
+/**
+ * Central schema for every environment variable the project reads.
+ * Values stay (optional) strings so existing parsing and fallbacks keep working;
+ * numeric variables are only checked to be integers when set and non-empty.
+ */
+const str = z.string().optional();
+const intStr = z
+ .string()
+ .refine((value) => value.trim() === '' || /^-?\d+$/.test(value.trim()), {
+ message: 'must be an integer',
+ })
+ .optional();
+
+const envSchema = z.object({
+ NODE_ENV: str,
+
+ // Discord
+ DISCORD_TOKEN: str,
+ TOKEN: str,
+ CLIENT_ID: str,
+ GUILD_ID: str,
+ TEST_GUILD_ID: str,
+ OWNER_IDS: str,
+ PREFIX: str,
+ MAINTENANCE_MODE: str,
+
+ // Logging / monitoring
+ LOG_LEVEL: str,
+ LOG_TO_FILE: str,
+ SENTRY_DSN: str,
+
+ // Web server
+ PORT: intStr,
+ PORT_RETRY_ATTEMPTS: intStr,
+ WEB_HOST: str,
+ CORS_ORIGIN: str,
+
+ // PostgreSQL
+ DATABASE_URL: str,
+ POSTGRES_URL: str,
+ POSTGRES_HOST: str,
+ POSTGRES_PORT: intStr,
+ POSTGRES_DB: str,
+ POSTGRES_USER: str,
+ POSTGRES_PASSWORD: str,
+ POSTGRES_SSL: str,
+ POSTGRES_MAX_CONNECTIONS: intStr,
+ POSTGRES_MIN_CONNECTIONS: intStr,
+ POSTGRES_IDLE_TIMEOUT: intStr,
+ POSTGRES_CONNECTION_TIMEOUT: intStr,
+ POSTGRES_RETRIES: intStr,
+ POSTGRES_BACKOFF_BASE: intStr,
+ POSTGRES_BACKOFF_MULTIPLIER: intStr,
+ POSTGRES_MIGRATION_TABLE: str,
+ POSTGRES_RESTORE_URL: str,
+ AUTO_MIGRATE: str,
+ SCHEMA_VERSION: intStr,
+ SCHEMA_VERSION_LABEL: str,
+
+ // Backups
+ BACKUP_DIR: str,
+ BACKUP_RETENTION_DAYS: intStr,
+
+ // Lavalink
+ LAVALINK_NODES: str,
+ LAVALINK_NODES_FILE: str,
+ LAVALINK_HOST: str,
+ LAVALINK_PORT: intStr,
+ LAVALINK_PASSWORD: str,
+ LAVALINK_SECURE: str,
+ LAVALINK_NAME: str,
+ LAVALINK_SEARCH_PLATFORM: str,
+ LAVALINK_REST_VERSION: str,
+
+ // Hosting
+ RAILWAY_ENVIRONMENT: str,
+ RAILWAY_PROJECT_ID: str,
+});
+
+export type Env = z.infer;
+
+function formatProblems(problems: string[]): string {
+ return `Invalid environment configuration:\n${problems.map((problem) => `- ${problem}`).join('\n')}`;
+}
+
+function issuesToProblems(error: z.ZodError): string[] {
+ return error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
+}
+
+/** Parses process.env on every call (scripts load .env after their imports run). */
+export function getEnv(): Env {
+ const result = envSchema.safeParse(process.env);
+ if (!result.success) {
+ throw new Error(formatProblems(issuesToProblems(result.error)));
+ }
+ return result.data;
+}
+
+/** Like getEnv(), but also requires the variables the bot needs to log in. */
+export function validateBotEnv(): Env {
+ const result = envSchema.safeParse(process.env);
+ const problems = result.success ? [] : issuesToProblems(result.error);
+
+ if (!process.env.DISCORD_TOKEN && !process.env.TOKEN) {
+ problems.push('DISCORD_TOKEN (or legacy TOKEN) is required');
+ }
+ if (!process.env.CLIENT_ID) {
+ problems.push('CLIENT_ID is required');
+ }
+
+ if (problems.length > 0 || !result.success) {
+ throw new Error(formatProblems(problems));
+ }
+ return result.data;
+}
diff --git a/src/config/guild/guildConfigDefaults.js b/src/config/guild/guildConfigDefaults.ts
similarity index 93%
rename from src/config/guild/guildConfigDefaults.js
rename to src/config/guild/guildConfigDefaults.ts
index ef60a90be6..52777c8dce 100644
--- a/src/config/guild/guildConfigDefaults.js
+++ b/src/config/guild/guildConfigDefaults.ts
@@ -8,7 +8,7 @@ import { DEFAULT_GUILD_CONFIG } from '../../utils/constants.js';
export const GUILD_CONFIG_DEFAULTS = {
...DEFAULT_GUILD_CONFIG,
prefix: getCommandPrefix(),
- welcomeMessage: BotConfig.welcome?.defaultWelcomeMessage || 'Welcome {user} to {server}!',
+ welcomeMessage: BotConfig.welcome?.defaultWelcomeMessage || 'Willkommen {user} auf {server}!',
dmOnClose: true,
disabledCommands: {},
disabledCategories: {},
diff --git a/src/config/music/lavalink.js b/src/config/music/lavalink.js
deleted file mode 100644
index e464557a9e..0000000000
--- a/src/config/music/lavalink.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { existsSync, readFileSync } from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-
-const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
-
-function parseBoolean(value, defaultValue = false) {
- if (value === undefined || value === null || value === '') {
- return defaultValue;
- }
- return ['true', '1', 'yes'].includes(String(value).toLowerCase());
-}
-
-function parseNodesFromEnv() {
- const raw = process.env.LAVALINK_NODES?.trim();
- if (!raw) {
- return null;
- }
-
- try {
- const parsed = JSON.parse(raw);
- return Array.isArray(parsed) ? parsed : null;
- } catch {
- return null;
- }
-}
-
-function parseNodesPayload(parsed) {
- if (Array.isArray(parsed)) {
- return parsed;
- }
- if (Array.isArray(parsed?.nodes)) {
- return parsed.nodes;
- }
- return null;
-}
-
-function loadNodesFromFile() {
- const nodesFile = process.env.LAVALINK_NODES_FILE?.trim()
- || path.join(projectRoot, 'lavalink', 'nodes.json');
-
- if (!existsSync(nodesFile)) {
- return null;
- }
-
- try {
- const parsed = JSON.parse(readFileSync(nodesFile, 'utf8'));
- return parseNodesPayload(parsed);
- } catch {
- return null;
- }
-}
-
-export function getLavalinkNodes() {
- const fromJson = parseNodesFromEnv();
- if (fromJson?.length) {
- return fromJson;
- }
-
- const fromFile = loadNodesFromFile();
- if (fromFile?.length) {
- return fromFile;
- }
-
- const host = process.env.LAVALINK_HOST || 'localhost';
- const port = Number(process.env.LAVALINK_PORT || 2333);
- const password = process.env.LAVALINK_PASSWORD || 'youshallnotpass';
- const secure = parseBoolean(process.env.LAVALINK_SECURE, false);
-
- return [{
- host,
- port,
- password,
- secure,
- name: process.env.LAVALINK_NAME || 'Main',
- }];
-}
-
-export const lavalinkConfig = {
- nodes: getLavalinkNodes(),
- defaultSearchPlatform: process.env.LAVALINK_SEARCH_PLATFORM || 'ytmsearch',
- restVersion: process.env.LAVALINK_REST_VERSION || 'v4',
-};
-
-export default lavalinkConfig;
diff --git a/src/config/music/lavalink.ts b/src/config/music/lavalink.ts
new file mode 100644
index 0000000000..895452f95a
--- /dev/null
+++ b/src/config/music/lavalink.ts
@@ -0,0 +1,97 @@
+import { existsSync, readFileSync } from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import type { LavalinkNode, SearchPlatform, Version } from 'riffy';
+import { getEnv } from '../env.js';
+
+const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
+
+function parseBoolean(value: string | null | undefined, defaultValue = false): boolean {
+ if (value === undefined || value === null || value === '') {
+ return defaultValue;
+ }
+ return ['true', '1', 'yes'].includes(String(value).toLowerCase());
+}
+
+function parseNodesFromEnv(): LavalinkNode[] | null {
+ const raw = getEnv().LAVALINK_NODES?.trim();
+ if (!raw) {
+ return null;
+ }
+
+ try {
+ const parsed: unknown = JSON.parse(raw);
+ // Node entries are passed to Riffy unvalidated, as before.
+ return Array.isArray(parsed) ? (parsed as LavalinkNode[]) : null;
+ } catch {
+ return null;
+ }
+}
+
+function parseNodesPayload(parsed: unknown): LavalinkNode[] | null {
+ if (Array.isArray(parsed)) {
+ return parsed as LavalinkNode[];
+ }
+ const nodes: unknown = (parsed as { nodes?: unknown } | null | undefined)?.nodes;
+ if (Array.isArray(nodes)) {
+ return nodes as LavalinkNode[];
+ }
+ return null;
+}
+
+function loadNodesFromFile(): LavalinkNode[] | null {
+ const nodesFile = getEnv().LAVALINK_NODES_FILE?.trim()
+ || path.join(projectRoot, 'lavalink', 'nodes.json');
+
+ if (!existsSync(nodesFile)) {
+ return null;
+ }
+
+ try {
+ const parsed: unknown = JSON.parse(readFileSync(nodesFile, 'utf8'));
+ return parseNodesPayload(parsed);
+ } catch {
+ return null;
+ }
+}
+
+export function getLavalinkNodes(): LavalinkNode[] {
+ const fromJson = parseNodesFromEnv();
+ if (fromJson?.length) {
+ return fromJson;
+ }
+
+ const fromFile = loadNodesFromFile();
+ if (fromFile?.length) {
+ return fromFile;
+ }
+
+ const env = getEnv();
+ const host = env.LAVALINK_HOST || 'localhost';
+ const port = Number(env.LAVALINK_PORT || 2333);
+ const password = env.LAVALINK_PASSWORD || 'youshallnotpass';
+ const secure = parseBoolean(env.LAVALINK_SECURE, false);
+
+ return [{
+ host,
+ port,
+ password,
+ secure,
+ name: env.LAVALINK_NAME || 'Main',
+ }];
+}
+
+export interface LavalinkConfig {
+ nodes: LavalinkNode[];
+ defaultSearchPlatform: SearchPlatform;
+ restVersion: Version;
+}
+
+export const lavalinkConfig: LavalinkConfig = {
+ nodes: getLavalinkNodes(),
+ defaultSearchPlatform: getEnv().LAVALINK_SEARCH_PLATFORM || 'ytmsearch',
+ // Passed through unvalidated, as before.
+ restVersion: (getEnv().LAVALINK_REST_VERSION || 'v4') as Version,
+};
+
+export default lavalinkConfig;
diff --git a/src/config/shop/index.js b/src/config/shop/index.ts
similarity index 71%
rename from src/config/shop/index.js
rename to src/config/shop/index.ts
index 17f6917989..e38e8d70bd 100644
--- a/src/config/shop/index.js
+++ b/src/config/shop/index.ts
@@ -1,10 +1,19 @@
import { shopItems, getItemById, getItemsByType, getItemPrice, validatePurchase } from './items.js';
+import type { ShopItem, ShopUserData } from './items.js';
import { botConfig } from '../bot.js';
const { currency } = botConfig.economy;
+export interface ShopCategory {
+ id: string;
+ name: string;
+ description: string;
+ icon: string;
+ itemTypes: string[];
+}
+
export const shopConfig = {
- name: 'TitanBot Shop',
+ name: 'Nexo Shop',
currency: currency.name,
currencyName: currency.name,
currencyNamePlural: currency.namePlural || `${currency.name}s`,
@@ -13,29 +22,29 @@ export const shopConfig = {
categories: [
{
id: 'consumables',
- name: 'Consumables',
- description: 'One-time use items that provide temporary benefits',
+ name: 'Verbrauchsgüter',
+ description: 'Einmal nutzbare Items mit zeitlich begrenzten Vorteilen',
icon: '🍯',
itemTypes: ['consumable']
},
{
id: 'upgrades',
name: 'Upgrades',
- description: 'Permanent upgrades that enhance your abilities',
+ description: 'Dauerhafte Upgrades, die deine Fähigkeiten verbessern',
icon: '⚡',
itemTypes: ['upgrade']
},
{
id: 'tools',
- name: 'Tools',
- description: 'Equipment that helps you gather resources more efficiently',
+ name: 'Werkzeuge',
+ description: 'Ausrüstung, mit der du Ressourcen effizienter sammelst',
icon: '⛏️',
itemTypes: ['tool']
},
{
id: 'roles',
- name: 'Roles',
- description: 'Special roles with unique perks',
+ name: 'Rollen',
+ description: 'Besondere Rollen mit einzigartigen Vorteilen',
icon: '🎭',
itemTypes: ['role']
}
@@ -96,8 +105,8 @@ mythic: '#E74C3C'
restock: {
enabled: true,
interval: 86400000,
-announcementChannel: null,
- message: '🛒 **Shop Restocked!** New items are now available!'
+announcementChannel: null as string | null,
+ message: '🛒 **Shop aufgefüllt!** Neue Items sind jetzt verfügbar!'
},
sales: {
@@ -106,7 +115,7 @@ announcementChannel: null,
{
day: 0,
discount: 0.2,
- message: '🔥 **Weekend Sale!** 20% off all items!'
+ message: '🔥 **Wochenend-Sale!** 20 % Rabatt auf alle Items!'
},
]
}
@@ -121,7 +130,10 @@ export {
validatePurchase
};
-export function getCurrentPrice(itemId, { quantity = 1, userData = null } = {}) {
+export function getCurrentPrice(
+ itemId: string,
+ { quantity = 1, userData = null }: { quantity?: number; userData?: ShopUserData | null } = {},
+): number {
const basePrice = getItemPrice(itemId) * quantity;
let discount = 0;
@@ -150,18 +162,18 @@ discount += 0.1;
return Math.floor(basePrice * (1 - discount));
}
-export function getCategoryForItem(itemType) {
+export function getCategoryForItem(itemType: string): Omit & { itemTypes?: string[] } {
return shopConfig.categories.find(cat =>
cat.itemTypes.includes(itemType)
) || {
id: 'other',
- name: 'Other',
- description: 'Miscellaneous items',
+ name: 'Sonstiges',
+ description: 'Verschiedene Items',
icon: '📦'
};
}
-export function getItemsInCategory(categoryId) {
+export function getItemsInCategory(categoryId: string): ShopItem[] {
const category = shopConfig.categories.find(cat => cat.id === categoryId);
if (!category) return [];
diff --git a/src/config/shop/items.js b/src/config/shop/items.ts
similarity index 55%
rename from src/config/shop/items.js
rename to src/config/shop/items.ts
index 5f47eaeaf5..5fdb507031 100644
--- a/src/config/shop/items.js
+++ b/src/config/shop/items.ts
@@ -1,9 +1,46 @@
-export const shopItems = [
+export type ShopItemType = 'consumable' | 'upgrade' | 'tool' | 'role';
+
+export interface ShopItemEffect {
+ type: string;
+ command?: string;
+ uses?: number;
+ multiplier?: number;
+ increase?: number;
+ protection?: boolean;
+}
+
+export interface ShopItem {
+ id: string;
+ name: string;
+ price: number;
+ description: string;
+ type: ShopItemType;
+ maxQuantity?: number;
+ maxLevel?: number;
+ cooldown?: number;
+ durability?: number | null;
+ roleId?: string | null;
+ effect: ShopItemEffect;
+}
+
+/** Subset of the user's economy data that purchase validation looks at. */
+export interface ShopUserData {
+ inventory?: Record;
+ upgrades?: Record;
+ roles?: string[];
+}
+
+export interface PurchaseValidation {
+ valid: boolean;
+ reason?: string;
+}
+
+export const shopItems: ShopItem[] = [
{
id: 'extra_work',
- name: 'Extra Work Shift',
+ name: 'Extraschicht',
price: 5000,
- description: 'Allows 1 extra use of the `/work` command.',
+ description: 'Erlaubt eine zusätzliche Nutzung von `/eco work`.',
type: 'consumable',
maxQuantity: 5,
cooldown: 86400000,
@@ -15,9 +52,9 @@ cooldown: 86400000,
},
{
id: 'bank_upgrade_1',
- name: 'Bank Upgrade I',
+ name: 'Bank-Upgrade I',
price: 15000,
- description: 'Increases bank capacity and allows more funds to be deposited.',
+ description: 'Erhöht deine Bankkapazität, damit du mehr einzahlen kannst.',
type: 'upgrade',
maxLevel: 5,
effect: {
@@ -27,9 +64,9 @@ cooldown: 86400000,
},
{
id: 'diamond_pickaxe',
- name: 'Diamond Pickaxe',
+ name: 'Diamantspitzhacke',
price: 50000,
- description: 'Increases yield from `/mine`',
+ description: 'Erhöht den Ertrag von `/eco mine`',
type: 'tool',
durability: 100,
effect: {
@@ -39,9 +76,9 @@ cooldown: 86400000,
},
{
id: 'premium_role',
- name: 'Premium Server Role',
+ name: 'Premium-Serverrolle',
price: 15000,
- description: 'A special role granting a fancy color and a 10% daily bonus.',
+ description: 'Eine besondere Rolle mit schicker Farbe und 10 % Daily-Bonus.',
type: 'role',
roleId: null,
effect: {
@@ -51,9 +88,9 @@ roleId: null,
},
{
id: 'lucky_clover',
- name: 'Lucky Clover',
+ name: 'Glücksklee',
price: 10000,
- description: 'Increases the chance of winning a higher payout on `/gamble` once.',
+ description: 'Erhöht einmalig die Chance auf einen höheren Gewinn bei `/eco gamble`.',
type: 'consumable',
maxQuantity: 10,
effect: {
@@ -64,9 +101,9 @@ roleId: null,
},
{
id: 'fishing_rod',
- name: '🎣 Fishing Rod',
+ name: '🎣 Angel',
price: 5000,
- description: 'Used for fishing commands',
+ description: 'Wird für Angel-Commands benötigt',
type: 'tool',
durability: 100,
effect: {
@@ -76,9 +113,9 @@ roleId: null,
},
{
id: 'pickaxe',
- name: '⛏️ Pickaxe',
+ name: '⛏️ Spitzhacke',
price: 7500,
- description: 'Used for mining commands',
+ description: 'Wird für Bergbau-Commands benötigt',
type: 'tool',
durability: 100,
effect: {
@@ -90,7 +127,7 @@ roleId: null,
id: 'laptop',
name: '💻 Laptop',
price: 15000,
- description: 'Increases work earnings',
+ description: 'Erhöht deine Einnahmen beim Arbeiten',
type: 'tool',
durability: 200,
effect: {
@@ -100,9 +137,9 @@ roleId: null,
},
{
id: 'lucky_charm',
- name: '🍀 Lucky Charm',
+ name: '🍀 Glücksbringer',
price: 10000,
- description: 'Increases luck for gambling. Has 3 uses before being consumed.',
+ description: 'Erhöht dein Glück beim Glücksspiel. Hält 3 Nutzungen lang.',
type: 'consumable',
maxQuantity: 10,
effect: {
@@ -113,9 +150,9 @@ roleId: null,
},
{
id: 'bank_note',
- name: '📜 Bank Note',
+ name: '📜 Bankschein',
price: 25000,
- description: 'Increases bank capacity by 10,000. Can be purchased multiple times.',
+ description: 'Erhöht deine Bankkapazität um 10.000. Mehrfach kaufbar.',
type: 'tool',
durability: null,
effect: {
@@ -125,9 +162,9 @@ roleId: null,
},
{
id: 'personal_safe',
- name: '🔒 Personal Safe',
+ name: '🔒 Persönlicher Tresor',
price: 30000,
- description: 'Protects your money from theft. Prevents others from robbing you.',
+ description: 'Schützt dein Geld vor Diebstahl. Andere können dich nicht mehr ausrauben.',
type: 'tool',
durability: null,
effect: {
@@ -137,34 +174,34 @@ roleId: null,
}
];
-export function getItemById(itemId) {
+export function getItemById(itemId: string): ShopItem | undefined {
return shopItems.find(item => item.id === itemId);
}
-export function getItemsByType(type) {
+export function getItemsByType(type: string): ShopItem[] {
return shopItems.filter(item => item.type === type);
}
-export function getItemPrice(itemId) {
+export function getItemPrice(itemId: string): number {
const item = getItemById(itemId);
return item ? item.price : 0;
}
-export function validatePurchase(itemId, userData) {
+export function validatePurchase(itemId: string, userData: ShopUserData): PurchaseValidation {
const item = getItemById(itemId);
if (!item) {
- return { valid: false, reason: 'Item not found' };
+ return { valid: false, reason: 'Item nicht gefunden' };
}
- const inventory = userData.inventory || {};
- const upgrades = userData.upgrades || {};
+ const inventory: Record = userData.inventory || {};
+ const upgrades: Record = userData.upgrades || {};
if (item.type === 'consumable' && item.maxQuantity) {
const currentQuantity = inventory[itemId] || 0;
if (currentQuantity >= item.maxQuantity) {
return {
valid: false,
- reason: `You can only have a maximum of ${item.maxQuantity} ${item.name}s`
+ reason: `Du kannst höchstens ${item.maxQuantity}× ${item.name} besitzen`
};
}
}
@@ -174,7 +211,7 @@ export function validatePurchase(itemId, userData) {
if (upgrades[itemId]) {
return {
valid: false,
- reason: `You've already purchased ${item.name}`
+ reason: `Du hast ${item.name} bereits gekauft`
};
}
}
@@ -185,7 +222,7 @@ export function validatePurchase(itemId, userData) {
if (itemId !== 'bank_note' && currentQuantity > 0) {
return {
valid: false,
- reason: `You already have a ${item.name}`
+ reason: `Du besitzt bereits: ${item.name}`
};
}
}
@@ -194,7 +231,7 @@ export function validatePurchase(itemId, userData) {
if (userData.roles?.includes(item.roleId)) {
return {
valid: false,
- reason: `You already have the ${item.name} role`
+ reason: `Du hast die Rolle ${item.name} bereits`
};
}
}
diff --git a/src/events/channelDelete.js b/src/events/channelDelete.ts
similarity index 98%
rename from src/events/channelDelete.js
rename to src/events/channelDelete.ts
index c8fd68f445..9f74204f29 100644
--- a/src/events/channelDelete.js
+++ b/src/events/channelDelete.ts
@@ -7,6 +7,7 @@ import {
} from '../utils/database.js';
import { getServerCounters, saveServerCounters } from '../services/serverstatsService.js';
import { logger } from '../utils/logger.js';
+import type { BotEvent } from '../types/index.js';
export default {
name: 'channelDelete',
@@ -96,4 +97,4 @@ if (channel.type !== 2 && channel.type !== 4) {
logger.error(`Error in channelDelete event for guild ${guildId}:`, error);
}
}
-};
\ No newline at end of file
+} satisfies BotEvent<'channelDelete'>;
\ No newline at end of file
diff --git a/src/events/guildCreate.js b/src/events/guildCreate.ts
similarity index 87%
rename from src/events/guildCreate.js
rename to src/events/guildCreate.ts
index 7972714a59..091913d928 100644
--- a/src/events/guildCreate.js
+++ b/src/events/guildCreate.ts
@@ -1,6 +1,7 @@
import { Events } from 'discord.js';
import { logger } from '../utils/logger.js';
import { getGuildConfig, setGuildConfig } from '../services/config/guildConfig.js';
+import type { BotEvent } from '../types/index.js';
export default {
name: Events.GuildCreate,
@@ -19,4 +20,4 @@ export default {
logger.error(`Error initializing guild ${guild?.id} on join:`, error);
}
},
-};
+} satisfies BotEvent;
diff --git a/src/events/guildMemberAdd.js b/src/events/guildMemberAdd.ts
similarity index 75%
rename from src/events/guildMemberAdd.js
rename to src/events/guildMemberAdd.ts
index cb77b477b4..9a9081e913 100644
--- a/src/events/guildMemberAdd.js
+++ b/src/events/guildMemberAdd.ts
@@ -1,4 +1,5 @@
import { Events, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
+import type { ColorResolvable, Guild, GuildMember, GuildTextBasedChannel, Role } from 'discord.js';
import { getColor, botConfig } from '../config/bot.js';
import { getGuildConfig } from '../services/config/guildConfig.js';
import { getWelcomeConfig } from '../utils/database.js';
@@ -7,6 +8,10 @@ import { logEvent, EVENT_TYPES } from '../services/loggingService.js';
import { getServerCounters, updateCounter } from '../services/serverstatsService.js';
import { setBirthday as dbSetBirthday } from '../utils/database.js';
import { logger } from '../utils/logger.js';
+import type { BirthdayEntry } from '../utils/database.js';
+import type { VerificationConfig } from '../services/verificationService.js';
+import type { BotEvent, NexoClient } from '../types/index.js';
+import type { ErrorLike } from '../utils/errorHandler.js';
export default {
name: Events.GuildMemberAdd,
@@ -31,47 +36,50 @@ export default {
if (permissions?.has([PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages])) {
const formatData = { user, guild, member };
const welcomeMessage = formatWelcomeMessage(
- welcomeConfig.welcomeMessage || welcomeConfig.welcomeEmbed?.description || botConfig.welcome?.defaultWelcomeMessage || 'Welcome {user} to {server}!',
+ welcomeConfig.welcomeMessage || welcomeConfig.welcomeEmbed?.description || botConfig.welcome?.defaultWelcomeMessage || 'Willkommen {user} auf {server}!',
formatData
);
const messageContent = welcomeConfig.welcomePing ? user.toString() : null;
const embedTitle = formatWelcomeMessage(
- welcomeConfig.welcomeEmbed?.title || '🎉 Welcome!',
+ welcomeConfig.welcomeEmbed?.title || '🎉 Willkommen!',
formatData
);
const embedFooter = welcomeConfig.welcomeEmbed?.footer
? formatWelcomeMessage(welcomeConfig.welcomeEmbed.footer, formatData)
- : `Welcome to ${guild.name}!`;
+ : `Willkommen auf ${guild.name}!`;
const canEmbed = permissions.has(PermissionFlagsBits.EmbedLinks);
if (!canEmbed) {
- await channel.send({
+ await (channel as GuildTextBasedChannel).send({
content: messageContent || welcomeMessage
});
} else {
const embed = new EmbedBuilder()
- .setColor(welcomeConfig.welcomeEmbed?.color || getColor('success'))
+ .setColor((welcomeConfig.welcomeEmbed?.color || getColor('success')) as ColorResolvable)
.setTitle(embedTitle)
.setDescription(welcomeMessage)
.setThumbnail(user.displayAvatarURL())
.addFields(
- { name: 'User', value: `${user.tag} (${user.id})`, inline: true },
- { name: 'Member Count', value: guild.memberCount.toString(), inline: true }
+ { name: 'Nutzer', value: `${user.tag} (${user.id})`, inline: true },
+ { name: 'Mitglieder', value: guild.memberCount.toString(), inline: true }
)
.setTimestamp()
.setFooter({ text: embedFooter });
+ // Stored embed config may carry an image object (not part of WelcomeEmbedConfig).
+ const welcomeEmbedImage = welcomeConfig.welcomeEmbed?.image as { url?: string } | undefined;
if (welcomeConfig.welcomeImage) {
embed.setImage(welcomeConfig.welcomeImage);
- } else if (welcomeConfig.welcomeEmbed?.image?.url) {
- embed.setImage(welcomeConfig.welcomeEmbed.image.url);
+ } else if (welcomeEmbedImage?.url) {
+ embed.setImage(welcomeEmbedImage.url);
}
- await channel.send({
- content: messageContent,
+ await (channel as GuildTextBasedChannel).send({
+ // discord.js serializes null content as empty; the typings only allow string | undefined.
+ content: messageContent as string | undefined,
embeds: [embed]
});
}
@@ -110,15 +118,15 @@ export default {
guildId: guild.id,
eventType: EVENT_TYPES.MEMBER_JOIN,
data: {
- title: 'User joined',
+ title: 'Nutzer beigetreten',
lines: [
- `**User:** ${user.toString()} (${user.displayName !== user.username ? `@${user.displayName}` : user.tag})`,
+ `**Nutzer:** ${user.toString()} (${user.displayName !== user.username ? `@${user.displayName}` : user.tag})`,
`**ID:** \`${user.id}\``,
- `**Created:** `,
- `**Members:** ${guild.memberCount}`,
+ `**Erstellt:** `,
+ `**Mitglieder:** ${guild.memberCount}`,
],
quoted: false,
- thumbnail: user.displayAvatarURL({ dynamic: true }),
+ thumbnail: user.displayAvatarURL(),
userId: user.id,
}
});
@@ -139,7 +147,7 @@ export default {
try {
const backupKey = `guild:${guild.id}:birthdays:left`;
- const backup = (await member.client.db.get(backupKey)) || {};
+ const backup = (await member.client.db.get>(backupKey)) || {};
if (backup[user.id]) {
const { month, day } = backup[user.id];
await dbSetBirthday(member.client, guild.id, user.id, month, day);
@@ -155,9 +163,9 @@ export default {
logger.error('Error in guildMemberAdd event:', error);
}
}
-};
+} satisfies BotEvent;
-async function handleVerification(member, guild, verificationConfig, client) {
+async function handleVerification(member: GuildMember, guild: Guild, verificationConfig: VerificationConfig, client: NexoClient) {
const { autoVerifyOnJoin } = await import('../services/verificationService.js');
try {
@@ -184,12 +192,12 @@ async function handleVerification(member, guild, verificationConfig, client) {
guildId: guild.id,
userId: member.id,
userTag: member.user.tag,
- error: error.message
+ error: (error as ErrorLike).message
});
}
}
-async function assignRoleSafely(member, role) {
+async function assignRoleSafely(member: GuildMember, role: Role) {
try {
await member.roles.add(role);
} catch (error) {
diff --git a/src/events/guildMemberRemove.js b/src/events/guildMemberRemove.ts
similarity index 77%
rename from src/events/guildMemberRemove.js
rename to src/events/guildMemberRemove.ts
index a550082635..f6678eef68 100644
--- a/src/events/guildMemberRemove.js
+++ b/src/events/guildMemberRemove.ts
@@ -1,4 +1,5 @@
import { Events, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
+import type { ColorResolvable } from 'discord.js';
import { getColor, botConfig } from '../config/bot.js';
import { getWelcomeConfig, getUserApplications, deleteApplication } from '../utils/database.js';
import { formatWelcomeMessage } from '../utils/welcome.js';
@@ -7,6 +8,8 @@ import { getServerCounters, updateCounter } from '../services/serverstatsService
import { getGuildBirthdays, deleteBirthday } from '../utils/database.js';
import { deleteUserLevelData } from '../services/leveling/leveling.js';
import { logger } from '../utils/logger.js';
+import type { BirthdayEntry } from '../utils/database.js';
+import type { BotEvent } from '../types/index.js';
export default {
name: Events.GuildMemberRemove,
@@ -31,17 +34,17 @@ export default {
const formatData = { user, guild, member };
const goodbyeMessage = formatWelcomeMessage(
- welcomeConfig.leaveMessage || welcomeConfig.leaveEmbed?.description || botConfig.welcome?.defaultGoodbyeMessage || '{user} has left the server.',
+ welcomeConfig.leaveMessage || welcomeConfig.leaveEmbed?.description || botConfig.welcome?.defaultGoodbyeMessage || '{user} hat den Server verlassen.',
formatData
);
const embedTitle = formatWelcomeMessage(
- welcomeConfig.leaveEmbed?.title || '👋 Goodbye',
+ welcomeConfig.leaveEmbed?.title || '👋 Auf Wiedersehen',
formatData
);
const embedFooter = welcomeConfig.leaveEmbed?.footer
? formatWelcomeMessage(welcomeConfig.leaveEmbed.footer, formatData)
- : `Goodbye from ${guild.name}!`;
+ : `Tschüss von ${guild.name}!`;
const canEmbed = permissions.has(PermissionFlagsBits.EmbedLinks);
@@ -54,19 +57,21 @@ export default {
const embed = new EmbedBuilder()
.setTitle(embedTitle)
.setDescription(goodbyeMessage)
- .setColor(welcomeConfig.leaveEmbed?.color || getColor('error'))
+ .setColor((welcomeConfig.leaveEmbed?.color || getColor('error')) as ColorResolvable)
.setThumbnail(user.displayAvatarURL())
.addFields(
- { name: 'User', value: `${user.tag} (${user.id})`, inline: true },
- { name: 'Member Count', value: guild.memberCount.toString(), inline: true }
+ { name: 'Nutzer', value: `${user.tag} (${user.id})`, inline: true },
+ { name: 'Mitglieder', value: guild.memberCount.toString(), inline: true }
)
.setTimestamp()
.setFooter({ text: embedFooter });
- if (typeof welcomeConfig.leaveEmbed?.image === 'string') {
- embed.setImage(welcomeConfig.leaveEmbed.image);
- } else if (welcomeConfig.leaveEmbed?.image?.url) {
- embed.setImage(welcomeConfig.leaveEmbed.image.url);
+ // Stored embed config may carry an image string or object (not part of WelcomeEmbedConfig).
+ const leaveEmbedImage = welcomeConfig.leaveEmbed?.image as string | { url?: string } | undefined;
+ if (typeof leaveEmbedImage === 'string') {
+ embed.setImage(leaveEmbedImage);
+ } else if (leaveEmbedImage?.url) {
+ embed.setImage(leaveEmbedImage.url);
}
await channel.send({
@@ -84,15 +89,15 @@ export default {
guildId: guild.id,
eventType: EVENT_TYPES.MEMBER_LEAVE,
data: {
- title: 'User left',
+ title: 'Nutzer hat den Server verlassen',
lines: [
- `**User:** ${user.toString()} (${user.tag})`,
+ `**Nutzer:** ${user.toString()} (${user.tag})`,
`**ID:** \`${user.id}\``,
- `**Joined:** `,
- `**Members:** ${guild.memberCount}`,
+ `**Beigetreten:** `,
+ `**Mitglieder:** ${guild.memberCount}`,
],
quoted: false,
- thumbnail: user.displayAvatarURL({ dynamic: true }),
+ thumbnail: user.displayAvatarURL(),
userId: user.id,
}
});
@@ -115,7 +120,7 @@ export default {
const birthdays = await getGuildBirthdays(member.client, guild.id);
if (birthdays[user.id]) {
const backupKey = `guild:${guild.id}:birthdays:left`;
- const backup = (await member.client.db.get(backupKey)) || {};
+ const backup = (await member.client.db.get>(backupKey)) || {};
backup[user.id] = birthdays[user.id];
await member.client.db.set(backupKey, backup);
await deleteBirthday(member.client, guild.id, user.id);
@@ -148,4 +153,4 @@ export default {
logger.error('Error in guildMemberRemove event:', error);
}
}
-};
\ No newline at end of file
+} satisfies BotEvent;
\ No newline at end of file
diff --git a/src/events/guildMemberUpdate.js b/src/events/guildMemberUpdate.ts
similarity index 65%
rename from src/events/guildMemberUpdate.js
rename to src/events/guildMemberUpdate.ts
index 80dc788067..d73c96b98b 100644
--- a/src/events/guildMemberUpdate.js
+++ b/src/events/guildMemberUpdate.ts
@@ -1,6 +1,7 @@
import { Events } from 'discord.js';
import { logEvent, EVENT_TYPES } from '../services/loggingService.js';
import { logger } from '../utils/logger.js';
+import type { BotEvent } from '../types/index.js';
export default {
name: Events.GuildMemberUpdate,
@@ -16,14 +17,14 @@ export default {
guildId: newMember.guild.id,
eventType: EVENT_TYPES.MEMBER_NAME_CHANGE,
data: {
- title: 'Nickname changed',
+ title: 'Nickname geändert',
lines: [
- `**User:** ${newMember.user.toString()} (${newMember.user.tag})`,
+ `**Nutzer:** ${newMember.user.toString()} (${newMember.user.tag})`,
`**ID:** \`${newMember.user.id}\``,
- `**Before:** ${oldMember.nickname || '*(no nickname)*'}`,
- `**After:** ${newMember.nickname || '*(no nickname)*'}`,
+ `**Vorher:** ${oldMember.nickname || '*(kein Nickname)*'}`,
+ `**Nachher:** ${newMember.nickname || '*(kein Nickname)*'}`,
],
- thumbnail: newMember.user.displayAvatarURL({ dynamic: true }),
+ thumbnail: newMember.user.displayAvatarURL(),
userId: newMember.user.id,
}
});
@@ -35,4 +36,4 @@ export default {
logger.error('Error in guildMemberUpdate event:', error);
}
}
-};
\ No newline at end of file
+} satisfies BotEvent;
\ No newline at end of file
diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.ts
similarity index 80%
rename from src/events/interactionCreate.js
rename to src/events/interactionCreate.ts
index 50a468b73e..942f947d4e 100644
--- a/src/events/interactionCreate.js
+++ b/src/events/interactionCreate.ts
@@ -1,4 +1,5 @@
import { Events, MessageFlags } from 'discord.js';
+import type { ChatInputCommandInteraction, GuildTextBasedChannel, Interaction } from 'discord.js';
import { logger } from '../utils/logger.js';
import { getGuildConfig } from '../services/config/guildConfig.js';
import {
@@ -19,8 +20,19 @@ import { resolveSlashAccessKey } from '../utils/messageAdapter.js';
import { isCollectorManagedComponent } from '../utils/collectorComponents.js';
import { ResponseCoordinator } from '../utils/responseCoordinator.js';
import { enforceDefaultCommandPermissions } from '../utils/permissionGuard.js';
+import type { ErrorContext, ErrorLike } from '../utils/errorHandler.js';
+import type { InteractionExtras, NexoInteraction } from '../utils/interactionHelper.js';
+import type { TraceContext } from '../utils/logger.js';
+import type { ReactionRoleMessage } from '../services/reactionRoleService.js';
+import type { BotEvent, Command, GuildConfig, ResolvedRoute } from '../types/index.js';
-const COMMAND_ERROR_SUBTYPES = {
+/** Error context plus the identifiers withTraceContext folds into the trace fields. */
+type TraceableContext = ErrorContext & { commandName?: string; guildId?: string | null; userId?: string | null };
+
+/** Any interaction viewed with the optional identifiers read in the unhandled-error path. */
+type UnhandledInteractionView = Interaction & { commandName?: string; customId?: string };
+
+const COMMAND_ERROR_SUBTYPES: Record = {
warn: 'warn_failed',
kick: 'kick_failed',
ban: 'ban_failed',
@@ -36,7 +48,15 @@ const COMMAND_ERROR_SUBTYPES = {
greroll: 'giveaway_failed',
};
-function withTraceContext(context = {}, traceContext = {}) {
+/** For consolidated router commands: the old command behind the invoked subcommand. */
+function resolveCommandRoute(command: Command | undefined, interaction: ChatInputCommandInteraction): ResolvedRoute | null {
+ return command?.resolveRoute?.(
+ interaction.options.getSubcommandGroup(false),
+ interaction.options.getSubcommand(false),
+ ) ?? null;
+}
+
+function withTraceContext(context: TraceableContext = {}, traceContext: Partial = {}): ErrorContext {
return {
traceId: traceContext.traceId,
guildId: context.guildId || traceContext.guildId,
@@ -50,13 +70,13 @@ export default {
name: Events.InteractionCreate,
async execute(interaction, client) {
const interactionTraceContext = createInteractionTraceContext(interaction);
- interaction.traceContext = interactionTraceContext;
- interaction.traceId = interactionTraceContext.traceId;
+ (interaction as Interaction & InteractionExtras).traceContext = interactionTraceContext;
+ (interaction as Interaction & InteractionExtras).traceId = interactionTraceContext.traceId;
return runWithTraceContext(interactionTraceContext, async () => {
try {
- InteractionHelper.patchInteractionResponses(interaction);
- ResponseCoordinator.attach(interaction);
+ InteractionHelper.patchInteractionResponses(interaction as NexoInteraction);
+ ResponseCoordinator.attach(interaction as NexoInteraction);
if (interaction.isChatInputCommand()) {
try {
@@ -79,7 +99,7 @@ export default {
throw createError(
`No command matching ${interaction.commandName} was found.`,
ErrorTypes.CONFIGURATION,
- 'Sorry, that command does not exist.',
+ 'Diesen Command gibt es leider nicht.',
withTraceContext({ commandName: interaction.commandName }, interactionTraceContext)
);
}
@@ -93,7 +113,11 @@ export default {
);
}
- if (!isCommandCategoryEnabled(command.category)) {
+ const route = resolveCommandRoute(command, interaction);
+ // Bucket name for cooldowns and abuse protection: the old command name, so limits stay per old command.
+ const protectionName = route?.legacyName ?? interaction.commandName;
+
+ if (!isCommandCategoryEnabled(command.category) || (route && !isCommandCategoryEnabled(route.category))) {
throw createError(
`Feature disabled for category ${command.category}`,
ErrorTypes.CONFIGURATION,
@@ -104,7 +128,7 @@ export default {
const defaultCooldownSec = Number(botConfig.commands?.defaultCooldown) || 0;
if (defaultCooldownSec > 0 && !isBotOwner(interaction.user.id)) {
- const cooldownKey = `${interaction.user.id}:${interaction.commandName}`;
+ const cooldownKey = `${interaction.user.id}:${protectionName}`;
const expiresAt = client.cooldowns.get(cooldownKey);
if (expiresAt && Date.now() < expiresAt) {
@@ -120,13 +144,17 @@ export default {
client.cooldowns.set(cooldownKey, Date.now() + defaultCooldownSec * 1000);
}
- const abuseProtection = await enforceAbuseProtection(interaction, command, interaction.commandName);
+ const abuseProtection = await enforceAbuseProtection(
+ interaction,
+ route ? { category: route.category, abuseProtection: route.module.abuseProtection } : command,
+ protectionName,
+ );
if (!abuseProtection.allowed) {
const formattedCooldown = formatCooldownDuration(abuseProtection.remainingMs);
throw createError(
`Risky command cooldown active for ${interaction.commandName}`,
ErrorTypes.RATE_LIMIT,
- `This command is on cooldown. Please wait ${formattedCooldown} before trying again.`,
+ `Dieser Command hat gerade Cooldown. Bitte warte ${formattedCooldown}, bevor du es erneut versuchst.`,
withTraceContext({
commandName: interaction.commandName,
subtype: 'command_cooldown',
@@ -138,15 +166,15 @@ export default {
);
}
- let guildConfig = null;
+ let guildConfig: GuildConfig | null = null;
if (interaction.guild) {
- guildConfig = await getGuildConfig(client, interaction.guild.id, interactionTraceContext);
+ guildConfig = await getGuildConfig(client, interaction.guild.id, { ...interactionTraceContext });
const accessKey = resolveSlashAccessKey(interaction);
if (!(await isCommandEnabled(client, interaction.guild.id, accessKey, command.category))) {
throw createError(
`Command ${accessKey} is disabled in this guild`,
ErrorTypes.CONFIGURATION,
- 'This command has been disabled for this server.',
+ 'Dieser Command wurde für diesen Server deaktiviert.',
withTraceContext({ commandName: accessKey, guildId: interaction.guild.id }, interactionTraceContext)
);
}
@@ -165,7 +193,7 @@ export default {
await handleInteractionError(interaction, error, withTraceContext({
type: 'command',
commandName: interaction.commandName,
- subtype: COMMAND_ERROR_SUBTYPES[interaction.commandName] || error?.context?.subtype,
+ subtype: COMMAND_ERROR_SUBTYPES[resolveCommandRoute(client.commands.get(interaction.commandName), interaction)?.legacyName ?? interaction.commandName] || (error as ErrorLike | null | undefined)?.context?.subtype,
}, interactionTraceContext));
}
} else if (interaction.isAutocomplete()) {
@@ -175,7 +203,7 @@ export default {
await autocompleteCommand.autocomplete(interaction, client);
} catch (error) {
logger.error('Error handling command autocomplete:', {
- error: error.message,
+ error: (error as ErrorLike).message,
guildId: interaction.guildId,
commandName: interaction.commandName,
});
@@ -189,7 +217,7 @@ export default {
if (interaction.commandName === 'apply' && focusedOption.name === 'application') {
try {
const { getApplicationRoles } = await import('../utils/database.js');
- const roles = await getApplicationRoles(client, interaction.guildId);
+ const roles = await getApplicationRoles(client, interaction.guildId!);
const roleName = interaction.options.getString('application', false);
const filtered = roles.filter(role =>
@@ -199,13 +227,13 @@ export default {
await interaction.respond(
filtered.slice(0, 25).map(role => ({
- name: `${role.name}${role.enabled === false ? ' (disabled)' : ''}`,
+ name: `${role.name}${role.enabled === false ? ' (deaktiviert)' : ''}`,
value: role.name
}))
);
} catch (error) {
logger.error('Error handling autocomplete:', {
- error: error.message,
+ error: (error as ErrorLike).message,
guildId: interaction.guildId,
commandName: interaction.commandName
});
@@ -214,7 +242,7 @@ export default {
} else if (interaction.commandName === 'app-admin' && focusedOption.name === 'application') {
try {
const { getApplicationRoles } = await import('../utils/database.js');
- const roles = await getApplicationRoles(client, interaction.guildId);
+ const roles = await getApplicationRoles(client, interaction.guildId!);
const appName = interaction.options.getString('application', false);
const filtered = roles.filter(role =>
@@ -223,13 +251,13 @@ export default {
await interaction.respond(
filtered.slice(0, 25).map(role => ({
- name: `${role.name}${role.enabled === false ? ' (disabled)' : ''}`,
+ name: `${role.name}${role.enabled === false ? ' (deaktiviert)' : ''}`,
value: role.name
}))
);
} catch (error) {
logger.error('Error handling app-admin autocomplete:', {
- error: error.message,
+ error: (error as ErrorLike).message,
guildId: interaction.guildId,
commandName: interaction.commandName
});
@@ -238,8 +266,8 @@ export default {
} else if (interaction.commandName === 'reactroles' && focusedOption.name === 'panel') {
try {
const { getAllReactionRoleMessages, deleteReactionRoleMessage } = await import('../services/reactionRoleService.js');
- const guildId = interaction.guildId;
- const guild = interaction.guild;
+ const guildId = interaction.guildId!;
+ const guild = interaction.guild!;
let panels = await getAllReactionRoleMessages(client, guildId);
@@ -248,13 +276,13 @@ export default {
return;
}
- const validPanels = [];
+ const validPanels: ReactionRoleMessage[] = [];
for (const panel of panels) {
if (!panel.messageId || !panel.channelId) {
continue;
}
- const channel = guild.channels.cache.get(panel.channelId);
+ const channel = guild.channels.cache.get(panel.channelId) as GuildTextBasedChannel | undefined;
if (!channel) {
await deleteReactionRoleMessage(client, guildId, panel.messageId).catch(() => {});
continue;
@@ -276,14 +304,14 @@ export default {
const choices = await Promise.all(
validPanels.slice(0, 25).map(async panel => {
try {
- const channel = guild.channels.cache.get(panel.channelId);
+ const channel = guild.channels.cache.get(panel.channelId) as GuildTextBasedChannel | undefined;
if (!channel) return null;
const msg = await channel.messages.fetch(panel.messageId).catch(() => null);
if (!msg) return null;
- const title = msg?.embeds?.[0]?.title ?? 'Untitled Panel';
- const channelName = channel?.name ?? 'unknown';
+ const title = msg?.embeds?.[0]?.title ?? 'Panel ohne Titel';
+ const channelName = channel?.name ?? 'unbekannt';
return {
name: `${title} (${channelName})`.substring(0, 100),
@@ -299,7 +327,7 @@ export default {
await interaction.respond(validChoices);
} catch (error) {
logger.error('Error handling reactroles autocomplete:', {
- error: error.message,
+ error: (error as ErrorLike).message,
guildId: interaction.guildId,
commandName: interaction.commandName
});
@@ -327,7 +355,7 @@ export default {
throw createError(
`No button handler found for ${buttonType}`,
ErrorTypes.CONFIGURATION,
- 'This button is not available.',
+ 'Dieser Button ist nicht verfügbar.',
withTraceContext({ buttonType }, interactionTraceContext)
);
}
@@ -345,7 +373,7 @@ export default {
throw createError(
`No button handler found for ${customId}`,
ErrorTypes.CONFIGURATION,
- 'This button is not available.',
+ 'Dieser Button ist nicht verfügbar.',
withTraceContext({ customId }, interactionTraceContext)
);
}
@@ -371,7 +399,7 @@ export default {
throw createError(
`No select menu handler found for ${customId}`,
ErrorTypes.CONFIGURATION,
- 'This select menu is not available.',
+ 'Dieses Auswahlmenü ist nicht verfügbar.',
withTraceContext({ customId }, interactionTraceContext)
);
}
@@ -424,7 +452,7 @@ export default {
throw createError(
`No modal handler found for ${customId}`,
ErrorTypes.CONFIGURATION,
- 'This form is not available.',
+ 'Dieses Formular ist nicht verfügbar.',
withTraceContext({ customId }, interactionTraceContext)
);
}
@@ -451,10 +479,10 @@ export default {
});
try {
- await handleInteractionError(interaction, error, withTraceContext({
+ await handleInteractionError(interaction as NexoInteraction, error, withTraceContext({
type: 'interaction',
- commandName: interaction.commandName,
- customId: interaction.customId,
+ commandName: (interaction as UnhandledInteractionView).commandName,
+ customId: (interaction as UnhandledInteractionView).customId,
source: 'interactionCreate.unhandled'
}, interactionTraceContext));
} catch (replyError) {
@@ -468,4 +496,4 @@ export default {
}
});
}
-};
\ No newline at end of file
+} satisfies BotEvent;
\ No newline at end of file
diff --git a/src/events/messageCreate.js b/src/events/messageCreate.ts
similarity index 75%
rename from src/events/messageCreate.js
rename to src/events/messageCreate.ts
index fb1a165f31..9dd22830de 100644
--- a/src/events/messageCreate.js
+++ b/src/events/messageCreate.ts
@@ -1,4 +1,5 @@
import { Events } from 'discord.js';
+import type { Message } from 'discord.js';
import { logger } from '../utils/logger.js';
import { getLevelingConfig, getUserLevelData } from '../services/leveling/leveling.js';
import { addXp } from '../services/leveling/xpSystem.js';
@@ -7,6 +8,7 @@ import { parsePrefixCommand } from '../utils/prefixParser.js';
import { supportsPrefixExecution, executePrefixCommand, resolvePrefixAccessKey } from '../utils/messageAdapter.js';
import { resolveCommandAlias, resolveSubcommandAlias } from '../config/commands/commandAliases.js';
import { getPrefixRestriction } from '../config/commands/prefixRestrictions.js';
+import { routeLegacyPrefixCommand } from '../config/commands/legacyCommandMap.js';
import { getGuildConfig } from '../services/config/guildConfig.js';
import { getCommandPrefix, getBotMessage, isBotOwner, isCommandCategoryEnabled, isMaintenanceMode } from '../config/bot.js';
import { enforceAbuseProtection, formatCooldownDuration } from '../utils/abuseProtection.js';
@@ -18,6 +20,7 @@ import {
isValidCountingMessage,
recordCorrectCount,
} from '../services/countingGameService.js';
+import type { BotEvent, NexoClient } from '../types/index.js';
const MESSAGE_XP_RATE_LIMIT_ATTEMPTS = 12;
const MESSAGE_XP_RATE_LIMIT_WINDOW_MS = 10000;
@@ -30,21 +33,21 @@ export default {
logger.debug(`Message received from ${message.author.tag}: ${message.content}`);
- const countingProcessed = await handleCountingGame(message, client);
+ const countingProcessed = await handleCountingGame(message as Message, client);
if (countingProcessed) {
return;
}
- await handlePrefixCommand(message, client);
+ await handlePrefixCommand(message as Message, client);
- await handleLeveling(message, client);
+ await handleLeveling(message as Message, client);
} catch (error) {
logger.error('Error in messageCreate event:', error);
}
}
-};
+} satisfies BotEvent;
-async function handlePrefixCommand(message, client) {
+async function handlePrefixCommand(message: Message, client: NexoClient) {
try {
const guildConfig = await getGuildConfig(client, message.guild.id);
const prefix = guildConfig?.prefix || getCommandPrefix();
@@ -64,7 +67,13 @@ async function handlePrefixCommand(message, client) {
logger.info(`Prefix command detected: ${commandName}, args: ${args.join(', ')}`);
- const resolvedCommandName = resolveCommandAlias(commandName);
+ let resolvedCommandName = resolveCommandAlias(commandName);
+ // Old command names keep working: `!bal` -> `/eco balance`.
+ const legacyRoute = routeLegacyPrefixCommand(resolvedCommandName, args);
+ if (legacyRoute) {
+ resolvedCommandName = legacyRoute.commandName;
+ args = legacyRoute.args;
+ }
logger.info(`Resolved command name: ${resolvedCommandName}`);
const command = client.commands.get(resolvedCommandName);
@@ -76,7 +85,7 @@ async function handlePrefixCommand(message, client) {
if (isMaintenanceMode() && !isBotOwner(message.author.id)) {
await message.channel.send({
embeds: [createEmbed({
- title: 'Maintenance Mode',
+ title: 'Wartungsmodus',
description: getBotMessage('maintenanceMode'),
color: 'warning',
})],
@@ -84,10 +93,15 @@ async function handlePrefixCommand(message, client) {
return;
}
- if (!isCommandCategoryEnabled(command.category)) {
+ // Loaded commands always have a name, so the access key is never null here.
+ const accessKey = resolvePrefixAccessKey(command.data, args)!;
+ const [, firstLevel = null, secondLevel = null] = accessKey.split(' ');
+ const route = command.resolveRoute?.(secondLevel ? firstLevel : null, secondLevel ?? firstLevel) ?? null;
+
+ if (!isCommandCategoryEnabled(command.category) || (route && !isCommandCategoryEnabled(route.category))) {
await message.channel.send({
embeds: [createEmbed({
- title: 'Feature Disabled',
+ title: 'Funktion deaktiviert',
description: getBotMessage('commandDisabled'),
color: 'error',
})],
@@ -99,8 +113,8 @@ async function handlePrefixCommand(message, client) {
if (!supportsPrefixExecution(command) || restriction.blocked) {
if (restriction.blocked && restriction.reason) {
const embed = createEmbed({
- title: 'Slash Command Only',
- description: `${restriction.reason}\nUse \`/${resolvedCommandName}\` instead.`,
+ title: 'Nur als Slash-Command',
+ description: `${restriction.reason}\nNutze stattdessen \`/${route ? accessKey : resolvedCommandName}\`.`,
color: 'info',
});
await message.channel.send({ embeds: [embed] }).catch(() => {});
@@ -108,30 +122,30 @@ async function handlePrefixCommand(message, client) {
return;
}
- if (!(await isCommandEnabled(client, message.guild.id, resolvePrefixAccessKey(command.data, args), command.category))) {
+ if (!(await isCommandEnabled(client, message.guild.id, accessKey, command.category))) {
const embed = createEmbed({
- title: 'Command Disabled',
- description: 'This command has been disabled for this server.',
+ title: 'Command deaktiviert',
+ description: 'Dieser Command wurde für diesen Server deaktiviert.',
color: 'error',
});
await message.channel.send({ embeds: [embed] }).catch(() => {});
return;
}
- const mockInteractionForProtection = {
+ const mockInteractionForProtection: Parameters[0] = {
guildId: message.guild.id,
user: message.author,
};
const abuseProtection = await enforceAbuseProtection(
mockInteractionForProtection,
- command,
- resolvedCommandName,
+ route ? { category: route.category, abuseProtection: route.module.abuseProtection } : command,
+ route?.legacyName ?? resolvedCommandName,
);
if (!abuseProtection.allowed) {
const formattedCooldown = formatCooldownDuration(abuseProtection.remainingMs);
const embed = createEmbed({
- title: 'Command Cooldown',
- description: `This command is on cooldown. Please wait ${formattedCooldown} before trying again.`,
+ title: 'Command-Cooldown',
+ description: `Dieser Command hat gerade Cooldown. Bitte warte ${formattedCooldown}, bevor du es erneut versuchst.`,
color: 'error',
});
await message.channel.send({ embeds: [embed] }).catch(() => {});
@@ -146,7 +160,7 @@ async function handlePrefixCommand(message, client) {
}
}
-async function handleCountingGame(message, client) {
+async function handleCountingGame(message: Message, client: NexoClient) {
try {
const config = await getCountingGameConfig(client, message.guild.id);
if (!config.enabled || !config.channelId || message.channel.id !== config.channelId) {
@@ -166,7 +180,7 @@ async function handleCountingGame(message, client) {
currentStreak: 0,
});
- const failureMessage = await message.channel.send(`❌ Count broken by <@${message.author.id}>. The sequence has been reset to **1**.`);
+ const failureMessage = await message.channel.send(`❌ <@${message.author.id}> hat die Zählung unterbrochen. Es geht wieder bei **1** los.`);
setTimeout(() => {
failureMessage.delete().catch(() => {});
}, 10000);
@@ -182,7 +196,7 @@ async function handleCountingGame(message, client) {
}
}
-async function handleLeveling(message, client) {
+async function handleLeveling(message: Message, client: NexoClient) {
try {
const rateLimitKey = `xp-event:${message.guild.id}:${message.author.id}`;
const canProcess = await checkRateLimit(rateLimitKey, MESSAGE_XP_RATE_LIMIT_ATTEMPTS, MESSAGE_XP_RATE_LIMIT_WINDOW_MS);
@@ -240,7 +254,7 @@ async function handleLeveling(message, client) {
finalXP = Math.floor(finalXP * levelingConfig.xpMultiplier);
}
- const result = await addXp(client, message.guild, message.member, finalXP);
+ const result = await addXp(client, message.guild, message.member!, finalXP);
if (result?.leveledUp) {
logger.info(
diff --git a/src/events/messageDelete.js b/src/events/messageDelete.ts
similarity index 67%
rename from src/events/messageDelete.js
rename to src/events/messageDelete.ts
index 54d7a9c1db..68d822adef 100644
--- a/src/events/messageDelete.js
+++ b/src/events/messageDelete.ts
@@ -1,8 +1,10 @@
import { Events } from 'discord.js';
+import type { GuildTextBasedChannel } from 'discord.js';
import { logEvent, EVENT_TYPES } from '../services/loggingService.js';
import { logger } from '../utils/logger.js';
import { getReactionRoleMessage, deleteReactionRoleMessage } from '../services/reactionRoleService.js';
import { formatLogLine } from '../utils/logging/logEmbeds.js';
+import type { BotEvent } from '../types/index.js';
const MAX_LOGGED_MESSAGE_CONTENT_LENGTH = 1024;
@@ -26,11 +28,11 @@ export default {
guildId: message.guild.id,
eventType: EVENT_TYPES.REACTION_ROLE_DELETE,
data: {
- title: 'Reaction Role Removed',
+ title: 'Reaktionsrolle entfernt',
lines: [
- formatLogLine('Channel', message.channel ? `${message.channel.name} ${message.channel.toString()}` : 'Unknown'),
- formatLogLine('Message ID', `\`${message.id}\``),
- formatLogLine('Cleanup', 'Database entry removed automatically'),
+ formatLogLine('Kanal', message.channel ? `${(message.channel as GuildTextBasedChannel).name} ${message.channel.toString()}` : 'Unbekannt'),
+ formatLogLine('Nachrichten-ID', `\`${message.id}\``),
+ formatLogLine('Bereinigung', 'Datenbankeintrag automatisch entfernt'),
],
quoted: true,
}
@@ -46,10 +48,10 @@ export default {
if (message.author?.bot) return;
const metaLines = [
- formatLogLine('Channel', message.channel ? `${message.channel.name} ${message.channel.toString()}` : 'Unknown'),
- formatLogLine('Message ID', `\`${message.id}\``),
- formatLogLine('Message author', message.author ? message.author.toString() : 'Unknown'),
- formatLogLine('Message created', ``),
+ formatLogLine('Kanal', message.channel ? `${(message.channel as GuildTextBasedChannel).name} ${message.channel.toString()}` : 'Unbekannt'),
+ formatLogLine('Nachrichten-ID', `\`${message.id}\``),
+ formatLogLine('Autor', message.author ? message.author.toString() : 'Unbekannt'),
+ formatLogLine('Erstellt', ``),
];
let messageBody = null;
@@ -60,7 +62,7 @@ export default {
}
if (message.attachments.size > 0) {
- metaLines.push(formatLogLine('Attachments', String(message.attachments.size)));
+ metaLines.push(formatLogLine('Anhänge', String(message.attachments.size)));
}
await logEvent({
@@ -68,10 +70,10 @@ export default {
guildId: message.guild.id,
eventType: EVENT_TYPES.MESSAGE_DELETE,
data: {
- title: 'Message deleted',
+ title: 'Nachricht gelöscht',
lines: metaLines,
quoted: true,
- section: messageBody ? { title: 'Message', body: messageBody || '*(empty message)*' } : null,
+ section: messageBody ? { title: 'Nachricht', body: messageBody || '*(leere Nachricht)*' } : null,
userId: message.author?.id,
channelId: message.channel.id,
}
@@ -81,4 +83,4 @@ export default {
logger.error('Error in messageDelete event:', error);
}
}
-};
+} satisfies BotEvent;
diff --git a/src/events/messageUpdate.js b/src/events/messageUpdate.ts
similarity index 60%
rename from src/events/messageUpdate.js
rename to src/events/messageUpdate.ts
index 18761fd553..b79118d6ea 100644
--- a/src/events/messageUpdate.js
+++ b/src/events/messageUpdate.ts
@@ -1,7 +1,9 @@
import { Events } from 'discord.js';
+import type { GuildTextBasedChannel } from 'discord.js';
import { logEvent, EVENT_TYPES } from '../services/loggingService.js';
import { logger } from '../utils/logger.js';
import { formatLogLine } from '../utils/logging/logEmbeds.js';
+import type { BotEvent } from '../types/index.js';
const MAX_LOGGED_EDIT_CONTENT_LENGTH = 512;
@@ -16,14 +18,14 @@ export default {
if (oldMessage.content === newMessage.content) return;
const metaLines = [
- formatLogLine('Channel', newMessage.channel ? `${newMessage.channel.name} ${newMessage.channel.toString()}` : 'Unknown'),
- formatLogLine('Message ID', `\`${newMessage.id}\``),
- formatLogLine('Message author', newMessage.author ? newMessage.author.toString() : 'Unknown'),
- formatLogLine('Message created', ``),
+ formatLogLine('Kanal', newMessage.channel ? `${(newMessage.channel as GuildTextBasedChannel).name} ${newMessage.channel.toString()}` : 'Unbekannt'),
+ formatLogLine('Nachrichten-ID', `\`${newMessage.id}\``),
+ formatLogLine('Autor', newMessage.author ? newMessage.author.toString() : 'Unbekannt'),
+ formatLogLine('Erstellt', ``),
];
- const oldContent = oldMessage.content || '*(empty message)*';
- const newContent = newMessage.content || '*(empty message)*';
+ const oldContent = oldMessage.content || '*(leere Nachricht)*';
+ const newContent = newMessage.content || '*(leere Nachricht)*';
const oldContentTruncated = oldContent.length > MAX_LOGGED_EDIT_CONTENT_LENGTH
? `${oldContent.substring(0, MAX_LOGGED_EDIT_CONTENT_LENGTH - 3)}...`
: oldContent;
@@ -36,12 +38,12 @@ export default {
guildId: newMessage.guild.id,
eventType: EVENT_TYPES.MESSAGE_EDIT,
data: {
- title: 'Message edited',
+ title: 'Nachricht bearbeitet',
lines: metaLines,
quoted: true,
fields: [
- { name: 'Before', value: oldContentTruncated, inline: true },
- { name: 'After', value: newContentTruncated, inline: true },
+ { name: 'Vorher', value: oldContentTruncated, inline: true },
+ { name: 'Nachher', value: newContentTruncated, inline: true },
],
userId: newMessage.author?.id,
channelId: newMessage.channel.id,
@@ -52,4 +54,4 @@ export default {
logger.error('Error in messageUpdate event:', error);
}
}
-};
+} satisfies BotEvent;
diff --git a/src/events/ready.js b/src/events/ready.ts
similarity index 97%
rename from src/events/ready.js
rename to src/events/ready.ts
index 2be8e5e424..048e8f9cf7 100644
--- a/src/events/ready.js
+++ b/src/events/ready.ts
@@ -5,6 +5,7 @@ import { reconcileReactionRoleMessages } from "../services/reactionRoleService.j
import { reconcileTicketPanels, reconcileVerificationPanels, reconcileReactionRolePanelHealth } from "../services/panelHealthService.js";
import { reconcileLevelRoles } from "../services/leveling/levelRoleSyncService.js";
import { initRiffyAfterReady } from "../services/music/riffySetup.js";
+import type { BotEvent } from '../types/index.js';
export default {
name: Events.ClientReady,
@@ -50,4 +51,4 @@ export default {
logger.error("Error in ready event:", error);
}
},
-};
\ No newline at end of file
+} satisfies BotEvent;
\ No newline at end of file
diff --git a/src/events/roleCreate.js b/src/events/roleCreate.ts
similarity index 78%
rename from src/events/roleCreate.js
rename to src/events/roleCreate.ts
index 1625974db1..dd1fef7914 100644
--- a/src/events/roleCreate.js
+++ b/src/events/roleCreate.ts
@@ -2,6 +2,7 @@ import { Events } from 'discord.js';
import { logEvent, EVENT_TYPES } from '../services/loggingService.js';
import { logger } from '../utils/logger.js';
import { buildRoleAuditLines } from '../utils/logging/logEmbeds.js';
+import type { BotEvent } from '../types/index.js';
export default {
name: Events.GuildRoleCreate,
@@ -18,8 +19,8 @@ export default {
guildId: role.guild.id,
eventType: EVENT_TYPES.ROLE_CREATE,
data: {
- title: 'Role Created',
- headline: `${role.toString()} was created`,
+ title: 'Rolle erstellt',
+ headline: `${role.toString()} wurde erstellt`,
lines,
},
});
@@ -28,4 +29,4 @@ export default {
logger.error('Error in roleCreate event:', error);
}
}
-};
\ No newline at end of file
+} satisfies BotEvent;
\ No newline at end of file
diff --git a/src/events/roleDelete.js b/src/events/roleDelete.ts
similarity index 78%
rename from src/events/roleDelete.js
rename to src/events/roleDelete.ts
index 1e16b23dc8..0363d3646f 100644
--- a/src/events/roleDelete.js
+++ b/src/events/roleDelete.ts
@@ -2,6 +2,7 @@ import { Events } from 'discord.js';
import { logEvent, EVENT_TYPES } from '../services/loggingService.js';
import { logger } from '../utils/logger.js';
import { buildRoleAuditLines } from '../utils/logging/logEmbeds.js';
+import type { BotEvent } from '../types/index.js';
export default {
name: Events.GuildRoleDelete,
@@ -18,8 +19,8 @@ export default {
guildId: role.guild.id,
eventType: EVENT_TYPES.ROLE_DELETE,
data: {
- title: 'Role Deleted',
- headline: `**${role.name}** was deleted`,
+ title: 'Rolle gelöscht',
+ headline: `**${role.name}** wurde gelöscht`,
lines,
},
});
@@ -28,4 +29,4 @@ export default {
logger.error('Error in roleDelete event:', error);
}
}
-};
\ No newline at end of file
+} satisfies BotEvent