Initiales Setup: minimaler Discord-Bot mit /ping, Docker-Deployment und README

- discord.js v14 Bot mit Slash-Command-Registry (Guild oder global)
- Konfiguration über .env mit Validierung (config.js)
- Dockerfile + docker-compose.yml für Portainer-Deployment
- README mit Schritt-für-Schritt-Anleitung (Discord Developer Portal)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:10:19 +02:00
co-authored by Claude Opus 4.8
commit 2b3fa3eb69
12 changed files with 633 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Vorlage — nach .env kopieren und Werte eintragen (NIEMALS .env committen!)
# Discord Developer Portal → deine App → Bot → "Reset Token"
DISCORD_TOKEN=
# Discord Developer Portal → deine App → General Information → "Application ID"
DISCORD_CLIENT_ID=
# Rechtsklick auf deinen Discord-Server → "Server-ID kopieren" (Entwicklermodus nötig)
# Optional: wenn gesetzt, sind Slash-Commands sofort verfügbar statt nach bis zu 1h
DISCORD_GUILD_ID=
+2
View File
@@ -0,0 +1,2 @@
# Einheitliche LF-Zeilenenden (wichtig für Docker/Linux trotz Windows-Entwicklung)
* text=auto eol=lf
+13
View File
@@ -0,0 +1,13 @@
# Secrets — niemals committen!
.env
# Dependencies
node_modules/
# Laufzeitdaten (SQLite etc.)
data/
# Logs & OS-Kram
*.log
.DS_Store
Thumbs.db
+17
View File
@@ -0,0 +1,17 @@
# EcoBot — Discord-Bot + (später) Webinterface
FROM node:22-alpine
WORKDIR /app
# Erst nur Manifest kopieren → Docker-Layer-Cache für npm ci
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
# Laufzeitdaten (SQLite ab Feature 2) landen in /app/data → Volume
RUN mkdir -p /app/data
ENV NODE_ENV=production
CMD ["node", "src/index.js"]
+114
View File
@@ -0,0 +1,114 @@
# EcoBot
Discord-Bot + Webinterface für die EcoGame-Community.
**Features (Roadmap):**
1. ✅ Bot online + `/ping` Slash-Command
2. ⬜ Commit-Feed: Gitea-Push-Webhooks → Discord-Embeds + SQLite-Archiv
3. ⬜ Devlog-Archiv: tägliche Devlogs archivieren
4. ⬜ Webinterface: Discord-Login, Devlog- & Commit-Seiten
**Stack:** Node.js 20+, discord.js v14, Fastify (ab Feature 2), React (ab Feature 4), SQLite, Docker
---
## Setup: Discord-App anlegen (einmalig)
### 1. App erstellen
1. Öffne das [Discord Developer Portal](https://discord.com/developers/applications)
2. **New Application** → Name: `EcoBot` → Create
3. Unter **General Information** die **Application ID** kopieren
→ das ist `DISCORD_CLIENT_ID`
### 2. Bot-Token holen
1. Linke Seitenleiste → **Bot**
2. **Reset Token** → Token kopieren → das ist `DISCORD_TOKEN`
⚠️ Der Token wird nur einmal angezeigt — direkt in die `.env` eintragen!
3. Auf der Bot-Seite weiter unten: **Message Content Intent** aktivieren
(brauchen wir ab Feature 3 zum Lesen der Devlog-Nachrichten)
### 3. Bot auf den Server einladen
1. Linke Seitenleiste → **OAuth2****URL Generator**
2. Scopes ankreuzen: `bot` und `applications.commands`
3. Bot Permissions ankreuzen:
- **Send Messages**
- **Embed Links**
- **Read Message History**
4. Generierte URL unten kopieren, im Browser öffnen, deinen Server auswählen → **Autorisieren**
### 4. Server-ID holen (für sofortige Slash-Commands)
1. In Discord: Einstellungen → Erweitert → **Entwicklermodus** aktivieren
2. Rechtsklick auf deinen Server → **Server-ID kopieren**
→ das ist `DISCORD_GUILD_ID`
### 5. .env anlegen
```
cp .env.example .env
```
Dann die drei Werte eintragen. Die `.env` ist in `.gitignore` und landet nie im Repo.
---
## Lokal starten (Entwicklung)
```
npm install
npm run dev
```
Erwartete Ausgabe:
```
[bot] Eingeloggt als EcoBot#1234
[bot] 1 Slash-Command(s) registriert (Guild)
```
Dann in Discord: `/ping` → Bot antwortet mit Latenz. 🎉
---
## Deployment (Docker / Portainer)
### Variante A: Portainer-Stack aus Git (empfohlen)
1. Portainer → **Stacks****Add stack**
2. **Repository** wählen:
- URL: `https://git.d4rkst3r.de/D4rkst3r/ecobot.git`
- Compose path: `docker-compose.yml`
3. Unter **Environment variables** die drei Werte aus der `.env` eintragen
(`DISCORD_TOKEN`, `DISCORD_CLIENT_ID`, `DISCORD_GUILD_ID`)
— alternativ `env_file` weglassen und die Variablen direkt im Stack setzen
4. **Deploy the stack**
### Variante B: Manuell auf dem Server
```
git clone git@gitea:D4rkst3r/ecobot.git
cd ecobot
cp .env.example .env # Werte eintragen
docker compose up -d --build
```
Logs prüfen:
```
docker logs -f ecobot
```
---
## Projektstruktur
```
ecobot/
├── src/
│ ├── index.js # Einstiegspunkt
│ ├── config.js # Env-Konfiguration mit Validierung
│ └── bot/
│ ├── client.js # Discord-Client, Command-Registry, Interactions
│ └── commands/
│ └── ping.js # /ping — Lebenszeichen
├── .env.example # Vorlage für Secrets (nach .env kopieren)
├── Dockerfile
├── docker-compose.yml
└── README.md
```
Neue Slash-Commands: Datei in `src/bot/commands/` anlegen (exportiert `data` + `execute`)
und in `src/bot/client.js` bei `commandModules` eintragen.
+17
View File
@@ -0,0 +1,17 @@
# EcoBot Stack — deploybar über Portainer (Stacks → Add stack)
services:
ecobot:
build: .
image: ecobot:latest
container_name: ecobot
restart: unless-stopped
env_file: .env
# Ab Feature 2: HTTP-Endpoint für Gitea-Webhooks + Webinterface
# ports:
# - "3080:3080"
volumes:
# SQLite & Co. überleben Container-Neustarts
- ecobot_data:/app/data
volumes:
ecobot_data:
+331
View File
@@ -0,0 +1,331 @@
{
"name": "ecobot",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecobot",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"discord.js": "^14.16.3",
"dotenv": "^16.4.7"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@discordjs/builders": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/formatters": "^0.6.2",
"@discordjs/util": "^1.2.0",
"@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.38.40",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.3"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/collection": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=16.11.0"
}
},
"node_modules/@discordjs/formatters": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.3.tgz",
"integrity": "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.1",
"@discordjs/util": "^1.2.0",
"@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.5",
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.38.50",
"magic-bytes.js": "^1.13.0",
"tslib": "^2.6.3",
"undici": "^6.27.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/util": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.0",
"@discordjs/rest": "^2.5.1",
"@discordjs/util": "^1.1.0",
"@sapphire/async-queue": "^1.5.2",
"@types/ws": "^8.5.10",
"@vladfrangu/async_event_emitter": "^2.2.4",
"discord-api-types": "^0.38.1",
"tslib": "^2.6.2",
"ws": "^8.17.0"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@sapphire/async-queue": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@sapphire/shapeshift": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=v16"
}
},
"node_modules/@sapphire/snowflake": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@types/node": {
"version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vladfrangu/async_event_emitter": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/discord-api-types": {
"version": "0.38.50",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.50.tgz",
"integrity": "sha512-J2n/bpIETX3DQ6AJ7/0xbsTLmYiJQtO/LKcXKC1YDbB56OUwtDbdXOFE8Q4g8jVGHBR2VAy1+D4ngaIgkMNV9w==",
"license": "MIT",
"workspaces": [
"scripts/actions/documentation"
]
},
"node_modules/discord.js": {
"version": "14.27.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.27.0.tgz",
"integrity": "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/builders": "^1.14.1",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.2",
"@discordjs/rest": "^2.6.2",
"@discordjs/util": "^1.2.0",
"@discordjs/ws": "^1.2.3",
"@sapphire/snowflake": "3.5.5",
"discord-api-types": "^0.38.49",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.13.0",
"tslib": "^2.6.3",
"undici": "^6.27.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
"license": "MIT"
},
"node_modules/magic-bytes.js": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
"license": "MIT"
},
"node_modules/ts-mixer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/undici": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"license": "MIT"
},
"node_modules/ws": {
"version": "8.21.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "ecobot",
"version": "0.1.0",
"description": "Discord-Bot + Webinterface für die EcoGame-Community (Commit-Feed, Devlog-Archiv)",
"author": "D4rkst3r",
"license": "MIT",
"type": "module",
"main": "src/index.js",
"engines": {
"node": ">=20"
},
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js"
},
"dependencies": {
"discord.js": "^14.16.3",
"dotenv": "^16.4.7"
}
}
+61
View File
@@ -0,0 +1,61 @@
// Discord-Client: Commands laden, registrieren und Interactions verarbeiten
import { Client, Collection, Events, GatewayIntentBits, REST, Routes } from 'discord.js';
import { config } from '../config.js';
import * as ping from './commands/ping.js';
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
const commandModules = [ping];
export async function startBot() {
const client = new Client({
intents: [GatewayIntentBits.Guilds],
});
// Commands in Collection ablegen für schnellen Zugriff im Interaction-Handler
client.commands = new Collection();
for (const command of commandModules) {
client.commands.set(command.data.name, command);
}
client.once(Events.ClientReady, async (readyClient) => {
console.log(`[bot] Eingeloggt als ${readyClient.user.tag}`);
await registerCommands();
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
console.error(`[bot] Fehler bei /${interaction.commandName}:`, error);
const reply = { content: '❌ Da ist etwas schiefgelaufen.', ephemeral: true };
if (interaction.replied || interaction.deferred) {
await interaction.followUp(reply).catch(() => {});
} else {
await interaction.reply(reply).catch(() => {});
}
}
});
await client.login(config.discordToken);
return client;
}
/** Slash-Commands bei Discord registrieren (Guild = sofort sichtbar, global = bis zu 1h Delay) */
async function registerCommands() {
const rest = new REST().setToken(config.discordToken);
const body = commandModules.map((c) => c.data.toJSON());
const route = config.discordGuildId
? Routes.applicationGuildCommands(config.discordClientId, config.discordGuildId)
: Routes.applicationCommands(config.discordClientId);
await rest.put(route, { body });
console.log(
`[bot] ${body.length} Slash-Command(s) registriert (${config.discordGuildId ? 'Guild' : 'global'})`
);
}
+14
View File
@@ -0,0 +1,14 @@
// Lebenszeichen-Command: /ping → antwortet mit Latenz
import { SlashCommandBuilder } from 'discord.js';
export const data = new SlashCommandBuilder()
.setName('ping')
.setDescription('Lebenszeichen — zeigt die Bot-Latenz');
export async function execute(interaction) {
const sent = await interaction.reply({ content: '🏓 Pong!', withResponse: true });
const latency = sent.resource.message.createdTimestamp - interaction.createdTimestamp;
await interaction.editReply(
`🏓 Pong! Latenz: **${latency}ms** | API: **${Math.round(interaction.client.ws.ping)}ms**`
);
}
+20
View File
@@ -0,0 +1,20 @@
// Zentrale Konfiguration — liest alle Werte aus der Umgebung (.env lokal, env_file im Container)
import 'dotenv/config';
/** Pflicht-Variable lesen, bei Fehlen sofort mit klarer Meldung abbrechen */
function required(name) {
const value = process.env[name];
if (!value) {
console.error(`[config] Fehlende Umgebungsvariable: ${name} — siehe .env.example`);
process.exit(1);
}
return value;
}
export const config = {
// Discord Bot
discordToken: required('DISCORD_TOKEN'),
discordClientId: required('DISCORD_CLIENT_ID'),
// Optional: Guild-ID für sofortige Slash-Command-Registrierung (global dauert bis zu 1h)
discordGuildId: process.env.DISCORD_GUILD_ID || null,
};
+13
View File
@@ -0,0 +1,13 @@
// Einstiegspunkt — startet den Discord-Bot (Webserver folgt in Feature 2)
import { startBot } from './bot/client.js';
process.on('unhandledRejection', (error) => {
console.error('[main] Unhandled Rejection:', error);
});
try {
await startBot();
} catch (error) {
console.error('[main] Bot-Start fehlgeschlagen:', error);
process.exit(1);
}