From 40be1e181cb08a285c7ff7d6785662f9286ad601 Mon Sep 17 00:00:00 2001 From: sarah Date: Sun, 22 Mar 2026 16:29:30 +0100 Subject: [PATCH] =?UTF-8?q?Initial:=20Pixelp=C3=B6bel=20Discord=20Bot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modular Discord Bot mit TypeScript und discord.js - Grouped Commands (/admin, /owner subcommands) - SQLite Datenbank mit Persistenz - Twitch Monitoring mit Stream-ID Tracking - Konfigurierbares Warn-System (Auto-Kick/Mute/Ban) - Mute mit verschiedenen Zeiteinheiten (sec, min, hour, day, week, month, year, perm) - Auto-Deploy beim Start (Option 3) - Docker Support Features: - /ping, /help, /twitch (online, add, remove, list) - /admin (kick, warn, unwarn, mute, unmute, ban, unban, config) - /owner (deploy, stats, servers) Co-Authored-By: Claude Opus 4.6 --- .dockerignore | 10 + .env.example | 4 + .gitea/workflows/docker-autobuild.yaml | 31 + .gitea/workflows/docker-build.yaml | 36 + .gitignore | 5 + AGENTS-BOT.md | 293 +++++ Dockerfile | 16 + README.md | 88 ++ docker-compose.yml | 17 + package-lock.json | 1360 ++++++++++++++++++++++++ package.json | 28 + src/commands/utility/admin.ts | 419 ++++++++ src/commands/utility/help.ts | 59 + src/commands/utility/owner.ts | 107 ++ src/commands/utility/ping.ts | 14 + src/commands/utility/twitch.ts | 121 +++ src/deploy-commands.ts | 9 + src/events/interactionCreate.ts | 32 + src/events/ready.ts | 10 + src/index.ts | 62 ++ src/structures/Command.ts | 6 + src/structures/Database.ts | 103 ++ src/structures/Deployer.ts | 59 + src/structures/ExtendedClient.ts | 17 + src/structures/TwitchManager.ts | 120 +++ tsconfig.json | 16 + 26 files changed, 3042 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/workflows/docker-autobuild.yaml create mode 100644 .gitea/workflows/docker-build.yaml create mode 100644 .gitignore create mode 100644 AGENTS-BOT.md 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/commands/utility/admin.ts create mode 100644 src/commands/utility/help.ts create mode 100644 src/commands/utility/owner.ts create mode 100644 src/commands/utility/ping.ts create mode 100644 src/commands/utility/twitch.ts create mode 100644 src/deploy-commands.ts create mode 100644 src/events/interactionCreate.ts create mode 100644 src/events/ready.ts create mode 100644 src/index.ts create mode 100644 src/structures/Command.ts create mode 100644 src/structures/Database.ts create mode 100644 src/structures/Deployer.ts create mode 100644 src/structures/ExtendedClient.ts create mode 100644 src/structures/TwitchManager.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..050d9da --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +npm-debug.log +.env +.git +.gitignore +README.md +AGENTS-BOT.md +dist +*.md +.DS_Store \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f7ab4c5 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +DISCORD_TOKEN=dein_bot_token +CLIENT_ID=deine_client_id +TWITCH_CLIENT_ID=deine_twitch_id +TWITCH_CLIENT_SECRET=dein_twitch_secret \ No newline at end of file diff --git a/.gitea/workflows/docker-autobuild.yaml b/.gitea/workflows/docker-autobuild.yaml new file mode 100644 index 0000000..1216f33 --- /dev/null +++ b/.gitea/workflows/docker-autobuild.yaml @@ -0,0 +1,31 @@ +name: Auto Build and Push Docker Image + +on: + push: + branches: + - main + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Build and Push to local registry + run: | + REGISTRY="192.168.88.201:5000" + IMAGE_NAME="${{ gitea.repository }}" + + # Automatische Versionierung mit Autobuild-Suffix + VERSION="v${{ gitea.run_number }}-autobuild" + FULL_IMAGE_PATH="$REGISTRY/$IMAGE_NAME" + + echo "Starte automatischen Build für Version: $VERSION" + + docker build -t $FULL_IMAGE_PATH:latest . + docker tag $FULL_IMAGE_PATH:latest $FULL_IMAGE_PATH:$VERSION + + docker push $FULL_IMAGE_PATH:latest + docker push $FULL_IMAGE_PATH:$VERSION \ No newline at end of file diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml new file mode 100644 index 0000000..cb94379 --- /dev/null +++ b/.gitea/workflows/docker-build.yaml @@ -0,0 +1,36 @@ +name: Build and Push Docker Image + +on: + workflow_dispatch: + inputs: + tag_name: + description: 'Version (z.B. 1.0.0). Leer lassen für automatische v-Nummer.' + required: false + default: '' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Build and Push to local registry + run: | + REGISTRY="192.168.88.201:5000" + # Hier wird automatisch der Name des neuen Projekts eingesetzt: + IMAGE_NAME="${{ gitea.repository }}" + + if [ -z "${{ github.event.inputs.tag_name }}" ]; then + VERSION="v${{ gitea.run_number }}" + else + VERSION="${{ github.event.inputs.tag_name }}" + fi + + FULL_IMAGE_PATH="$REGISTRY/$IMAGE_NAME" + + docker build -t $FULL_IMAGE_PATH:latest . + docker tag $FULL_IMAGE_PATH:latest $FULL_IMAGE_PATH:$VERSION + + docker push $FULL_IMAGE_PATH:latest + docker push $FULL_IMAGE_PATH:$VERSION \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edfc413 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +data/ +.env +*.log \ No newline at end of file diff --git a/AGENTS-BOT.md b/AGENTS-BOT.md new file mode 100644 index 0000000..a087485 --- /dev/null +++ b/AGENTS-BOT.md @@ -0,0 +1,293 @@ +# AI Agent Guide: Pixelpöbel Discord Bot + +This document is intended for AI agents to understand and recreate the `pixelpöbel` Discord bot. + +## 🏗 Architecture Overview + +The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScript Modules)** and **TypeScript**. + +### Key Design Patterns: +1. **Extended Client Pattern:** Extend `discord.js` `Client` class to hold global state. +2. **Dynamic Discovery:** `index.ts` uses `readdirSync` and dynamic `import()` to register commands/events. +3. **Interface-Driven Commands:** All commands implement `Command` interface. +4. **SQLite Database:** Persistent data storage with `better-sqlite3`. +5. **Twitch Monitoring:** Periodic API polling with Stream ID tracking. +6. **Grouped Commands:** Admin/Owner commands grouped under subcommands. + +## 📁 Project Structure + +``` +pixelpoebel/ +├── src/ +│ ├── index.ts # Entry point, loads commands/events +│ ├── deploy-commands.ts # Deploy script for Discord +│ ├── commands/ +│ │ └── utility/ # All commands grouped here +│ │ ├── ping.ts +│ │ ├── help.ts +│ │ ├── twitch.ts +│ │ ├── admin.ts # All admin subcommands +│ │ └── owner.ts # All owner subcommands +│ ├── events/ +│ │ ├── ready.ts +│ │ └── interactionCreate.ts +│ └── structures/ +│ ├── ExtendedClient.ts +│ ├── Command.ts +│ ├── Database.ts +│ ├── TwitchManager.ts +│ └── Deployer.ts +├── data/ # SQLite database (volume mounted) +├── package.json +├── tsconfig.json +├── Dockerfile +├── docker-compose.yml +└── .env.example +``` + +## 📝 Implementation Steps + +### 1. Project Setup + +```json +{ + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "nodemon --watch 'src/**/*.ts' --exec 'node --loader ts-node/esm' src/index.ts", + "deploy": "node --loader ts-node/esm src/deploy-commands.ts" + } +} +``` + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true + } +} +``` + +### 2. Database Structure + +```sql +-- guild_settings +CREATE TABLE guild_settings ( + guild_id TEXT PRIMARY KEY, + prefix TEXT DEFAULT '!', + mod_log_channel TEXT, + welcome_channel TEXT, + warn_threshold INTEGER DEFAULT 3, + warn_action TEXT DEFAULT 'ban', + warn_mute_duration INTEGER DEFAULT 1800 +); + +-- mod_logs +CREATE TABLE mod_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT, + user_id TEXT, + moderator_id TEXT, + action TEXT, + reason TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- command_stats +CREATE TABLE command_stats ( + command_name TEXT PRIMARY KEY, + uses INTEGER DEFAULT 0 +); + +-- dm_users +CREATE TABLE dm_users ( + user_id TEXT PRIMARY KEY, + username TEXT, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- twitch_monitors +CREATE TABLE twitch_monitors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT, + channel_name TEXT, + discord_channel_id TEXT, + custom_message TEXT, + last_status TEXT DEFAULT 'offline', + last_stream_id TEXT, + UNIQUE(guild_id, channel_name) +); + +-- auto_responses +CREATE TABLE auto_responses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT, + trigger_word TEXT, + response_text TEXT, + UNIQUE(guild_id, trigger_word) +); +``` + +### 3. Key Components + +**ExtendedClient.ts:** +```typescript +import { Client, Collection, GatewayIntentBits } from 'discord.js'; +import { Command } from './Command.js'; + +export class ExtendedClient extends Client { + public commands: Collection = new Collection(); + + constructor() { + super({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers, + ], + }); + } +} +``` + +**Command Interface:** +```typescript +export interface Command { + data: any; + category: 'Owner' | 'Admin' | 'Public'; + execute: (interaction: any, client: any) => Promise | void; + cooldown?: number; +} +``` + +**Deployer - Auto-Deploy (Option 3):** +```typescript +export class Deployer { + static async deployIfMissing(clientId: string, token: string) { + const rest = new REST().setToken(token); + + try { + const data = await rest.get(Routes.applicationCommands(clientId)); + if (data.length > 0) { + console.log('[DEPLOYER] Commands already registered. Skipping deployment.'); + return data.length; + } + } catch (error) { + // Commands don't exist yet + } + + return this.deploy(clientId, token); + } +} +``` + +### 4. Command Examples + +**Grouped Command (admin):** +```typescript +data: new SlashCommandBuilder() + .setName('admin') + .setDescription('Admin-Commands') + .addSubcommand(sub => sub.setName('kick').setDescription('Kickt einen Nutzer') + .addUserOption(opt => opt.setName('target').setDescription('Nutzer').setRequired(true)) + .addStringOption(opt => opt.setName('reason').setDescription('Grund'))) + .addSubcommand(sub => sub.setName('warn').setDescription('Verwarnt einen Nutzer') + .addUserOption(opt => opt.setName('target').setDescription('Nutzer').setRequired(true)) + .addStringOption(opt => opt.setName('reason').setDescription('Grund').setRequired(true))) + // ... more subcommands +``` + +### 5. Twitch Monitoring with Stream ID + +```typescript +// Stream ID tracking prevents duplicate notifications +const isNewStream = monitor.last_stream_id !== stream.id; +const wasOffline = monitor.last_status === 'offline'; + +if (wasOffline || (monitor.last_status === 'online' && isNewStream)) { + // Send notification + DB.run('UPDATE twitch_monitors SET last_status = "online", last_stream_id = ? WHERE id = ?', + stream.id, monitor.id); +} +``` + +### 6. Configurable Warn System + +**Database Schema:** +- `warn_threshold`: Number of warns before action +- `warn_action`: 'none', 'kick', 'mute', 'ban' +- `warn_mute_duration`: Duration in seconds for mute + +**Auto-Action Logic:** +```typescript +const warnCount = countResult?.count ?? 0; +const warnThreshold = settings?.warn_threshold ?? 3; +const warnAction = settings?.warn_action ?? 'ban'; + +if (warnCount >= warnThreshold) { + if (warnAction === 'kick') await target.kick(...); + else if (warnAction === 'mute') await target.timeout(duration, ...); + else if (warnAction === 'ban') await target.ban(...); +} +``` + +### 7. Mute with Multiple Time Units + +```typescript +const unitSeconds: Record = { + sec: 1, + min: 60, + hour: 60 * 60, + day: 60 * 60 * 24, + week: 60 * 60 * 24 * 7, + month: 60 * 60 * 24 * 30, + year: 60 * 60 * 24 * 365 +}; + +if (unit === 'perm') { + durationMs = null; // Permanent +} else { + durationMs = duration * unitSeconds[unit!] * 1000; +} +``` + +## 🚀 Build & Deploy + +```bash +# Build +npm run build + +# Deploy Commands +npm run deploy + +# Run +npm start + +# Docker +docker-compose up -d --build +``` + +## ⚠️ Critical Constraints + +1. **Environment Variables:** Always use `process.env`, never hardcode. +2. **Type Safety:** Use `any` for command `data` and `execute` parameters (compatibility). +3. **Interaction States:** Check `interaction.replied` before replying. +4. **Twitch API:** Max 100 channels per request, batch properly. +5. **Deploy:** Commands NOT auto-registered on start – use `/owner deploy`. + +## 📋 Command Summary + +| Command | Subcommands | Permission | +|---------|-------------|------------| +| `/ping` | – | Public | +| `/help` | – | Public | +| `/twitch` | online, add, remove, list | Public/Admin | +| `/admin` | kick, warn, unwarn, mute, unmute, ban, unban, config | Admin | +| `/owner` | deploy, stats, servers | Owner | \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bda71ab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM node:22-alpine + +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci --only=production + +# Copy source and build +COPY . . +RUN npm run build + +# Create data directory +RUN mkdir -p data + +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b93c4f7 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# Pixelpöbel Discord Bot + +Ein moderner, modularer Discord-Bot in TypeScript. + +## 🚀 Installation + +```bash +npm install +``` + +## ⚙️ Konfiguration + +Erstelle `.env` Datei: +```env +DISCORD_TOKEN=dein_bot_token +CLIENT_ID=deine_client_id +TWITCH_CLIENT_ID=deine_twitch_id +TWITCH_CLIENT_SECRET=dein_twitch_secret +``` + +## 🚀 Start + +```bash +# Development +npm run dev + +# Production +npm run build +npm start +``` + +## 🐳 Docker + +```bash +# Build und Start +docker-compose up -d --build + +# Logs ansehen +docker-compose logs -f pixelpoebel-bot + +# Stoppen +docker-compose down + +# Container neu bauen +docker-compose up -d --build --force-recreate +``` + +## 📋 Commands registrieren + +```bash +npm run deploy +``` + +## 🎮 Befehle + +### Öffentlich +- `/ping` – Latenz test +- `/help` – Alle Befehle +- `/twitch online ` – Twitch-Status +- `/twitch list` – Überwachte Kanäle + +### Admin +- `/twitch add ` – Kanal überwachen +- `/twitch remove ` – Kanal entfernen +- `/admin kick ` – Nutzer kicken +- `/admin warn ` – Nutzer verwarnen +- `/admin unwarn ` – Letzte Warnung entfernen +- `/admin mute ` – Nutzer muten +- `/admin unmute ` – Nutzer entmuten +- `/admin ban ` – Nutzer bannen +- `/admin unban ` – Nutzer entbannen +- `/admin config` – Warn-System konfigurieren + +### Owner +- `/owner deploy` – Commands neu registrieren +- `/owner stats` – Bot-Statistiken +- `/owner servers` – Alle Server + +## 📡 Features + +- ✅ Modular Architecture +- ✅ TypeScript +- ✅ SQLite Database +- ✅ Twitch Monitoring +- ✅ Auto-Deploy beim Start +- ✅ Moderation Tools +- ✅ Konfigurierbares Warn-System +- ✅ Docker Support \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9eb396b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + pixelpoebel-bot: + build: . + container_name: pixelpoebel-bot + restart: unless-stopped + env_file: + - .env + volumes: + - ./data:/app/data + networks: + - pixelpoebel-net + +networks: + pixelpoebel-net: + driver: bridge \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..094d718 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1360 @@ +{ + "name": "pixelpoebel", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pixelpoebel", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "better-sqlite3": "^12.8.0", + "discord.js": "^14.18.0", + "dotenv": "^16.4.7" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22.13.9", + "nodemon": "^3.1.9", + "ts-node": "^10.9.2", + "typescript": "^5.8.2" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", + "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==", + "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.33", + "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.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "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/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.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/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.42", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", + "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "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/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "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/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "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/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "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 + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..46ba5ce --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "pixelpoebel", + "version": "1.0.0", + "description": "Moderner, erweiterbarer Discord Bot", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "nodemon --watch 'src/**/*.ts' --exec 'node --loader ts-node/esm' src/index.ts", + "deploy": "node --loader ts-node/esm src/deploy-commands.ts" + }, + "keywords": ["discord", "bot", "typescript", "modular"], + "author": "sarah", + "license": "MIT", + "dependencies": { + "better-sqlite3": "^12.8.0", + "discord.js": "^14.18.0", + "dotenv": "^16.4.7" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22.13.9", + "nodemon": "^3.1.9", + "ts-node": "^10.9.2", + "typescript": "^5.8.2" + } +} \ No newline at end of file diff --git a/src/commands/utility/admin.ts b/src/commands/utility/admin.ts new file mode 100644 index 0000000..319a18a --- /dev/null +++ b/src/commands/utility/admin.ts @@ -0,0 +1,419 @@ +import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember } from 'discord.js'; +import { Command } from '../../structures/Command.js'; + +const command: Command = { + data: new SlashCommandBuilder() + .setName('admin') + .setDescription('Admin-Commands') + .addSubcommand(subcommand => + subcommand + .setName('kick') + .setDescription('Kickt einen Nutzer') + .addUserOption(option => + option.setName('target') + .setDescription('Der zu kickende Nutzer') + .setRequired(true)) + .addStringOption(option => + option.setName('reason') + .setDescription('Grund für den Kick'))) + .addSubcommand(subcommand => + subcommand + .setName('warn') + .setDescription('Verwarnt einen Nutzer') + .addUserOption(option => + option.setName('target') + .setDescription('Der zu verwarnende Nutzer') + .setRequired(true)) + .addStringOption(option => + option.setName('reason') + .setDescription('Grund für die Verwarnung') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('unwarn') + .setDescription('Entfernt die letzte Warnung') + .addUserOption(option => + option.setName('target') + .setDescription('Der Nutzer') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('mute') + .setDescription('Muted einen Nutzer') + .addUserOption(option => + option.setName('target') + .setDescription('Der zu mutende Nutzer') + .setRequired(true)) + .addIntegerOption(option => + option.setName('duration') + .setDescription('Dauer (Zahl)') + .setMinValue(1)) + .addStringOption(option => + option.setName('unit') + .setDescription('Zeiteinheit') + .addChoices( + { name: 'Sekunden', value: 'sec' }, + { name: 'Minuten', value: 'min' }, + { name: 'Stunden', value: 'hour' }, + { name: 'Tage', value: 'day' }, + { name: 'Wochen', value: 'week' }, + { name: 'Monate', value: 'month' }, + { name: 'Jahre', value: 'year' }, + { name: 'Permanent', value: 'perm' } + )) + .addStringOption(option => + option.setName('reason') + .setDescription('Grund für den Mute'))) + .addSubcommand(subcommand => + subcommand + .setName('unmute') + .setDescription('Entmutet einen Nutzer') + .addUserOption(option => + option.setName('target') + .setDescription('Der zu entmutende Nutzer') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('ban') + .setDescription('Bannt einen Nutzer') + .addUserOption(option => + option.setName('target') + .setDescription('Der zu bannende Nutzer') + .setRequired(true)) + .addStringOption(option => + option.setName('reason') + .setDescription('Grund für den Ban')) + .addIntegerOption(option => + option.setName('delete_days') + .setDescription('Nachrichten der letzten X Tage löschen') + .setMinValue(0) + .setMaxValue(7))) + .addSubcommand(subcommand => + subcommand + .setName('unban') + .setDescription('Entbannt einen Nutzer') + .addStringOption(option => + option.setName('target') + .setDescription('Nutzer-ID oder @username') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('config') + .setDescription('Konfiguriert das Warn-System') + .addStringOption(option => + option.setName('setting') + .setDescription('Welche Einstellung ändern?') + .setRequired(true) + .addChoices( + { name: 'warn_threshold', value: 'warn_threshold' }, + { name: 'warn_action', value: 'warn_action' }, + { name: 'warn_mute_duration', value: 'warn_mute_duration' }, + { name: 'show', value: 'show' })) + .addStringOption(option => + option.setName('value') + .setDescription('Neuer Wert')) + .addIntegerOption(option => + option.setName('duration_value') + .setDescription('Dauer in Sekunden') + .setMinValue(0))) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), + category: 'Admin', + async execute(interaction: any) { + const subcommand = interaction.options.getSubcommand(); + const DB = interaction.client.DB; + const guild = interaction.guild; + const guildId = interaction.guildId; + + // Get or create guild settings + let settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId); + if (!settings) { + DB.run('INSERT INTO guild_settings (guild_id) VALUES (?)', guildId); + settings = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId); + } + + if (subcommand === 'kick') { + const target = interaction.options.getMember('target') as GuildMember; + const reason = interaction.options.getString('reason') ?? 'Kein Grund'; + + if (!target) { + await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true }); + return; + } + + if (!target.kickable) { + await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken.', ephemeral: true }); + return; + } + + try { + await target.kick(reason); + await interaction.reply(`✅ Gekickt: ${target.user.tag}\nGrund: ${reason}`); + } catch (error) { + console.error(error); + await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true }); + } + return; + } + + if (subcommand === 'warn') { + const target = interaction.options.getMember('target') as GuildMember; + const reason = interaction.options.getString('reason')!; + + if (!target) { + await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true }); + return; + } + + DB.run( + 'INSERT INTO mod_logs (guild_id, user_id, moderator_id, action, reason) VALUES (?, ?, ?, ?, ?)', + guildId, + target.id, + interaction.user.id, + 'WARN', + reason + ); + + const countResult: any = DB.get( + 'SELECT COUNT(*) as count FROM mod_logs WHERE guild_id = ? AND user_id = ? AND action = ?', + guildId, + target.id, + 'WARN' + ); + + const warnCount = countResult?.count ?? 0; + const warnThreshold = settings?.warn_threshold ?? 3; + const warnAction = settings?.warn_action ?? 'ban'; + + let message = `✅ Verwarnt: ${target.user.tag}\nGrund: ${reason}\nDas ist die ${warnCount}. Verwarnung.`; + + // Check if threshold reached + if (warnCount >= warnThreshold) { + message += `\n\n⚠️ **Warn-Limit erreicht (${warnThreshold}/${warnThreshold})!**`; + + try { + if (warnAction === 'kick') { + await target.kick(`Auto-Kick: ${warnThreshold} Warnungen`); + message += `\n👢 **Automatisch gekickt!**`; + } else if (warnAction === 'mute') { + const muteDuration = settings?.warn_mute_duration ?? 1800; // 30 min default + await target.timeout(muteDuration * 1000, `Auto-Mute: ${warnThreshold} Warnungen`); + const durationMins = Math.floor(muteDuration / 60); + message += `\n🔇 **Automatisch gemuted (${durationMins} Min)!**`; + } else if (warnAction === 'ban') { + await target.ban({ reason: `Auto-Ban: ${warnThreshold} Warnungen` }); + message += `\n🔨 **Automatisch gebannt!**`; + } + } catch (error) { + console.error('[ADMIN] Auto-action failed:', error); + message += `\n❌ Auto-Aktion fehlgeschlagen.`; + } + } + + await interaction.reply({ content: message }); + return; + } + + if (subcommand === 'unwarn') { + const target = interaction.options.getMember('target') as GuildMember; + + if (!target) { + await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true }); + return; + } + + const latestWarn: any = DB.get( + 'SELECT * FROM mod_logs WHERE guild_id = ? AND user_id = ? AND action = ? ORDER BY id DESC LIMIT 1', + guildId, + target.id, + 'WARN' + ); + + if (!latestWarn) { + await interaction.reply({ content: 'Keine Warnung für diesen Nutzer gefunden.', ephemeral: true }); + return; + } + + DB.run('DELETE FROM mod_logs WHERE id = ?', latestWarn.id); + await interaction.reply(`✅ Letzte Warnung für ${target.user.tag} entfernt.`); + return; + } + + if (subcommand === 'mute') { + const target = interaction.options.getMember('target') as GuildMember; + const duration = interaction.options.getInteger('duration'); + const unit = interaction.options.getString('unit'); + const reason = interaction.options.getString('reason') ?? 'Kein Grund'; + + if (!target) { + await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true }); + return; + } + + // Parse duration + let durationMs: number | null = null; + let durationText = ''; + + if (unit === 'perm') { + durationMs = null; // Permanent = null + durationText = 'Permanent'; + } else if (duration) { + const unitSeconds: Record = { + sec: 1, + min: 60, + hour: 60 * 60, + day: 60 * 60 * 24, + week: 60 * 60 * 24 * 7, + month: 60 * 60 * 24 * 30, + year: 60 * 60 * 24 * 365 + }; + + const multiplier = unitSeconds[unit!]; + if (!multiplier) { + await interaction.reply({ content: '❌ Ungültige Zeiteinheit.', ephemeral: true }); + return; + } + + durationMs = duration * multiplier * 1000; + + // Human-readable duration + const unitNames: Record = { + sec: 'Sekunden', + min: 'Minuten', + hour: 'Stunden', + day: 'Tage', + week: 'Wochen', + month: 'Monate', + year: 'Jahre' + }; + durationText = `${duration} ${unitNames[unit!]}`; + } else { + await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', ephemeral: true }); + return; + } + + try { + await target.timeout(durationMs, reason); + await interaction.reply(`✅ Gemuted: ${target.user.tag}\nDauer: ${durationText}\nGrund: ${reason}`); + } catch (error) { + console.error(error); + await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true }); + } + return; + } + + if (subcommand === 'unmute') { + const target = interaction.options.getMember('target') as GuildMember; + + if (!target) { + await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true }); + return; + } + + try { + await target.timeout(null, 'Unmute'); + await interaction.reply(`✅ Entmutet: ${target.user.tag}`); + } catch (error) { + console.error(error); + await interaction.reply({ content: '❌ Fehler beim Entmuten.', ephemeral: true }); + } + return; + } + + if (subcommand === 'ban') { + const target = interaction.options.getMember('target') as GuildMember; + const reason = interaction.options.getString('reason') ?? 'Kein Grund'; + const deleteDays = interaction.options.getInteger('delete_days') ?? 0; + + if (!target) { + await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true }); + return; + } + + if (!target.bannable) { + await interaction.reply({ content: 'Ich kann diesen Nutzer nicht bannen.', ephemeral: true }); + return; + } + + try { + await target.ban({ deleteMessageSeconds: deleteDays * 86400, reason }); + await interaction.reply(`✅ Gebannt: ${target.user.tag}\nGrund: ${reason}`); + } catch (error) { + console.error(error); + await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true }); + } + return; + } + + if (subcommand === 'unban') { + let targetId = interaction.options.getString('target')!; + + if (targetId.startsWith('<@') && targetId.endsWith('>')) { + targetId = targetId.replace(/[<@!>]/g, ''); + } + + try { + await guild.bans.remove(targetId, 'Unban durch Admin'); + await interaction.reply(`✅ Entbannt: ${targetId}`); + } catch (error: any) { + console.error(error); + await interaction.reply({ content: `❌ Fehler beim Entbannen: ${error.message}`, ephemeral: true }); + } + return; + } + + if (subcommand === 'config') { + const setting = interaction.options.getString('setting')!; + const value = interaction.options.getString('value'); + const durationValue = interaction.options.getInteger('duration_value'); + + if (setting === 'show') { + const embed = new EmbedBuilder() + .setTitle('⚙️ Warn-System Konfiguration') + .setColor(0x3498db) + .addFields( + { name: 'Warn-Limit', value: `${settings?.warn_threshold ?? 3} Warnungen`, inline: true }, + { name: 'Auto-Aktion', value: settings?.warn_action ?? 'ban', inline: true }, + { name: 'Mute-Dauer', value: `${(settings?.warn_mute_duration ?? 1800) / 60} Min`, inline: true } + ); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + return; + } + + if (setting === 'warn_threshold') { + const threshold = parseInt(value!); + if (isNaN(threshold) || threshold < 1) { + await interaction.reply({ content: '❌ Ungültiger Wert. Mindestens 1.', ephemeral: true }); + return; + } + DB.run('UPDATE guild_settings SET warn_threshold = ? WHERE guild_id = ?', threshold, guildId); + await interaction.reply(`✅ Warn-Limit auf ${threshold} gesetzt.`); + return; + } + + if (setting === 'warn_action') { + const validActions = ['none', 'kick', 'mute', 'ban']; + if (!validActions.includes(value!)) { + await interaction.reply({ content: `❌ Ungültige Aktion. Möglich: ${validActions.join(', ')}`, ephemeral: true }); + return; + } + DB.run('UPDATE guild_settings SET warn_action = ? WHERE guild_id = ?', value, guildId); + await interaction.reply(`✅ Auto-Aktion auf ${value} gesetzt.`); + return; + } + + if (setting === 'warn_mute_duration') { + const duration = durationValue ?? 1800; + if (duration < 0) { + await interaction.reply({ content: '❌ Ungültige Dauer.', ephemeral: true }); + return; + } + DB.run('UPDATE guild_settings SET warn_mute_duration = ? WHERE guild_id = ?', duration, guildId); + await interaction.reply(`✅ Mute-Dauer auf ${duration} Sekunden (${Math.floor(duration/60)} Min) gesetzt.`); + return; + } + } + }, +}; + +export default command; \ No newline at end of file diff --git a/src/commands/utility/help.ts b/src/commands/utility/help.ts new file mode 100644 index 0000000..876d579 --- /dev/null +++ b/src/commands/utility/help.ts @@ -0,0 +1,59 @@ +import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; +import { Command } from '../../structures/Command.js'; + +const command: Command = { + data: new SlashCommandBuilder() + .setName('help') + .setDescription('Zeigt alle verfügbaren Befehle an.') + .setDMPermission(true), + category: 'Public', + async execute(interaction: any, client: any) { + if (!client.application?.owner) await client.application?.fetch(); + + const isOwner = interaction.user.id === client.application?.owner?.id; + const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator); + + const embed = new EmbedBuilder() + .setTitle('📖 Hilfe-Menü') + .setDescription('Hier sind die Befehle:') + .setColor(0x2ecc71) + .setTimestamp(); + + const publicCmds: string[] = []; + const adminCmds: string[] = []; + const ownerCmds: string[] = []; + + client.commands.forEach((cmd: Command) => { + const desc = 'description' in cmd.data ? cmd.data.description : 'Kontextmenü'; + const entry = `• **/${cmd.data.name}**: ${desc}`; + + if (cmd.category === 'Public') { + publicCmds.push(entry); + } else if (cmd.category === 'Admin') { + adminCmds.push(entry); + } else if (cmd.category === 'Owner') { + ownerCmds.push(entry); + } + }); + + if (publicCmds.length > 0) { + embed.addFields({ name: '🌍 Öffentliche Befehle', value: publicCmds.join('\n') }); + } + + if (isAdmin || isOwner) { + if (adminCmds.length > 0) { + embed.addFields({ name: '🛡️ Admin-Befehle', value: adminCmds.join('\n') }); + } + } + + if (isOwner) { + if (ownerCmds.length > 0) { + embed.addFields({ name: '🔑 Owner-Befehle', value: ownerCmds.join('\n') }); + } + } + + await interaction.reply({ embeds: [embed], ephemeral: true }); + }, +}; + +export default command; \ No newline at end of file diff --git a/src/commands/utility/owner.ts b/src/commands/utility/owner.ts new file mode 100644 index 0000000..bf1acd4 --- /dev/null +++ b/src/commands/utility/owner.ts @@ -0,0 +1,107 @@ +import { SlashCommandBuilder, EmbedBuilder } from 'discord.js'; +import { Command } from '../../structures/Command.js'; +import { Deployer } from '../../structures/Deployer.js'; + +const command: Command = { + data: new SlashCommandBuilder() + .setName('owner') + .setDescription('Bot-Owner Commands') + .addSubcommand(subcommand => + subcommand + .setName('deploy') + .setDescription('Aktualisiert Commands')) + .addSubcommand(subcommand => + subcommand + .setName('stats') + .setDescription('Zeigt Bot-Statistiken')) + .addSubcommand(subcommand => + subcommand + .setName('servers') + .setDescription('Listet alle Server')) + .setDMPermission(true), + category: 'Owner', + async execute(interaction: any, client: any) { + if (!client.application?.owner) await client.application?.fetch(); + + if (interaction.user.id !== client.application?.owner?.id) { + await interaction.reply({ content: 'Keine Berechtigung.', ephemeral: true }); + return; + } + + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'deploy') { + await interaction.deferReply({ ephemeral: true }); + + try { + const count = await Deployer.deploy(client.user!.id, client.token!); + await interaction.editReply(`✅ ${count} Befehle aktualisiert!`); + } catch (error) { + console.error(error); + await interaction.editReply('❌ Fehler beim Deployment.'); + } + return; + } + + if (subcommand === 'stats') { + await interaction.deferReply({ ephemeral: true }); + + const uptime = process.uptime(); + const days = Math.floor(uptime / 86400); + const hours = Math.floor(uptime / 3600) % 24; + const minutes = Math.floor(uptime / 60) % 60; + const uptimeStr = `${days}d ${hours}h ${minutes}m`; + + const totalServers = client.guilds.cache.size; + const totalUsers = client.guilds.cache.reduce((acc: number, guild: any) => acc + guild.memberCount, 0); + + const embed = new EmbedBuilder() + .setTitle('📊 Bot-Statistiken') + .setColor(0x3498db) + .addFields( + { name: '🤖 Bot', value: `**Uptime:** ${uptimeStr}\n**Latency:** ${client.ws.ping}ms`, inline: true }, + { name: '🌍 Netzwerk', value: `**Server:** ${totalServers}\n**Nutzer:** ${totalUsers}`, inline: true } + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + return; + } + + if (subcommand === 'servers') { + await interaction.deferReply({ ephemeral: true }); + + const guilds = client.guilds.cache.map((guild: any) => + `• **${guild.name}** \`(${guild.id})\` - ${guild.memberCount} Mitglieder` + ); + + const chunks = []; + for (let i = 0; i < guilds.length; i += 10) { + chunks.push(guilds.slice(i, i + 10).join('\n')); + } + + if (chunks.length === 0) { + await interaction.editReply('Keine Server.'); + return; + } + + const embed = new EmbedBuilder() + .setTitle(`🌍 Server (${client.guilds.cache.size})`) + .setColor(0xf1c40f) + .setDescription(chunks[0].substring(0, 4000)) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + for (let i = 1; i < chunks.length; i++) { + const nextEmbed = new EmbedBuilder() + .setColor(0xf1c40f) + .setDescription(chunks[i].substring(0, 4000)); + await interaction.followUp({ embeds: [nextEmbed], ephemeral: true }); + } + return; + } + }, +}; + +export default command; \ No newline at end of file diff --git a/src/commands/utility/ping.ts b/src/commands/utility/ping.ts new file mode 100644 index 0000000..945fbac --- /dev/null +++ b/src/commands/utility/ping.ts @@ -0,0 +1,14 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { Command } from '../../structures/Command.js'; + +const command: Command = { + data: new SlashCommandBuilder() + .setName('ping') + .setDescription('Testet die Latenz.'), + category: 'Public', + async execute(interaction: any) { + await interaction.reply(`🏓 Pong! Latenz: ${interaction.client.ws.ping}ms`); + }, +}; + +export default command; \ No newline at end of file diff --git a/src/commands/utility/twitch.ts b/src/commands/utility/twitch.ts new file mode 100644 index 0000000..d6446c0 --- /dev/null +++ b/src/commands/utility/twitch.ts @@ -0,0 +1,121 @@ +import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js'; +import { Command } from '../../structures/Command.js'; +import { TwitchManager } from '../../structures/TwitchManager.js'; + +const command: Command = { + data: new SlashCommandBuilder() + .setName('twitch') + .setDescription('Twitch-Befehle.') + .addSubcommand(subcommand => + subcommand + .setName('online') + .setDescription('Prüft, ob ein Kanal online ist.') + .addStringOption(option => + option.setName('channel') + .setDescription('Der Name des Twitch-Kanals') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('add') + .setDescription('Kanal überwachen (Admin).') + .addStringOption(option => + option.setName('channel') + .setDescription('Der Name des Twitch-Kanals') + .setRequired(true)) + .addStringOption(option => + option.setName('message') + .setDescription('Optionale Nachricht'))) + .addSubcommand(subcommand => + subcommand + .setName('remove') + .setDescription('Kanal aus Überwachung entfernen (Admin).') + .addStringOption(option => + option.setName('channel') + .setDescription('Der Name des Twitch-Kanals') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('list') + .setDescription('Listet überwachte Kanäle.')), + category: 'Public', + async execute(interaction: any) { + const subcommand = interaction.options.getSubcommand(); + const guildId = interaction.guildId; + const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator); + const DB = interaction.client.DB; + + if (subcommand === 'online') { + const channelName = interaction.options.getString('channel')!.toLowerCase(); + await interaction.deferReply(); + + const stream = await TwitchManager.fetchStreamData(channelName); + + if (!stream) { + await interaction.editReply(`🔴 Der Kanal **${channelName}** ist offline.`); + return; + } + + const thumbnailUrl = stream.thumbnail_url + .replace('{width}', '1280') + .replace('{height}', '720') + `?t=${Date.now()}`; + + const embed = new EmbedBuilder() + .setTitle(`🟢 ${stream.user_name} ist ONLINE!`) + .setURL(`https://twitch.tv/${stream.user_login}`) + .setColor(0x00FF00) + .setImage(thumbnailUrl) + .addFields( + { name: 'Titel', value: stream.title || 'Kein Titel', inline: false }, + { name: 'Kategorie', value: stream.game_name || 'Unbekannt', inline: true }, + { name: 'Zuschauer', value: stream.viewer_count.toString(), inline: true } + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + return; + } + + if (subcommand === 'list') { + const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? AND discord_channel_id = ?', guildId, interaction.channelId); + if (monitors.length === 0) { + await interaction.reply('Keine Twitch-Kanäle überwacht.'); + return; + } + + const list = monitors.map((m: any) => `• **${m.channel_name}**`).join('\n'); + const embed = new EmbedBuilder() + .setTitle('📺 Überwachte Twitch-Kanäle') + .setColor(0x6441a5) + .setDescription(list); + + await interaction.reply({ embeds: [embed] }); + return; + } + + // Admin only + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung.', ephemeral: true }); + return; + } + + if (subcommand === 'add') { + const channelName = interaction.options.getString('channel')!.toLowerCase(); + const message = interaction.options.getString('message'); + const discordChannelId = interaction.channelId; + + DB.run( + 'INSERT INTO twitch_monitors (guild_id, channel_name, discord_channel_id, custom_message) VALUES (?, ?, ?, ?) ON CONFLICT(guild_id, channel_name) DO UPDATE SET discord_channel_id = ?, custom_message = ?', + guildId, channelName, discordChannelId, message, discordChannelId, message + ); + await interaction.reply(`✅ Kanal **${channelName}** wird überwacht.`); + } + + if (subcommand === 'remove') { + const channelName = interaction.options.getString('channel')!.toLowerCase(); + DB.run('DELETE FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, channelName); + await interaction.reply(`✅ Kanal **${channelName}** entfernt.`); + } + }, +}; + +export default command; \ No newline at end of file diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts new file mode 100644 index 0000000..268f0e3 --- /dev/null +++ b/src/deploy-commands.ts @@ -0,0 +1,9 @@ +import 'dotenv/config'; +import { Deployer } from './structures/Deployer.js'; + +if (!process.env.CLIENT_ID || !process.env.DISCORD_TOKEN) { + console.error('Missing CLIENT_ID or DISCORD_TOKEN in .env'); + process.exit(1); +} + +Deployer.deploy(process.env.CLIENT_ID!, process.env.DISCORD_TOKEN!); \ No newline at end of file diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts new file mode 100644 index 0000000..29d7906 --- /dev/null +++ b/src/events/interactionCreate.ts @@ -0,0 +1,32 @@ +import { Events } from 'discord.js'; + +export default { + name: Events.InteractionCreate, + async execute(interaction: any, client: any) { + if (!interaction.isChatInputCommand()) return; + + const command = client.commands.get(interaction.commandName); + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`); + return; + } + + try { + await command.execute(interaction, client); + client.DB.run(` + INSERT INTO command_stats (command_name, uses) + VALUES (?, 1) + ON CONFLICT(command_name) DO UPDATE SET uses = uses + 1 + `, interaction.commandName); + } catch (error) { + console.error(error); + const replyOptions = { content: 'There was an error while executing this command!', ephemeral: true }; + if (interaction.replied || interaction.deferred) { + await interaction.followUp(replyOptions); + } else { + await interaction.reply(replyOptions); + } + } + }, +}; \ No newline at end of file diff --git a/src/events/ready.ts b/src/events/ready.ts new file mode 100644 index 0000000..42846a0 --- /dev/null +++ b/src/events/ready.ts @@ -0,0 +1,10 @@ +import { Events } from 'discord.js'; + +export default { + name: Events.ClientReady, + once: true, + execute(client: any) { + console.log(`[READY] Logged in as ${client.user.tag}`); + console.log(`[READY] Serving ${client.guilds.cache.size} servers`); + }, +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..91622de --- /dev/null +++ b/src/index.ts @@ -0,0 +1,62 @@ +import 'dotenv/config'; +import { ExtendedClient } from './structures/ExtendedClient.js'; +import { readdirSync } from 'node:fs'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { dirname, join } from 'node:path'; +import { Command } from './structures/Command.js'; +import { DB } from './structures/Database.js'; +import { TwitchManager } from './structures/TwitchManager.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Initialize Database +DB.init(); + +const client = new ExtendedClient(); +// Make DB available on client +(client as any).DB = DB; + +// Load Commands +const commandsPath = join(__dirname, 'commands'); +const commandFolders = readdirSync(commandsPath); + +for (const folder of commandFolders) { + const folderPath = join(commandsPath, folder); + const commandFiles = readdirSync(folderPath).filter(file => file.endsWith('.ts')); + + for (const file of commandFiles) { + const filePath = join(folderPath, file); + const fileUrl = pathToFileURL(filePath).href; + const command: Command = (await import(fileUrl)).default; + + if (command && 'data' in command && 'execute' in command) { + client.commands.set(command.data.name, command); + console.log(`[COMMAND] Loaded: ${command.data.name}`); + } else { + console.warn(`[COMMAND] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } +} + +// Load Events +const eventsPath = join(__dirname, 'events'); +const eventFiles = readdirSync(eventsPath).filter(file => file.endsWith('.ts')); + +for (const file of eventFiles) { + const filePath = join(eventsPath, file); + const fileUrl = pathToFileURL(filePath).href; + const event = (await import(fileUrl)).default; + + if (event.once) { + client.once(event.name, (...args) => event.execute(...args, client)); + } else { + client.on(event.name, (...args) => event.execute(...args, client)); + } + console.log(`[EVENT] Loaded: ${event.name}`); +} + +// Start Twitch Polling +TwitchManager.startPolling(client); + +client.login(process.env.DISCORD_TOKEN); \ No newline at end of file diff --git a/src/structures/Command.ts b/src/structures/Command.ts new file mode 100644 index 0000000..c7834f5 --- /dev/null +++ b/src/structures/Command.ts @@ -0,0 +1,6 @@ +export interface Command { + data: any; + category: 'Owner' | 'Admin' | 'Public'; + execute: (interaction: any, client: any) => Promise | void; + cooldown?: number; +} \ No newline at end of file diff --git a/src/structures/Database.ts b/src/structures/Database.ts new file mode 100644 index 0000000..7af7820 --- /dev/null +++ b/src/structures/Database.ts @@ -0,0 +1,103 @@ +import Database from 'better-sqlite3'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { existsSync, mkdirSync } from 'node:fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const dataDir = join(process.cwd(), 'data'); +if (!existsSync(dataDir)) { + mkdirSync(dataDir, { recursive: true }); +} + +const dbPath = join(dataDir, 'database.sqlite'); +const db = new Database(dbPath); + +export class DB { + static init() { + console.log(`[DATABASE] Initializing at ${dbPath}...`); + + // Guild Settings + db.prepare(` + CREATE TABLE IF NOT EXISTS guild_settings ( + guild_id TEXT PRIMARY KEY, + prefix TEXT DEFAULT '!', + mod_log_channel TEXT, + welcome_channel TEXT, + warn_threshold INTEGER DEFAULT 3, + warn_action TEXT DEFAULT 'ban', + warn_mute_duration INTEGER DEFAULT 1800 + ) + `).run(); + + // Mod Logs + db.prepare(` + CREATE TABLE IF NOT EXISTS mod_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT, + user_id TEXT, + moderator_id TEXT, + action TEXT, + reason TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `).run(); + + // Command Stats + db.prepare(` + CREATE TABLE IF NOT EXISTS command_stats ( + command_name TEXT PRIMARY KEY, + uses INTEGER DEFAULT 0 + ) + `).run(); + + // DM Users + db.prepare(` + CREATE TABLE IF NOT EXISTS dm_users ( + user_id TEXT PRIMARY KEY, + username TEXT, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `).run(); + + // Twitch Monitors + db.prepare(` + CREATE TABLE IF NOT EXISTS twitch_monitors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT, + channel_name TEXT, + discord_channel_id TEXT, + custom_message TEXT, + last_status TEXT DEFAULT 'offline', + last_stream_id TEXT, + UNIQUE(guild_id, channel_name) + ) + `).run(); + + // Auto Responses + db.prepare(` + CREATE TABLE IF NOT EXISTS auto_responses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT, + trigger_word TEXT, + response_text TEXT, + UNIQUE(guild_id, trigger_word) + ) + `).run(); + + console.log('[DATABASE] Initialization complete.'); + } + + static get(query: string, ...params: any[]) { + return db.prepare(query).get(...params); + } + + static all(query: string, ...params: any[]) { + return db.prepare(query).all(...params); + } + + static run(query: string, ...params: any[]) { + return db.prepare(query).run(...params); + } +} \ No newline at end of file diff --git a/src/structures/Deployer.ts b/src/structures/Deployer.ts new file mode 100644 index 0000000..e30faaf --- /dev/null +++ b/src/structures/Deployer.ts @@ -0,0 +1,59 @@ +import { REST, Routes } from 'discord.js'; +import { readdirSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export class Deployer { + static async deploy(clientId: string, token: string) { + const commands = []; + const rootDir = join(__dirname, '..'); + const commandsPath = join(rootDir, 'commands'); + const commandFolders = readdirSync(commandsPath); + + for (const folder of commandFolders) { + const folderPath = join(commandsPath, folder); + const commandFiles = readdirSync(folderPath).filter(file => file.endsWith('.ts')); + + for (const file of commandFiles) { + const filePath = join(folderPath, file); + const fileUrl = pathToFileURL(filePath).href; + const command = (await import(fileUrl)).default; + + if (command && 'data' in command) { + commands.push(command.data.toJSON()); + } + } + } + + const rest = new REST().setToken(token); + + console.log(`[DEPLOYER] Refreshing ${commands.length} application (/) commands.`); + + const data: any = await rest.put( + Routes.applicationCommands(clientId), + { body: commands }, + ); + + console.log(`[DEPLOYER] Successfully reloaded ${data.length} application (/) commands.`); + return data.length; + } + + static async deployIfMissing(clientId: string, token: string) { + const rest = new REST().setToken(token); + + try { + const data: any = await rest.get(Routes.applicationCommands(clientId)); + if (data.length > 0) { + console.log('[DEPLOYER] Commands already registered. Skipping deployment.'); + return data.length; + } + } catch (error) { + // Commands don't exist yet + } + + return this.deploy(clientId, token); + } +} \ No newline at end of file diff --git a/src/structures/ExtendedClient.ts b/src/structures/ExtendedClient.ts new file mode 100644 index 0000000..ed25be0 --- /dev/null +++ b/src/structures/ExtendedClient.ts @@ -0,0 +1,17 @@ +import { Client, Collection, GatewayIntentBits } from 'discord.js'; +import { Command } from './Command.js'; + +export class ExtendedClient extends Client { + public commands: Collection = new Collection(); + + constructor() { + super({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers, + ], + }); + } +} \ No newline at end of file diff --git a/src/structures/TwitchManager.ts b/src/structures/TwitchManager.ts new file mode 100644 index 0000000..f2953c8 --- /dev/null +++ b/src/structures/TwitchManager.ts @@ -0,0 +1,120 @@ +import { EmbedBuilder, TextChannel } from 'discord.js'; + +export class TwitchManager { + private static accessToken: string | null = null; + private static tokenExpires: number = 0; + + private static async getAccessToken() { + if (this.accessToken && Date.now() < this.tokenExpires) return this.accessToken; + + const clientId = process.env.TWITCH_CLIENT_ID; + const clientSecret = process.env.TWITCH_CLIENT_SECRET; + + if (!clientId || !clientSecret) { + console.error('[TWITCH] Missing Client ID or Secret in .env'); + return null; + } + + try { + const response = await fetch(`https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, { + method: 'POST' + }); + const data: any = await response.json(); + this.accessToken = data.access_token; + this.tokenExpires = Date.now() + (data.expires_in * 1000) - 60000; + return this.accessToken; + } catch (error) { + console.error('[TWITCH] Error getting access token:', error); + return null; + } + } + + static async fetchStreamData(channelName: string) { + const token = await this.getAccessToken(); + if (!token) return null; + + try { + const response = await fetch(`https://api.twitch.tv/helix/streams?user_login=${channelName}`, { + headers: { + 'Client-ID': process.env.TWITCH_CLIENT_ID!, + 'Authorization': `Bearer ${token}` + } + }); + const data: any = await response.json(); + return data.data?.[0] || null; + } catch (error) { + console.error(`[TWITCH] Error fetching stream data for ${channelName}:`, error); + return null; + } + } + + static async checkStreams(client: any) { + const token = await this.getAccessToken(); + if (!token) return; + + const monitors: any[] = (client as any).DB?.all('SELECT * FROM twitch_monitors') || []; + if (monitors.length === 0) return; + + for (const monitor of monitors) { + try { + const stream = await this.fetchStreamData(monitor.channel_name); + + if (stream) { + const isNewStream = monitor.last_stream_id !== stream.id; + const wasOffline = monitor.last_status === 'offline'; + + if (wasOffline || (monitor.last_status === 'online' && isNewStream)) { + await this.sendNotification(client, monitor, stream); + (client as any).DB?.run( + 'UPDATE twitch_monitors SET last_status = "online", last_stream_id = ? WHERE id = ?', + stream.id, + monitor.id + ); + } + } else { + if (monitor.last_status === 'online') { + (client as any).DB?.run('UPDATE twitch_monitors SET last_status = "offline", last_stream_id = NULL WHERE id = ?', monitor.id); + } + } + } catch (error) { + console.error(`[TWITCH] Error checking stream ${monitor.channel_name}:`, error); + } + } + } + + private static async sendNotification(client: any, monitor: any, stream: any) { + const guild = client.guilds.cache.get(monitor.guild_id); + if (!guild || !monitor.discord_channel_id) return; + + const channel = guild.channels.cache.get(monitor.discord_channel_id) as TextChannel; + if (!channel) return; + + const thumbnailUrl = stream.thumbnail_url + .replace('{width}', '1280') + .replace('{height}', '720') + `?t=${Date.now()}`; + + const embed = new EmbedBuilder() + .setTitle(`${stream.user_name} ist jetzt LIVE auf Twitch!`) + .setURL(`https://twitch.tv/${stream.user_login}`) + .setColor(0x6441a5) + .setImage(thumbnailUrl) + .addFields( + { name: 'Titel', value: stream.title || 'Kein Titel', inline: false }, + { name: 'Kategorie', value: stream.game_name || 'Unbekannt', inline: true }, + { name: 'Zuschauer', value: stream.viewer_count.toString(), inline: true } + ) + .setTimestamp(); + + let content = `📢 **${stream.user_name}** ist online!`; + if (monitor.custom_message) { + content += `\n\n${monitor.custom_message}`; + } + + await channel.send({ content, embeds: [embed] }); + } + + static startPolling(client: any) { + setInterval(() => this.checkStreams(client), 5 * 60 * 1000); + console.log('[TWITCH] Polling started (5m interval).'); + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..06ba7a2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file