diff --git a/.env.example b/.env.example index 424abf5..f7ab4c5 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,4 @@ DISCORD_TOKEN=dein_bot_token CLIENT_ID=deine_client_id TWITCH_CLIENT_ID=deine_twitch_id -TWITCH_CLIENT_SECRET=dein_twitch_secret -KICK_CLIENT_ID=deine_kick_id -KICK_CLIENT_SECRET=dein_kick_secret \ No newline at end of file +TWITCH_CLIENT_SECRET=dein_twitch_secret \ No newline at end of file diff --git a/AGENTS-BOT.md b/AGENTS-BOT.md index c198dc3..696cc65 100644 --- a/AGENTS-BOT.md +++ b/AGENTS-BOT.md @@ -12,12 +12,11 @@ The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScrip 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. +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 @@ -31,7 +30,6 @@ pixelpoebel/ │ │ ├── ping.ts │ │ ├── help.ts │ │ ├── twitch.ts -│ │ ├── kick.ts # Kick streaming │ │ ├── trigger.ts # Auto-responses │ │ ├── timer.ts # Reminders │ │ ├── welcome.ts # Welcome/Goodbye messages @@ -54,7 +52,6 @@ pixelpoebel/ │ ├── Command.ts │ ├── Database.ts │ ├── TwitchManager.ts -│ ├── KickManager.ts │ ├── ReminderManager.ts │ └── Deployer.ts ├── data/ # SQLite database (volume mounted) @@ -149,18 +146,6 @@ CREATE TABLE twitch_monitors ( 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, @@ -344,42 +329,7 @@ if (wasOffline || (monitor.last_status === 'online' && isNewStream)) { } ``` -### 9. Kick Monitoring with @nekiro/kick-api - -```typescript -// 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 +### 9. Configurable Warn System **Database Schema:** - `warn_threshold`: Number of warns before action @@ -475,7 +425,6 @@ docker-compose up -d --build | `/ping` | – | Public | | `/help` | – | Public | | `/twitch` | online, add, remove, list, listall, help | Public/Admin | -| `/kick` | 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 | diff --git a/README.md b/README.md index 51798bb..e00be73 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ DISCORD_TOKEN=dein_bot_token CLIENT_ID=deine_client_id TWITCH_CLIENT_ID=deine_twitch_id TWITCH_CLIENT_SECRET=dein_twitch_secret -KICK_CLIENT_ID=deine_kick_id -KICK_CLIENT_SECRET=dein_kick_secret ``` ## 🚀 Start @@ -61,9 +59,6 @@ npm run deploy - `/twitch online ` – Twitch-Status - `/twitch list` – Überwachte Kanäle - `/twitch help` – Twitch-Hilfe -- `/kick online ` – Kick-Status -- `/kick list` – Überwachte Kanäle -- `/kick help` – Kick-Hilfe - `/timer list` – Timer dieses Kanals - `/timer help` – Timer-Hilfe - `/trigger list` – Alle Trigger anzeigen @@ -90,13 +85,6 @@ npm run deploy - `/twitch listall` – Alle überwachten Streamer (Mod) - `/twitch help` – Twitch-Hilfe -### Kick -- `/kick online ` – Prüft ob Kanal online ist -- `/kick add ` – Streamer überwachen (Mod) -- `/kick remove ` – Streamer entfernen (Mod) -- `/kick list` – Überwachte Kanäle -- `/kick help` – Kick-Hilfe - ### Admin - `/admin kick ` – Nutzer kicken - `/admin warn ` – Nutzer verwarnen @@ -124,7 +112,6 @@ npm run deploy - ✅ TypeScript - ✅ SQLite Database - ✅ Twitch Monitoring -- ✅ Kick Monitoring - ✅ Moderation Tools - ✅ Konfigurierbares Warn-System - ✅ Auto-Antworten (Trigger) diff --git a/package-lock.json b/package-lock.json index 4cdfd07..ea4896e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@nekiro/kick-api": "^2.1.2", "better-sqlite3": "^12.8.0", "discord.js": "^14.18.0", "dotenv": "^16.4.7", @@ -193,14 +192,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@nekiro/kick-api": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@nekiro/kick-api/-/kick-api-2.1.2.tgz", - "integrity": "sha512-D2Yq+j3U7TBK4WNc48lM+fHYh3s+9b6+EGV66wVzh6QPTr0kcQ0ThgrXkyMbppkqv+nZwFYrIw+PxmECQ3BnfQ==", - "engines": { - "node": ">=20.0" - } - }, "node_modules/@sapphire/async-queue": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", diff --git a/package.json b/package.json index 341fd87..bccca81 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "author": "sarah", "license": "MIT", "dependencies": { - "@nekiro/kick-api": "^2.1.2", "better-sqlite3": "^12.8.0", "discord.js": "^14.18.0", "dotenv": "^16.4.7", diff --git a/src/commands/utility/kick.ts b/src/commands/utility/kick.ts deleted file mode 100644 index 940beb6..0000000 --- a/src/commands/utility/kick.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags, ChannelType } from 'discord.js'; -import { Command } from '../../structures/Command.js'; -import { KickManager } from '../../structures/KickManager.js'; - -const command: Command = { - data: new SlashCommandBuilder() - .setName('kick') - .setDescription('Kick-Streamer Benachrichtigungen verwalten.') - .addSubcommand(subcommand => - subcommand - .setName('add') - .setDescription('Fügt einen Kick-Streamer zum Monitoring hinzu.') - .addStringOption(option => - option.setName('channel') - .setDescription('Der Kick-Channel-Name (Slug)') - .setRequired(true)) - .addChannelOption(option => - option.setName('discord_channel') - .setDescription('Der Discord-Kanal für Benachrichtigungen') - .addChannelTypes(ChannelType.GuildText) - .setRequired(true)) - .addStringOption(option => - option.setName('nachricht') - .setDescription('Optionale custom Nachricht'))) - .addSubcommand(subcommand => - subcommand - .setName('remove') - .setDescription('Entfernt einen Kick-Streamer vom Monitoring.') - .addStringOption(option => - option.setName('channel') - .setDescription('Der Kick-Channel-Name') - .setRequired(true))) - .addSubcommand(subcommand => - subcommand - .setName('list') - .setDescription('Listet alle überwachten Kick-Streamer auf.')) - .addSubcommand(subcommand => - subcommand - .setName('online') - .setDescription('Prüft ob ein Kick-Streamer online ist.') - .addStringOption(option => - option.setName('channel') - .setDescription('Der Kick-Channel-Name') - .setRequired(true))) - .addSubcommand(subcommand => - subcommand - .setName('help') - .setDescription('Zeigt Hilfe zu den Kick-Befehlen an.')), - category: 'Admin', - async execute(interaction: any) { - const subcommand = interaction.options.getSubcommand(); - const guildId = interaction.guildId; - const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers); - const DB = interaction.client.DB; - - if (subcommand === 'help') { - const embed = new EmbedBuilder() - .setTitle('🟢 Kick Streaming Hilfe') - .setColor(0x53fc18) - .setDescription('Benachrichtigungen bei Kick-Streams.') - .addFields( - { name: '`/kick add [nachricht]`', value: 'Fügt einen Streamer hinzu (Mod).', inline: false }, - { name: '`/kick remove `', value: 'Entfernt einen Streamer (Mod).', inline: false }, - { name: '`/kick list`', value: 'Zeigt alle überwachten Streamer.', inline: false }, - { name: '`/kick online `', value: 'Prüft ob ein Streamer live ist.', inline: false }, - { name: 'Hinweis', value: 'Benötigt KICK_CLIENT_ID und KICK_CLIENT_SECRET in der .env', inline: false } - ) - .setTimestamp(); - - await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); - return; - } - - if (subcommand === 'online') { - const channelName = interaction.options.getString('channel')!.toLowerCase().replace(/^@/, ''); - await interaction.deferReply(); - - const { channel, livestream } = await KickManager.fetchChannelData(channelName); - if (!channel) { - await interaction.editReply({ content: `❌ Channel **${channelName}** nicht gefunden.` }); - return; - } - - const isLive = livestream !== null; - const embed = new EmbedBuilder() - .setTitle(isLive ? `🟢 ${channel.user.username} ist LIVE` : `⚫ ${channel.user.username} ist offline`) - .setURL(`https://kick.com/${channel.slug}`) - .setColor(isLive ? 0x53fc18 : 0x5865F2); - - if (isLive && livestream) { - embed.addFields( - { name: 'Titel', value: livestream.stream_title || 'Kein Titel', inline: false }, - { name: 'Kategorie', value: livestream.category?.name || 'Unbekannt', inline: true }, - { name: 'Zuschauer', value: livestream.viewer_count.toString(), inline: true } - ); - if (livestream.thumbnail) { - embed.setImage(livestream.thumbnail); - } - } - - await interaction.editReply({ embeds: [embed] }); - return; - } - - if (subcommand === 'list') { - const monitors: any[] = DB.all('SELECT * FROM kick_monitors WHERE guild_id = ? ORDER BY channel_name ASC', guildId); - - if (monitors.length === 0) { - await interaction.reply({ content: 'Keine Kick-Streamer überwacht.', flags: [MessageFlags.Ephemeral] }); - return; - } - - const embed = new EmbedBuilder() - .setTitle('🟢 Kick-Monitoring') - .setColor(0x53fc18) - .setDescription(monitors.map((m: any) => - `• **${m.channel_name}** → <#${m.discord_channel_id}> ${m.last_status === 'online' ? '🔴' : '⚫'}` - ).join('\n')) - .setTimestamp(); - - await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); - return; - } - - if (!isAdmin) { - await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); - return; - } - - if (subcommand === 'add') { - const channelName = interaction.options.getString('channel')!.toLowerCase().replace(/^@/, ''); - const discordChannel = interaction.options.getChannel('discord_channel'); - const customMessage = interaction.options.getString('nachricht'); - - const { channel } = await KickManager.fetchChannelData(channelName); - if (!channel) { - await interaction.reply({ content: `❌ Kick-Channel **${channelName}** nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); - return; - } - - DB.run( - 'INSERT INTO kick_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, - discordChannel.id, - customMessage, - discordChannel.id, - customMessage - ); - - await interaction.reply({ content: `✅ **${channelName}** wird jetzt überwacht.`, flags: [MessageFlags.Ephemeral] }); - return; - } - - if (subcommand === 'remove') { - const channelName = interaction.options.getString('channel')!.toLowerCase().replace(/^@/, ''); - - const result = DB.run('DELETE FROM kick_monitors WHERE guild_id = ? AND channel_name = ?', guildId, channelName); - - if (result.changes === 0) { - await interaction.reply({ content: `❌ **${channelName}** nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); - return; - } - - await interaction.reply({ content: `✅ **${channelName}** entfernt.`, flags: [MessageFlags.Ephemeral] }); - } - }, -}; - -export default command; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index bf85fdb..cde96ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ import { dirname, join } from 'node:path'; import { Command } from './structures/Command.js'; import { DB } from './structures/Database.js'; import { TwitchManager } from './structures/TwitchManager.js'; -import { KickManager } from './structures/KickManager.js'; import { ReminderManager } from './structures/ReminderManager.js'; const __filename = fileURLToPath(import.meta.url); @@ -61,9 +60,6 @@ for (const file of eventFiles) { // Start Twitch Polling TwitchManager.startPolling(client); -// Start Kick Polling -KickManager.startPolling(client); - // Start Reminder Polling ReminderManager.startPolling(client); diff --git a/src/structures/Database.ts b/src/structures/Database.ts index ae39859..62c0e3e 100644 --- a/src/structures/Database.ts +++ b/src/structures/Database.ts @@ -79,20 +79,6 @@ export class DB { ) `).run(); - // Kick Monitors - db.prepare(` - CREATE TABLE IF NOT EXISTS 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) - ) - `).run(); - // Auto Responses db.prepare(` CREATE TABLE IF NOT EXISTS auto_responses ( diff --git a/src/structures/KickManager.ts b/src/structures/KickManager.ts deleted file mode 100644 index 7188f36..0000000 --- a/src/structures/KickManager.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { EmbedBuilder, TextChannel } from 'discord.js'; -import { client as KickClient } from '@nekiro/kick-api'; - -interface KickChannel { - id: number; - user_id: number; - slug: string; - user: { username: string }; - category?: { id: number; name: string }; - followers_count: number; -} - -interface KickLivestream { - broadcaster_user_id: number; - category: { id: number; name: string }; - channel_id: number; - slug: string; - started_at: string; - stream_title: string; - thumbnail: string; - viewer_count: number; - session_id?: string; -} - -export class KickManager { - private static kickClient: InstanceType | null = null; - private static channelLivestreamMap: Map = new Map(); - - private static initClient() { - if (this.kickClient) return this.kickClient; - - const clientId = process.env.KICK_CLIENT_ID; - const clientSecret = process.env.KICK_CLIENT_SECRET; - - if (!clientId || !clientSecret) { - console.error('[KICK] Missing KICK_CLIENT_ID or KICK_CLIENT_SECRET in .env'); - return null; - } - - this.kickClient = new KickClient({ - clientId, - clientSecret, - }); - - return this.kickClient; - } - - static async fetchChannelData(channelName: string): Promise<{ channel: KickChannel | null; livestream: KickLivestream | null }> { - const client = this.initClient(); - if (!client) return { channel: null, livestream: null }; - - try { - const channels = await client.channels.getChannels({ slug: [channelName] }) as KickChannel[]; - const channel = channels?.[0] || null; - - if (!channel) return { channel: null, livestream: null }; - - const livestreams = await client.livestreams.getLivestreams({ - broadcaster_user_id: [channel.user_id] - }) as KickLivestream[]; - const livestream = livestreams?.[0] || null; - - return { channel, livestream }; - } catch (error) { - console.error(`[KICK] Error fetching data for ${channelName}:`, error); - return { channel: null, livestream: null }; - } - } - - static async checkStreams(client: any) { - if (!this.initClient()) return; - - const monitors: any[] = (client as any).DB?.all('SELECT * FROM kick_monitors') || []; - if (monitors.length === 0) return; - - for (const monitor of monitors) { - try { - const { channel, livestream } = await this.fetchChannelData(monitor.channel_name); - - if (channel && livestream) { - const streamId = livestream.session_id?.toString() || livestream.broadcaster_user_id.toString(); - const isNewStream = monitor.last_stream_id !== streamId; - const wasOffline = monitor.last_status === 'offline'; - - if (wasOffline || (monitor.last_status === 'online' && isNewStream)) { - await this.sendNotification(client, monitor, channel, livestream); - (client as any).DB?.run( - "UPDATE kick_monitors SET last_status = 'online', last_stream_id = ? WHERE id = ?", - streamId, - monitor.id - ); - } - } else { - if (monitor.last_status === 'online') { - (client as any).DB?.run("UPDATE kick_monitors SET last_status = 'offline', last_stream_id = NULL WHERE id = ?", monitor.id); - } - } - } catch (error) { - console.error(`[KICK] Error checking stream ${monitor.channel_name}:`, error); - } - } - } - - private static async sendNotification(client: any, monitor: any, channel: KickChannel, livestream: KickLivestream) { - const guild = client.guilds.cache.get(monitor.guild_id); - if (!guild || !monitor.discord_channel_id) return; - - const discordChannel = guild.channels.cache.get(monitor.discord_channel_id) as TextChannel; - if (!discordChannel) return; - - const thumbnailUrl = livestream.thumbnail || `https://kick.com/img/channel-thumbnails/${channel.id}?t=${Date.now()}`; - - const embed = new EmbedBuilder() - .setTitle(`${channel.user.username} ist jetzt LIVE auf Kick!`) - .setURL(`https://kick.com/${channel.slug}`) - .setColor(0x53fc18) - .setImage(thumbnailUrl) - .addFields( - { name: 'Titel', value: livestream.stream_title || 'Kein Titel', inline: false }, - { name: 'Kategorie', value: livestream.category?.name || 'Unbekannt', inline: true }, - { name: 'Zuschauer', value: livestream.viewer_count.toString(), inline: true } - ) - .setTimestamp(); - - let content = `📢 **${channel.user.username}** ist jetzt auf Kick online!`; - if (monitor.custom_message) { - content += `\n\n${monitor.custom_message}`; - } - - await discordChannel.send({ content, embeds: [embed] }); - } - - static startPolling(client: any) { - if (!process.env.KICK_CLIENT_ID || !process.env.KICK_CLIENT_SECRET) { - console.log('[KICK] Skipping polling - missing credentials.'); - return; - } - - setInterval(() => this.checkStreams(client), 5 * 60 * 1000); - console.log('[KICK] Polling started (5m interval).'); - } -} \ No newline at end of file