From 2b3fa3eb69504a20166f49d118c166ca9e594320 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Wed, 22 Jul 2026 23:10:19 +0200 Subject: [PATCH] Initiales Setup: minimaler Discord-Bot mit /ping, Docker-Deployment und README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .env.example | 11 ++ .gitattributes | 2 + .gitignore | 13 ++ Dockerfile | 17 ++ README.md | 114 ++++++++++++++ docker-compose.yml | 17 ++ package-lock.json | 331 +++++++++++++++++++++++++++++++++++++++ package.json | 20 +++ src/bot/client.js | 61 ++++++++ src/bot/commands/ping.js | 14 ++ src/config.js | 20 +++ src/index.js | 13 ++ 12 files changed, 633 insertions(+) create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/bot/client.js create mode 100644 src/bot/commands/ping.js create mode 100644 src/config.js create mode 100644 src/index.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5d5ab0c --- /dev/null +++ b/.env.example @@ -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= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f995138 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Einheitliche LF-Zeilenenden (wichtig für Docker/Linux trotz Windows-Entwicklung) +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d92dd54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Secrets — niemals committen! +.env + +# Dependencies +node_modules/ + +# Laufzeitdaten (SQLite etc.) +data/ + +# Logs & OS-Kram +*.log +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..74bd565 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e34a613 --- /dev/null +++ b/README.md @@ -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. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..75accfb --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..32c6d7e --- /dev/null +++ b/package-lock.json @@ -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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..091bc8c --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/bot/client.js b/src/bot/client.js new file mode 100644 index 0000000..d095545 --- /dev/null +++ b/src/bot/client.js @@ -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'})` + ); +} diff --git a/src/bot/commands/ping.js b/src/bot/commands/ping.js new file mode 100644 index 0000000..f64cbb6 --- /dev/null +++ b/src/bot/commands/ping.js @@ -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**` + ); +} diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..6494753 --- /dev/null +++ b/src/config.js @@ -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, +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..d3e7a50 --- /dev/null +++ b/src/index.js @@ -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); +}