All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 13s
433 lines
13 KiB
Markdown
433 lines
13 KiB
Markdown
# 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. **Reminder System:** Periodic reminder checking with target_time tracking.
|
||
7. **Auto-Response System:** Trigger word detection for automatic replies.
|
||
8. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages.
|
||
9. **Logging System:** Configurable event logging (messages, roles, moderation, etc.).
|
||
10. **Grouped Commands:** Admin/Owner/Trigger/Timer 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
|
||
│ │ ├── trigger.ts # Auto-responses
|
||
│ │ ├── timer.ts # Reminders
|
||
│ │ ├── welcome.ts # Welcome/Goodbye messages
|
||
│ │ ├── log.ts # Event logging
|
||
│ │ ├── admin.ts # All admin subcommands
|
||
│ │ └── owner.ts # All owner subcommands
|
||
│ ├── events/
|
||
│ │ ├── ready.ts
|
||
│ │ ├── interactionCreate.ts
|
||
│ │ ├── messageCreate.ts # Auto-response triggers
|
||
│ │ ├── messageDelete.ts # Deleted messages
|
||
│ │ ├── messageUpdate.ts # Edited messages
|
||
│ │ ├── guildMemberAdd.ts # Welcome messages
|
||
│ │ ├── guildMemberRemove.ts # Goodbye messages
|
||
│ │ ├── guildMemberUpdate.ts # Nickname/role changes
|
||
│ │ ├── channelCreate.ts # Channel create
|
||
│ │ └── channelDelete.ts # Channel delete
|
||
│ └── structures/
|
||
│ ├── ExtendedClient.ts
|
||
│ ├── Command.ts
|
||
│ ├── Database.ts
|
||
│ ├── TwitchManager.ts
|
||
│ ├── ReminderManager.ts
|
||
│ └── Deployer.ts
|
||
├── data/ # SQLite database (volume mounted)
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── Dockerfile
|
||
├── docker-compose.yml
|
||
├── .env.example
|
||
└── .gitattributes
|
||
```
|
||
|
||
## 📝 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,
|
||
welcome_message TEXT,
|
||
goodbye_message TEXT,
|
||
log_channel TEXT,
|
||
log_events TEXT DEFAULT 'messages,roles,nicks,channels,warns,mutes,kicks,bans',
|
||
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)
|
||
);
|
||
|
||
-- reminders
|
||
CREATE TABLE reminders (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
guild_id TEXT,
|
||
channel_id TEXT,
|
||
user_id TEXT,
|
||
reminder_text TEXT,
|
||
target_time DATETIME,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
```
|
||
|
||
### 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<string, Command> = 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> | void;
|
||
cooldown?: number;
|
||
}
|
||
```
|
||
|
||
### 4. Timer System
|
||
|
||
**Time Parsing:**
|
||
```typescript
|
||
// Duration parsing: "30s", "2h", "1d30m", "3wochen"
|
||
function parseDuration(input: string): number | null {
|
||
const unitMs: Record<string, number> = {
|
||
's': 1000, 'sec': 1000, 'sek': 1000, 'sekunden': 1000,
|
||
'm': 60000, 'min': 60000, 'minuten': 60000,
|
||
'h': 3600000, 'hour': 3600000, 'std': 3600000, 'stunden': 3600000,
|
||
'd': 86400000, 'day': 86400000, 't': 86400000, 'tage': 86400000,
|
||
'w': 604800000, 'week': 604800000, 'wochen': 604800000,
|
||
'mo': 2592000000, 'month': 2592000000, 'monat': 2592000000, 'monate': 2592000000,
|
||
'y': 31536000000, 'year': 31536000000, 'j': 31536000000, 'jahr': 31536000000, 'jahre': 31536000000
|
||
};
|
||
// Parse and calculate total milliseconds
|
||
}
|
||
|
||
// Date parsing: "25.12.2024 14:30", "morgen 15:00"
|
||
function parseDateTime(input: string): Date | null {
|
||
// German date format, ISO format, or relative "tomorrow"
|
||
}
|
||
```
|
||
|
||
**Reminder Polling:**
|
||
```typescript
|
||
export class ReminderManager {
|
||
static async checkReminders(client: any) {
|
||
const now = new Date().toISOString();
|
||
const dueReminders = DB.all('SELECT * FROM reminders WHERE target_time <= ?', now);
|
||
|
||
for (const reminder of dueReminders) {
|
||
const guild = client.guilds.cache.get(reminder.guild_id);
|
||
const channel = guild.channels.cache.get(reminder.channel_id);
|
||
// Send notification and delete reminder
|
||
}
|
||
}
|
||
|
||
static startPolling(client: any) {
|
||
setInterval(() => this.checkReminders(client), 30 * 1000);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5. Welcome/Goodbye System
|
||
|
||
**Variables in messages:** `{user}`, `{username}`, `{server}`, `{membercount}`
|
||
|
||
**Auto-use Discord system channel if no custom channel set:**
|
||
```typescript
|
||
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
||
```
|
||
|
||
### 6. Logging System
|
||
|
||
**Events that can be logged:**
|
||
- `messages` - Deleted and edited messages
|
||
- `roles` - Role changes
|
||
- `nicks` - Nickname changes
|
||
- `channels` - Channel create/delete
|
||
- `warns` - User warnings
|
||
- `mutes` - User mutes
|
||
- `kicks` - User kicks
|
||
- `bans` - User bans
|
||
|
||
**Log moderation action:**
|
||
```typescript
|
||
async function sendModerationLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||
const DB = client.DB;
|
||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||
|
||
if (!settings?.log_events) return;
|
||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||
if (!activeEvents.includes(event)) return;
|
||
|
||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||
if (!logSettings?.log_channel) return;
|
||
|
||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||
if (!channel || !channel.isTextBased()) return;
|
||
|
||
await (channel as any).send({ embeds: [embed] });
|
||
}
|
||
```
|
||
|
||
### 7. Auto-Response (Trigger) System
|
||
|
||
**Event Handler (`messageCreate.ts`):**
|
||
```typescript
|
||
// Listens for messages and checks against auto_responses
|
||
// Ignores bots and DM messages
|
||
const triggers = DB.all('SELECT * FROM auto_responses WHERE guild_id = ?', guildId);
|
||
const contentLower = message.content.toLowerCase();
|
||
|
||
for (const trigger of triggers) {
|
||
if (contentLower.includes(trigger.trigger_word.toLowerCase())) {
|
||
await message.channel.send(trigger.response_text);
|
||
return;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Command (`trigger.ts`):**
|
||
```typescript
|
||
// Add trigger (admin only)
|
||
DB.run('INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?)',
|
||
guildId, triggerWord.toLowerCase(), responseText);
|
||
|
||
// List triggers (public)
|
||
DB.all('SELECT * FROM auto_responses WHERE guild_id = ? ORDER BY trigger_word ASC', guildId);
|
||
|
||
// Remove trigger (admin only)
|
||
DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord);
|
||
```
|
||
|
||
### 8. 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);
|
||
}
|
||
```
|
||
|
||
### 9. 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(...);
|
||
}
|
||
```
|
||
|
||
### 10. Mute with Multiple Time Units
|
||
|
||
```typescript
|
||
const unitSeconds: Record<string, number> = {
|
||
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;
|
||
}
|
||
```
|
||
|
||
### 11. Purge Command (Message Deletion)
|
||
|
||
**Subcommands:**
|
||
- `purge amount <1-100>` - Delete X messages
|
||
- `purge time <duration> <min|hour>` - Delete messages from last X time
|
||
- `purge user <user> <1-100>` - Delete X messages from specific user
|
||
- `purge all` - Delete all possible messages (max 100)
|
||
|
||
**Discord API Constraints:**
|
||
- Max 100 messages per bulk delete
|
||
- Messages older than 14 days cannot be bulk deleted
|
||
- Requires `ModerateMembers` permission
|
||
|
||
```typescript
|
||
// Filter messages older than 14 days (Discord API limit)
|
||
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
||
const filtered = messages.filter((m: any) => m.createdTimestamp > twoWeeksAgo);
|
||
|
||
await channel.bulkDelete(filtered, true);
|
||
```
|
||
|
||
**Logging:** Purge actions are logged to the moderation log channel with event type `messages`.
|
||
|
||
## 🚀 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`.
|
||
6. **Ephemeral Replies:** Use `flags: [MessageFlags.Ephemeral]` instead of deprecated `ephemeral: true`.
|
||
7. **Line Endings:** Use `.gitattributes` to ensure LF line endings for consistency.
|
||
|
||
## 📋 Command Summary
|
||
|
||
| Command | Subcommands | Permission |
|
||
|---------|-------------|------------|
|
||
| `/ping` | – | Public |
|
||
| `/help` | – | Public |
|
||
| `/twitch` | online, add, remove, list, listall, help | Public/Admin |
|
||
| `/trigger` | add, remove, list, help | Public/Admin |
|
||
| `/timer` | add, remove, list, listall, help | Public/Admin |
|
||
| `/welcome` | setchannel, add, remove, list, help | Admin |
|
||
| `/log` | setchannel, enable, disable, list, help | Admin |
|
||
| `/admin` | kick, warn, unwarn, mute, unmute, ban, unban, purge, config, help | Admin |
|
||
| `/owner` | deploy, stats, servers, help | Owner | |