Files
pixelpoebel/AGENTS-BOT.md
coding ce578ab377
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 36s
Add Kick streaming monitoring module
2026-03-31 09:28:28 +00:00

15 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. Kick Monitoring: Periodic API polling with @nekiro/kick-api library.
  7. Reminder System: Periodic reminder checking with target_time tracking.
  8. Auto-Response System: Trigger word detection for automatic replies.
  9. Welcome/Goodbye System: Guild member add/remove events with customizable messages.
  10. Logging System: Configurable event logging (messages, roles, moderation, etc.).
  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
│   │       ├── kickstream.ts   # Kick streaming
│   │       ├── 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
│       ├── KickManager.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)
);

-- kick_monitors
CREATE TABLE kick_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:

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. Kick Monitoring with @nekiro/kick-api

// Uses @nekiro/kick-api library for OAuth 2.1 authentication
import { client as KickClient } from '@nekiro/kick-api';

const kickClient = new KickClient({
    clientId: process.env.KICK_CLIENT_ID,
    clientSecret: process.env.KICK_CLIENT_SECRET,
});

// Fetch channel and livestream data
const channels = await kickClient.channels.getChannels({ slug: [channelName] });
const livestreams = await kickClient.livestreams.getLivestreams({ 
    broadcaster_user_id: [channel.user_id] 
});

// Stream ID tracking prevents duplicate notifications
const streamId = livestream.session_id?.toString();
const isNewStream = monitor.last_stream_id !== streamId;
const wasOffline = monitor.last_status === 'offline';

if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
    // Send notification to Discord channel
    DB.run('UPDATE kick_monitors SET last_status = "online", last_stream_id = ? WHERE id = ?',
        streamId, monitor.id);
}

Required Environment Variables:

  • KICK_CLIENT_ID - Kick Developer App Client ID
  • KICK_CLIENT_SECRET - Kick Developer App Client Secret

Kick Developer App Setup: https://docs.kick.com/getting-started/kick-apps-setup

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

🚀 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
/kickstream online, add, remove, list, 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