Add Twitch identity auth and token validation to TwitchMonitor
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 8s

This commit is contained in:
2026-04-23 11:37:58 +00:00
parent c2c009501f
commit 21c3b01e64
4 changed files with 120 additions and 2 deletions

View File

@@ -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
TWITCH_CLIENT_SECRET=dein_twitch_secret
TWITCH_USERNAME=dein_twitch_username
TWITCH_OAUTH_TOKEN=oauth:xxxxxxxxxxxxxxxxxx
BOT_OWNER_ID=deine_discord_user_id

View File

@@ -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

View File

@@ -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;

View File

@@ -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<string, MonitorChannel> = new Map();
private webhookClients: Map<string, WebhookClient> = 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<void> {
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<void> {
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;
}
}