Add Twitch identity auth and token validation to TwitchMonitor
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 8s
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 8s
This commit is contained in:
@@ -2,3 +2,6 @@ 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
|
||||
10
README.md
10
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user