Remove Kick monitoring module
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 13s

This commit is contained in:
2026-04-02 07:55:42 +00:00
parent ed2919224b
commit f90f0cb7ab
9 changed files with 7 additions and 413 deletions

View File

@@ -2,5 +2,3 @@ DISCORD_TOKEN=dein_bot_token
CLIENT_ID=deine_client_id CLIENT_ID=deine_client_id
TWITCH_CLIENT_ID=deine_twitch_id TWITCH_CLIENT_ID=deine_twitch_id
TWITCH_CLIENT_SECRET=dein_twitch_secret TWITCH_CLIENT_SECRET=dein_twitch_secret
KICK_CLIENT_ID=deine_kick_id
KICK_CLIENT_SECRET=dein_kick_secret

View File

@@ -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. 3. **Interface-Driven Commands:** All commands implement `Command` interface.
4. **SQLite Database:** Persistent data storage with `better-sqlite3`. 4. **SQLite Database:** Persistent data storage with `better-sqlite3`.
5. **Twitch Monitoring:** Periodic API polling with Stream ID tracking. 5. **Twitch Monitoring:** Periodic API polling with Stream ID tracking.
6. **Kick Monitoring:** Periodic API polling with `@nekiro/kick-api` library. 6. **Reminder System:** Periodic reminder checking with target_time tracking.
7. **Reminder System:** Periodic reminder checking with target_time tracking. 7. **Auto-Response System:** Trigger word detection for automatic replies.
8. **Auto-Response System:** Trigger word detection for automatic replies. 8. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages.
9. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages. 9. **Logging System:** Configurable event logging (messages, roles, moderation, etc.).
10. **Logging System:** Configurable event logging (messages, roles, moderation, etc.). 10. **Grouped Commands:** Admin/Owner/Trigger/Timer commands grouped under subcommands.
11. **Grouped Commands:** Admin/Owner/Trigger/Timer commands grouped under subcommands.
## 📁 Project Structure ## 📁 Project Structure
@@ -31,7 +30,6 @@ pixelpoebel/
│ │ ├── ping.ts │ │ ├── ping.ts
│ │ ├── help.ts │ │ ├── help.ts
│ │ ├── twitch.ts │ │ ├── twitch.ts
│ │ ├── kick.ts # Kick streaming
│ │ ├── trigger.ts # Auto-responses │ │ ├── trigger.ts # Auto-responses
│ │ ├── timer.ts # Reminders │ │ ├── timer.ts # Reminders
│ │ ├── welcome.ts # Welcome/Goodbye messages │ │ ├── welcome.ts # Welcome/Goodbye messages
@@ -54,7 +52,6 @@ pixelpoebel/
│ ├── Command.ts │ ├── Command.ts
│ ├── Database.ts │ ├── Database.ts
│ ├── TwitchManager.ts │ ├── TwitchManager.ts
│ ├── KickManager.ts
│ ├── ReminderManager.ts │ ├── ReminderManager.ts
│ └── Deployer.ts │ └── Deployer.ts
├── data/ # SQLite database (volume mounted) ├── data/ # SQLite database (volume mounted)
@@ -149,18 +146,6 @@ CREATE TABLE twitch_monitors (
UNIQUE(guild_id, channel_name) 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 -- auto_responses
CREATE TABLE auto_responses ( CREATE TABLE auto_responses (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -344,42 +329,7 @@ if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
} }
``` ```
### 9. Kick Monitoring with @nekiro/kick-api ### 9. Configurable Warn System
```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
**Database Schema:** **Database Schema:**
- `warn_threshold`: Number of warns before action - `warn_threshold`: Number of warns before action
@@ -475,7 +425,6 @@ docker-compose up -d --build
| `/ping` | | Public | | `/ping` | | Public |
| `/help` | | Public | | `/help` | | Public |
| `/twitch` | online, add, remove, list, listall, help | Public/Admin | | `/twitch` | online, add, remove, list, listall, help | Public/Admin |
| `/kick` | online, add, remove, list, help | Public/Admin |
| `/trigger` | add, remove, list, help | Public/Admin | | `/trigger` | add, remove, list, help | Public/Admin |
| `/timer` | add, remove, list, listall, help | Public/Admin | | `/timer` | add, remove, list, listall, help | Public/Admin |
| `/welcome` | setchannel, add, remove, list, help | Admin | | `/welcome` | setchannel, add, remove, list, help | Admin |

View File

@@ -16,8 +16,6 @@ DISCORD_TOKEN=dein_bot_token
CLIENT_ID=deine_client_id CLIENT_ID=deine_client_id
TWITCH_CLIENT_ID=deine_twitch_id TWITCH_CLIENT_ID=deine_twitch_id
TWITCH_CLIENT_SECRET=dein_twitch_secret TWITCH_CLIENT_SECRET=dein_twitch_secret
KICK_CLIENT_ID=deine_kick_id
KICK_CLIENT_SECRET=dein_kick_secret
``` ```
## 🚀 Start ## 🚀 Start
@@ -61,9 +59,6 @@ npm run deploy
- `/twitch online <channel>` Twitch-Status - `/twitch online <channel>` Twitch-Status
- `/twitch list` Überwachte Kanäle - `/twitch list` Überwachte Kanäle
- `/twitch help` Twitch-Hilfe - `/twitch help` Twitch-Hilfe
- `/kick online <channel>` Kick-Status
- `/kick list` Überwachte Kanäle
- `/kick help` Kick-Hilfe
- `/timer list` Timer dieses Kanals - `/timer list` Timer dieses Kanals
- `/timer help` Timer-Hilfe - `/timer help` Timer-Hilfe
- `/trigger list` Alle Trigger anzeigen - `/trigger list` Alle Trigger anzeigen
@@ -90,13 +85,6 @@ npm run deploy
- `/twitch listall` Alle überwachten Streamer (Mod) - `/twitch listall` Alle überwachten Streamer (Mod)
- `/twitch help` Twitch-Hilfe - `/twitch help` Twitch-Hilfe
### Kick
- `/kick online <kanal>` Prüft ob Kanal online ist
- `/kick add <channel> <discord_channel>` Streamer überwachen (Mod)
- `/kick remove <channel>` Streamer entfernen (Mod)
- `/kick list` Überwachte Kanäle
- `/kick help` Kick-Hilfe
### Admin ### Admin
- `/admin kick <user>` Nutzer kicken - `/admin kick <user>` Nutzer kicken
- `/admin warn <user> <reason>` Nutzer verwarnen - `/admin warn <user> <reason>` Nutzer verwarnen
@@ -124,7 +112,6 @@ npm run deploy
- ✅ TypeScript - ✅ TypeScript
- ✅ SQLite Database - ✅ SQLite Database
- ✅ Twitch Monitoring - ✅ Twitch Monitoring
- ✅ Kick Monitoring
- ✅ Moderation Tools - ✅ Moderation Tools
- ✅ Konfigurierbares Warn-System - ✅ Konfigurierbares Warn-System
- ✅ Auto-Antworten (Trigger) - ✅ Auto-Antworten (Trigger)

9
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nekiro/kick-api": "^2.1.2",
"better-sqlite3": "^12.8.0", "better-sqlite3": "^12.8.0",
"discord.js": "^14.18.0", "discord.js": "^14.18.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
@@ -193,14 +192,6 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@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": { "node_modules/@sapphire/async-queue": {
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",

View File

@@ -19,7 +19,6 @@
"author": "sarah", "author": "sarah",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nekiro/kick-api": "^2.1.2",
"better-sqlite3": "^12.8.0", "better-sqlite3": "^12.8.0",
"discord.js": "^14.18.0", "discord.js": "^14.18.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",

View File

@@ -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 <channel> <discord_channel> [nachricht]`', value: 'Fügt einen Streamer hinzu (Mod).', inline: false },
{ name: '`/kick remove <channel>`', value: 'Entfernt einen Streamer (Mod).', inline: false },
{ name: '`/kick list`', value: 'Zeigt alle überwachten Streamer.', inline: false },
{ name: '`/kick online <channel>`', 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;

View File

@@ -6,7 +6,6 @@ import { dirname, join } from 'node:path';
import { Command } from './structures/Command.js'; import { Command } from './structures/Command.js';
import { DB } from './structures/Database.js'; import { DB } from './structures/Database.js';
import { TwitchManager } from './structures/TwitchManager.js'; import { TwitchManager } from './structures/TwitchManager.js';
import { KickManager } from './structures/KickManager.js';
import { ReminderManager } from './structures/ReminderManager.js'; import { ReminderManager } from './structures/ReminderManager.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -61,9 +60,6 @@ for (const file of eventFiles) {
// Start Twitch Polling // Start Twitch Polling
TwitchManager.startPolling(client); TwitchManager.startPolling(client);
// Start Kick Polling
KickManager.startPolling(client);
// Start Reminder Polling // Start Reminder Polling
ReminderManager.startPolling(client); ReminderManager.startPolling(client);

View File

@@ -79,20 +79,6 @@ export class DB {
) )
`).run(); `).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 // Auto Responses
db.prepare(` db.prepare(`
CREATE TABLE IF NOT EXISTS auto_responses ( CREATE TABLE IF NOT EXISTS auto_responses (

View File

@@ -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<typeof KickClient> | null = null;
private static channelLivestreamMap: Map<string, KickLivestream> = 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).');
}
}