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:
@@ -1,4 +1,7 @@
|
|||||||
DISCORD_TOKEN=dein_bot_token
|
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
|
||||||
|
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
|
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
|
||||||
|
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
|
## 🚀 Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ async function validateRoles(client: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initTwitchMonitor(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');
|
const monitors: any[] = client.DB.all('SELECT * FROM twitch_monitor_channels');
|
||||||
|
|
||||||
if (monitors.length === 0) {
|
if (monitors.length === 0) {
|
||||||
@@ -46,6 +51,7 @@ async function initTwitchMonitor(client: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const monitor = TwitchMonitor.getInstance();
|
const monitor = TwitchMonitor.getInstance();
|
||||||
|
monitor.setDiscordClient(client);
|
||||||
|
|
||||||
for (const m of monitors) {
|
for (const m of monitors) {
|
||||||
const webhookExists = m.webhook_url && m.webhook_url.length > 0;
|
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';
|
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 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 {
|
interface MonitorChannel {
|
||||||
guildId: string;
|
guildId: string;
|
||||||
@@ -17,6 +20,9 @@ export class TwitchMonitor {
|
|||||||
private cache: TwitchCache;
|
private cache: TwitchCache;
|
||||||
private monitors: Map<string, MonitorChannel> = new Map();
|
private monitors: Map<string, MonitorChannel> = new Map();
|
||||||
private webhookClients: Map<string, WebhookClient> = new Map();
|
private webhookClients: Map<string, WebhookClient> = new Map();
|
||||||
|
private tokenFailureCount = 0;
|
||||||
|
private tokenCheckInterval: NodeJS.Timeout | null = null;
|
||||||
|
private isTokenInvalid = false;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.cache = new TwitchCache(1000);
|
this.cache = new TwitchCache(1000);
|
||||||
@@ -29,6 +35,62 @@ export class TwitchMonitor {
|
|||||||
return TwitchMonitor.instance;
|
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> {
|
async connect(): Promise<void> {
|
||||||
if (this.client) return;
|
if (this.client) return;
|
||||||
|
|
||||||
@@ -36,7 +98,11 @@ export class TwitchMonitor {
|
|||||||
|
|
||||||
this.client = new Client({
|
this.client = new Client({
|
||||||
options: {
|
options: {
|
||||||
skipUpdatingEmotesets: true,
|
debug: false,
|
||||||
|
},
|
||||||
|
identity: {
|
||||||
|
username: process.env.TWITCH_USERNAME,
|
||||||
|
password: this.getOAuthToken(),
|
||||||
},
|
},
|
||||||
channels: channels,
|
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) => {
|
this.client.on('cheer', (channel: string, userstate: ChatUserstate, message: string) => {
|
||||||
const monitor = this.findMonitor(channel);
|
const monitor = this.findMonitor(channel);
|
||||||
if (!monitor) return;
|
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 {
|
try {
|
||||||
await this.client.connect();
|
await this.client.connect();
|
||||||
console.log('[TWITCH] Monitor connected to IRC');
|
console.log('[TWITCH] Monitor connected to IRC');
|
||||||
|
this.tokenFailureCount = 0;
|
||||||
|
this.startTokenValidation();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TWITCH] Failed to connect:', 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 {
|
private findMonitor(channel: string): MonitorChannel | undefined {
|
||||||
const normalizedChannel = channel.startsWith('#') ? channel : `#${channel}`;
|
const normalizedChannel = channel.startsWith('#') ? channel : `#${channel}`;
|
||||||
return Array.from(this.monitors.values()).find(
|
return Array.from(this.monitors.values()).find(
|
||||||
@@ -198,11 +294,14 @@ export class TwitchMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnect(): void {
|
disconnect(): void {
|
||||||
|
this.stopTokenValidation();
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
this.client.disconnect();
|
this.client.disconnect();
|
||||||
this.client = null;
|
this.client = null;
|
||||||
}
|
}
|
||||||
this.monitors.clear();
|
this.monitors.clear();
|
||||||
this.webhookClients.clear();
|
this.webhookClients.clear();
|
||||||
|
this.tokenFailureCount = 0;
|
||||||
|
this.isTokenInvalid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user