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:
@@ -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=
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Einheitliche LF-Zeilenenden (wichtig für Docker/Linux trotz Windows-Entwicklung)
|
||||||
|
* text=auto eol=lf
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Secrets — niemals committen!
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Laufzeitdaten (SQLite etc.)
|
||||||
|
data/
|
||||||
|
|
||||||
|
# Logs & OS-Kram
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
+17
@@ -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"]
|
||||||
@@ -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.
|
||||||
@@ -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:
|
||||||
Generated
+331
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'})`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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**`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user