diff --git a/.env.example b/.env.example index f7ab4c5..a514dfa 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ DISCORD_TOKEN=dein_bot_token CLIENT_ID=deine_client_id TWITCH_CLIENT_ID=deine_twitch_id -TWITCH_CLIENT_SECRET=dein_twitch_secret \ No newline at end of file +TWITCH_CLIENT_SECRET=dein_twitch_secret +TWITCH_USERNAME=dein_twitch_username +TWITCH_OAUTH_TOKEN=oauth:xxxxxxxxxxxxxxxxxx +BOT_OWNER_ID=deine_discord_user_id \ No newline at end of file diff --git a/README.md b/README.md index d017da5..afcabdd 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,18 @@ DISCORD_TOKEN=dein_bot_token CLIENT_ID=deine_client_id TWITCH_CLIENT_ID=deine_twitch_id TWITCH_CLIENT_SECRET=dein_twitch_secret +TWITCH_USERNAME=dein_twitch_username +TWITCH_OAUTH_TOKEN=oauth:xxxxxxxxxxxxxxxxxx +BOT_OWNER_ID=deine_discord_user_id ``` +### TwitchMonitor Setup +1. **Twitch OAuth Token holen:** https://chatterino.com/client_login +2. **Username:** Dein Twitch Username +3. **Bot Owner ID:** Deine Discord User ID (für Fehlerbenachrichtigungen) + +> ⚠️ **Hinweis:** Der Twitch OAuth Token kann ungültig werden. In diesem Fall wirst du automatisch per Discord DM benachrichtigt. + ## 🚀 Start ```bash diff --git a/src/events/ready.ts b/src/events/ready.ts index 1d33ea9..17c943b 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -38,6 +38,11 @@ async function validateRoles(client: any) { } async function initTwitchMonitor(client: any) { + if (!process.env.TWITCH_OAUTH_TOKEN || !process.env.TWITCH_USERNAME) { + console.warn('[TWITCH] Missing TWITCH_OAUTH_TOKEN or TWITCH_USERNAME - moderation events will not be received'); + return; + } + const monitors: any[] = client.DB.all('SELECT * FROM twitch_monitor_channels'); if (monitors.length === 0) { @@ -46,6 +51,7 @@ async function initTwitchMonitor(client: any) { } const monitor = TwitchMonitor.getInstance(); + monitor.setDiscordClient(client); for (const m of monitors) { const webhookExists = m.webhook_url && m.webhook_url.length > 0; diff --git a/src/structures/TwitchMonitor.ts b/src/structures/TwitchMonitor.ts index e9b1396..3207acb 100644 --- a/src/structures/TwitchMonitor.ts +++ b/src/structures/TwitchMonitor.ts @@ -3,6 +3,9 @@ import { EmbedBuilder, WebhookClient } from 'discord.js'; import { TwitchCache, ChatMessage } from './TwitchCache.js'; const MOD_ICON = 'https://static-cdn.jtvnw.net/jtv_user_pictures/a736384e-8a1d-40ed-8a7a-808a23a15476-profile_image-300x300.png'; +const TOKEN_CHECK_INTERVAL = 5 * 60 * 1000; +const TOKEN_CHECK_MAX_FAILURES = 3; +const TOKEN_CHECK_RETRY_DELAY = 30 * 1000; interface MonitorChannel { guildId: string; @@ -17,6 +20,9 @@ export class TwitchMonitor { private cache: TwitchCache; private monitors: Map = new Map(); private webhookClients: Map = new Map(); + private tokenFailureCount = 0; + private tokenCheckInterval: NodeJS.Timeout | null = null; + private isTokenInvalid = false; private constructor() { this.cache = new TwitchCache(1000); @@ -29,6 +35,62 @@ export class TwitchMonitor { return TwitchMonitor.instance; } + private getOAuthToken(): string { + const token = process.env.TWITCH_OAUTH_TOKEN || ''; + return token.startsWith('oauth:') ? token : 'oauth:' + token; + } + + private async notifyOwner(message: string): Promise { + const ownerId = process.env.BOT_OWNER_ID; + if (!ownerId) return; + + try { + const { Client: DiscordClient } = await import('discord.js'); + const client = TwitchMonitor.getInstance(); + const discordClient = (client as any).discordClient; + if (!discordClient) return; + + const owner = await discordClient.users.fetch(ownerId).catch(() => null); + if (owner) { + await owner.send(`⚠️ **TwitchMonitor Warning**\n${message}`); + } + } catch (error) { + console.error('[TWITCH] Failed to notify owner:', error); + } + } + + private startTokenValidation(): void { + this.tokenCheckInterval = setInterval(async () => { + if (this.isTokenInvalid) return; + + const isConnected = this.client?.readyState() === 'OPEN'; + + if (!isConnected) { + this.tokenFailureCount++; + console.warn(`[TWITCH] Connection check failed (${this.tokenFailureCount}/${TOKEN_CHECK_MAX_FAILURES})`); + + if (this.tokenFailureCount >= TOKEN_CHECK_MAX_FAILURES) { + this.isTokenInvalid = true; + console.error('[TWITCH] Token validation failed - stopping monitoring'); + this.notifyOwner('TwitchMonitor Token ungültig. Bitte neuen Token holen: https://chatterino.com/client_login'); + this.stopTokenValidation(); + this.disconnect(); + } else { + await new Promise(resolve => setTimeout(resolve, TOKEN_CHECK_RETRY_DELAY)); + } + } else { + this.tokenFailureCount = 0; + } + }, TOKEN_CHECK_INTERVAL); + } + + private stopTokenValidation(): void { + if (this.tokenCheckInterval) { + clearInterval(this.tokenCheckInterval); + this.tokenCheckInterval = null; + } + } + async connect(): Promise { if (this.client) return; @@ -36,7 +98,11 @@ export class TwitchMonitor { this.client = new Client({ options: { - skipUpdatingEmotesets: true, + debug: false, + }, + identity: { + username: process.env.TWITCH_USERNAME, + password: this.getOAuthToken(), }, channels: channels, }); @@ -92,6 +158,17 @@ export class TwitchMonitor { }); }); + this.client.on('clearchat', (channel: string) => { + const monitor = this.findMonitor(channel); + if (!monitor) return; + + this.sendWebhook(monitor.webhookUrl, { + title: '🧹 Chat geleert', + description: `Der Chat in **${channel}** wurde geleert.`, + color: 0xffa500, + }); + }); + this.client.on('cheer', (channel: string, userstate: ChatUserstate, message: string) => { const monitor = this.findMonitor(channel); if (!monitor) return; @@ -127,14 +204,33 @@ export class TwitchMonitor { }); }); + this.client.on('disconnected', async () => { + console.warn('[TWITCH] Disconnected from IRC'); + if (!this.isTokenInvalid) { + this.tokenFailureCount++; + } + }); + + this.client.on('reconnect', () => { + console.log('[TWITCH] Attempting to reconnect...'); + }); + try { await this.client.connect(); console.log('[TWITCH] Monitor connected to IRC'); + this.tokenFailureCount = 0; + this.startTokenValidation(); } catch (error) { console.error('[TWITCH] Failed to connect:', error); + this.isTokenInvalid = true; + this.notifyOwner('TwitchMonitor: Verbindung fehlgeschlagen. Token prüfen.'); } } + setDiscordClient(discordClient: any): void { + (this as any).discordClient = discordClient; + } + private findMonitor(channel: string): MonitorChannel | undefined { const normalizedChannel = channel.startsWith('#') ? channel : `#${channel}`; return Array.from(this.monitors.values()).find( @@ -198,11 +294,14 @@ export class TwitchMonitor { } disconnect(): void { + this.stopTokenValidation(); if (this.client) { this.client.disconnect(); this.client = null; } this.monitors.clear(); this.webhookClients.clear(); + this.tokenFailureCount = 0; + this.isTokenInvalid = false; } } \ No newline at end of file