Files
pixelpoebel/AGENTS-BOT.md
coding ad988c9333
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 7s
Add role selection system with select menus
2026-04-02 10:55:43 +00:00

16 KiB
Raw Blame History

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. Role Selection System: Self-service role assignment via select menus.
  11. 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
│   │       ├── 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
│       ├── 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
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
);

-- 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 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:

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. 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:

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 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
// 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

  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
/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