18 KiB
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:
- Extended Client Pattern: Extend
discord.jsClientclass to hold global state. - Dynamic Discovery:
index.tsusesreaddirSyncand dynamicimport()to register commands/events. - Interface-Driven Commands: All commands implement
Commandinterface. - SQLite Database: Persistent data storage with
better-sqlite3. - Twitch Monitoring: Periodic API polling with Stream ID tracking.
- TwitchMonitor IRC: TMI.js-based IRC chat monitoring for mod events.
- Reminder System: Periodic reminder checking with target_time tracking.
- Auto-Response System: Trigger word detection for automatic replies.
- Welcome/Goodbye System: Guild member add/remove events with customizable messages.
- Logging System: Configurable event logging (messages, roles, moderation, etc.).
- Role Selection System: Self-service role assignment via select menus.
- 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
│ │ ├── twitchmonitor.ts # Twitch IRC chat monitoring
│ │ ├── rolesetup.ts # Self-service role selection
│ │ ├── 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
│ ├── TwitchMonitor.ts
│ ├── TwitchCache.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
{
"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"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
2. Database Structure
-- 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 (Stream notifications)
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)
);
-- twitch_monitor_channels (IRC chat logging)
CREATE TABLE twitch_monitor_channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
twitch_channel TEXT NOT NULL,
discord_channel_id TEXT NOT NULL,
webhook_url TEXT,
UNIQUE(guild_id, twitch_channel)
);
-- 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
);
-- role_categories
CREATE TABLE role_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
channel_id TEXT NOT NULL,
message_id TEXT NOT NULL,
category_name TEXT NOT NULL,
max_roles INTEGER DEFAULT 0,
exclusive BOOLEAN DEFAULT 0,
remove_on_delete BOOLEAN DEFAULT 0,
UNIQUE(guild_id, category_name)
);
-- role_options
CREATE TABLE role_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
label TEXT NOT NULL,
emoji TEXT,
role_id TEXT NOT NULL,
UNIQUE(category_id, role_id),
FOREIGN KEY (category_id) REFERENCES role_categories(id) ON DELETE CASCADE
);
3. Key Components
ExtendedClient.ts:
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:
export interface Command {
data: any;
category: 'Owner' | 'Admin' | 'Public';
execute: (interaction: any, client: any) => Promise<void> | void;
cooldown?: number;
}
4. Timer System
Time Parsing:
// 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:
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:
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
6. Logging System
Events that can be logged:
messages- Deleted and edited messagesroles- Role changesnicks- Nickname changeschannels- Channel create/deletewarns- User warningsmutes- User muteskicks- User kicksbans- User bans
Log moderation action:
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):
// 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):
// 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
// 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. TwitchMonitor IRC Chat Logging
Overview: Uses TMI.js to connect to Twitch IRC as a "lurker" (read-only) and logs moderation events to Discord via webhooks.
Architecture:
// Singleton pattern for IRC connection
class TwitchMonitor {
private static instance: TwitchMonitor | null = null;
private client: tmi.Client;
private cache: TwitchCache; // In-memory FIFO ring buffer (default: 1000 messages)
private monitors: Map<string, MonitorChannel>;
}
TwitchCache (In-Memory Ring Buffer):
interface ChatMessage {
channel: string; // twitch channel
user: string;
text: string;
timestamp: number;
msgId: string;
}
class TwitchCache {
private buffer: ChatMessage[] = [];
private maxSize: number = 1000;
push(msg: ChatMessage): void { /* FIFO - shift oldest when full */ }
getLastN(user: string, channel: string, n: number): ChatMessage[] { /* for ban context */ }
getByMsgId(msgId: string): ChatMessage | undefined { /* for delete restore */ }
}
Events Logged:
| Event | Discord Embed |
|---|---|
ban |
🔨 Ban - mit letzten 5 Nachrichten als Kontext |
timeout |
⏱️ Timeout |
messagedeleted |
🗑️ Delete - |
cheer |
💰 Bits |
subscription |
🅿️ Sub |
Mod-Icon: https://static-cdn.jtvnw.net/jtv_user_pictures/a736384e-8a1d-40ed-8a7a-808a23a15476-profile_image-300x300.png
Commands:
/twitchmonitor add <twitch_channel> <discord_channel> // Admin
/twitchmonitor remove <twitch_channel> // Admin
/twitchmonitor list // Public
/twitchmonitor help // Public
10. Configurable Warn System
Auto-Action Logic:
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
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 messagespurge time <duration> <min|hour>- Delete messages from last X timepurge user <user> <1-100>- Delete X messages from specific userpurge 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
ModerateMemberspermission
// 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.
12. Role Selection System
Overview: Self-service role assignment via Discord Select Menus. Users can choose roles from a dropdown message without needing admin intervention.
Admin Commands:
/rolesetup create <#channel> <category_name> // Create new role selection message
/rolesetup add <kategorie> <@rolle> [emoji] // Add role option
/rolesetup edit <kategorie> <@rolle> [new_emoji] // Change emoji
/rolesetup remove <kategorie> <@rolle> // Remove role option
/rolesetup delete <kategorie> // Delete entire category
/rolesetup config <kategorie> max:1 exclusive:true // Configure settings
/rolesetup list // Show all configurations
Config Options:
| Option | Default | Description |
|---|---|---|
max_roles |
0 (unlimited) | Maximum roles a user can select from this category |
exclusive |
false | If true, selecting a role removes other roles in this category |
remove_on_delete |
false | Remove role from users when option is deleted |
User Interaction:
- User clicks on Select Menu dropdown
- Role is added/removed based on config
- Ephemeral confirmation message appears (auto-hides after 60s)
Database Schema:
role_categories: Stores category settings (channel, message_id, config)role_options: Stores individual role options with labels and emojis- CASCADE DELETE removes options when category is deleted
Role Validation:
- On bot startup, validates that all stored role IDs still exist in Discord
- Missing roles are removed from database (and optionally from users)
🚀 Build & Deploy
# Build
npm run build
# Deploy Commands
npm run deploy
# Run
npm start
# Docker
docker-compose up -d --build
⚠️ Critical Constraints
- Environment Variables: Always use
process.env, never hardcode. - Type Safety: Use
anyfor commanddataandexecuteparameters (compatibility). - Interaction States: Check
interaction.repliedbefore replying. - Twitch API: Max 100 channels per request, batch properly.
- Deploy: Commands NOT auto-registered on start – use
/owner deploy. - Ephemeral Replies: Use
flags: [MessageFlags.Ephemeral]instead of deprecatedephemeral: true. - Line Endings: Use
.gitattributesto ensure LF line endings for consistency.
📋 Command Summary
| Command | Subcommands | Permission |
|---|---|---|
/ping |
– | Public |
/help |
– | Public |
/twitch |
online, add, remove, list, listall, help | Public/Admin |
/twitchmonitor |
add, remove, list, help | Admin |
/rolesetup |
create, add, edit, remove, delete, config, list | 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 |