Add Kick streaming monitoring module
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 36s
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 36s
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
DISCORD_TOKEN=dein_bot_token
|
||||
CLIENT_ID=deine_client_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
|
||||
@@ -12,9 +12,10 @@ 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. **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.
|
||||
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.
|
||||
|
||||
@@ -30,6 +31,7 @@ pixelpoebel/
|
||||
│ │ ├── ping.ts
|
||||
│ │ ├── help.ts
|
||||
│ │ ├── twitch.ts
|
||||
│ │ ├── kickstream.ts # Kick streaming
|
||||
│ │ ├── trigger.ts # Auto-responses
|
||||
│ │ ├── timer.ts # Reminders
|
||||
│ │ ├── welcome.ts # Welcome/Goodbye messages
|
||||
@@ -52,6 +54,7 @@ pixelpoebel/
|
||||
│ ├── Command.ts
|
||||
│ ├── Database.ts
|
||||
│ ├── TwitchManager.ts
|
||||
│ ├── KickManager.ts
|
||||
│ ├── ReminderManager.ts
|
||||
│ └── Deployer.ts
|
||||
├── data/ # SQLite database (volume mounted)
|
||||
@@ -146,6 +149,18 @@ 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,
|
||||
@@ -329,7 +344,42 @@ if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Configurable Warn System
|
||||
### 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
|
||||
|
||||
**Database Schema:**
|
||||
- `warn_threshold`: Number of warns before action
|
||||
@@ -425,6 +475,7 @@ docker-compose up -d --build
|
||||
| `/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 |
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -9,16 +9,17 @@
|
||||
"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"
|
||||
"dotenv": "^16.4.7",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^22.13.9",
|
||||
"nodemon": "^3.1.9",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -192,6 +193,14 @@
|
||||
"@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",
|
||||
@@ -1274,7 +1283,6 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
10
package.json
10
package.json
@@ -10,10 +10,16 @@
|
||||
"dev": "nodemon --watch 'src/**/*.ts' --exec 'node --loader ts-node/esm' src/index.ts",
|
||||
"deploy": "node --loader ts-node/esm src/deploy-commands.ts"
|
||||
},
|
||||
"keywords": ["discord", "bot", "typescript", "modular"],
|
||||
"keywords": [
|
||||
"discord",
|
||||
"bot",
|
||||
"typescript",
|
||||
"modular"
|
||||
],
|
||||
"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",
|
||||
@@ -25,4 +31,4 @@
|
||||
"nodemon": "^3.1.9",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
170
src/commands/utility/kickstream.ts
Normal file
170
src/commands/utility/kickstream.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
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('kickstream')
|
||||
.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: '`/kickstream add <channel> <discord_channel> [nachricht]`', value: 'Fügt einen Streamer hinzu (Mod).', inline: false },
|
||||
{ name: '`/kickstream remove <channel>`', value: 'Entfernt einen Streamer (Mod).', inline: false },
|
||||
{ name: '`/kickstream list`', value: 'Zeigt alle überwachten Streamer.', inline: false },
|
||||
{ name: '`/kickstream 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;
|
||||
@@ -6,6 +6,7 @@ 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);
|
||||
@@ -60,6 +61,9 @@ for (const file of eventFiles) {
|
||||
// Start Twitch Polling
|
||||
TwitchManager.startPolling(client);
|
||||
|
||||
// Start Kick Polling
|
||||
KickManager.startPolling(client);
|
||||
|
||||
// Start Reminder Polling
|
||||
ReminderManager.startPolling(client);
|
||||
|
||||
|
||||
@@ -79,6 +79,20 @@ 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 (
|
||||
|
||||
142
src/structures/KickManager.ts
Normal file
142
src/structures/KickManager.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
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).');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user