Optimize Twitch polling with batching and implement database caching with WAL mode
This commit is contained in:
@@ -1,17 +1,13 @@
|
||||
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
const settings = client.DB.getSettings(guildId);
|
||||
if (!settings?.log_events || !settings?.log_channel) return;
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
@@ -24,6 +20,8 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed
|
||||
export default {
|
||||
name: Events.ChannelCreate,
|
||||
async execute(channel: GuildChannel) {
|
||||
if (!channel.guild) return;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📁 Channel erstellt')
|
||||
.setColor(0x27ae60)
|
||||
@@ -33,6 +31,6 @@ export default {
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(channel.client, channel.guild!.id, 'channels', embed);
|
||||
await sendLog(channel.client, channel.guild.id, 'channels', embed);
|
||||
},
|
||||
};
|
||||
@@ -1,17 +1,13 @@
|
||||
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
const settings = client.DB.getSettings(guildId);
|
||||
if (!settings?.log_events || !settings?.log_channel) return;
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
@@ -24,6 +20,8 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed
|
||||
export default {
|
||||
name: Events.ChannelDelete,
|
||||
async execute(channel: GuildChannel) {
|
||||
if (!channel.guild) return;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📁 Channel gelöscht')
|
||||
.setColor(0xe74c3c)
|
||||
@@ -33,6 +31,6 @@ export default {
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(channel.client, channel.guild!.id, 'channels', embed);
|
||||
await sendLog(channel.client, channel.guild.id, 'channels', embed);
|
||||
},
|
||||
};
|
||||
@@ -6,7 +6,7 @@ export default {
|
||||
const DB = (member.client as any).DB;
|
||||
const guildId = member.guild.id;
|
||||
|
||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
const settings = DB.getSettings(guildId);
|
||||
|
||||
// Use custom channel or fall back to Discord's system channel
|
||||
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
||||
|
||||
@@ -2,16 +2,15 @@ import { Events, GuildMember, TextChannel, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
const settings = DB.getSettings(guildId);
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
if (!settings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
@@ -40,7 +39,7 @@ export default {
|
||||
await sendLog(member.client, guildId, 'leaves', embed);
|
||||
|
||||
// Send goodbye message if configured
|
||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
const settings = DB.getSettings(guildId);
|
||||
|
||||
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
||||
if (!channelId || !settings?.goodbye_message) {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { Events, GuildMember, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
const settings = client.DB.getSettings(guildId);
|
||||
if (!settings?.log_events || !settings?.log_channel) return;
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
@@ -21,7 +17,6 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed
|
||||
}
|
||||
}
|
||||
|
||||
// Guild Member Update (nickname + roles)
|
||||
export default {
|
||||
name: Events.GuildMemberUpdate,
|
||||
async execute(oldMember: GuildMember, newMember: GuildMember) {
|
||||
|
||||
@@ -6,10 +6,9 @@ export default {
|
||||
if (message.author.bot) return;
|
||||
if (!message.guildId) return;
|
||||
|
||||
const triggers: any[] = client.DB.all(
|
||||
'SELECT * FROM auto_responses WHERE guild_id = ?',
|
||||
message.guildId
|
||||
);
|
||||
const triggers = client.DB.getTriggers(message.guildId);
|
||||
|
||||
if (triggers.length === 0) return;
|
||||
|
||||
const contentLower = message.content.toLowerCase();
|
||||
|
||||
|
||||
@@ -1,51 +1,22 @@
|
||||
import { Events, Message, TextChannel, GuildChannel, AuditLogEvent, EmbedBuilder } from 'discord.js';
|
||||
|
||||
function getLogChannel(client: any, guildId: string): TextChannel | null {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return channel as TextChannel;
|
||||
}
|
||||
|
||||
function isEventEnabled(client: any, guildId: string, event: string): boolean {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
return activeEvents.includes(event);
|
||||
}
|
||||
import { Events, Message, TextChannel, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
if (!isEventEnabled(client, guildId, event)) {
|
||||
return;
|
||||
}
|
||||
const settings = client.DB.getSettings(guildId);
|
||||
if (!settings?.log_events || !settings?.log_channel) return;
|
||||
|
||||
const channel = getLogChannel(client, guildId);
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
await channel.send({ embeds: [embed] });
|
||||
await (channel as any).send({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[LOG] Error sending log (${event}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Message Delete
|
||||
export default {
|
||||
name: Events.MessageDelete,
|
||||
async execute(message: Message) {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { Events, Message, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
const settings = client.DB.getSettings(guildId);
|
||||
if (!settings?.log_events || !settings?.log_channel) return;
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
@@ -21,7 +17,6 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed
|
||||
}
|
||||
}
|
||||
|
||||
// Message Update (Edit)
|
||||
export default {
|
||||
name: Events.MessageUpdate,
|
||||
async execute(oldMessage: Message, newMessage: Message) {
|
||||
|
||||
@@ -15,9 +15,15 @@ const dbPath = join(dataDir, 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
export class DB {
|
||||
private static settingsCache = new Map<string, any>();
|
||||
private static triggersCache = new Map<string, any[]>();
|
||||
|
||||
static init() {
|
||||
console.log(`[DATABASE] Initializing at ${dbPath}...`);
|
||||
|
||||
// Enable WAL mode for better performance
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// Guild Settings
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS guild_settings (
|
||||
@@ -143,7 +149,7 @@ export class DB {
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Migration: Add missing columns to existing guild_settings
|
||||
// Migration logic
|
||||
const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[];
|
||||
const columnNames = columns.map((c: any) => c.name);
|
||||
|
||||
@@ -163,6 +169,47 @@ export class DB {
|
||||
console.log('[DATABASE] Initialization complete.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized getter for guild settings with caching
|
||||
*/
|
||||
static getSettings(guildId: string): any {
|
||||
if (this.settingsCache.has(guildId)) {
|
||||
return this.settingsCache.get(guildId);
|
||||
}
|
||||
|
||||
const settings = db.prepare('SELECT * FROM guild_settings WHERE guild_id = ?').get(guildId);
|
||||
if (settings) {
|
||||
this.settingsCache.set(guildId, settings);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized getter for auto responses with caching
|
||||
*/
|
||||
static getTriggers(guildId: string): any[] {
|
||||
if (this.triggersCache.has(guildId)) {
|
||||
return this.triggersCache.get(guildId)!;
|
||||
}
|
||||
|
||||
const triggers = db.prepare('SELECT * FROM auto_responses WHERE guild_id = ?').all(guildId);
|
||||
this.triggersCache.set(guildId, triggers);
|
||||
return triggers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for a specific guild or all
|
||||
*/
|
||||
static clearCache(guildId?: string) {
|
||||
if (guildId) {
|
||||
this.settingsCache.delete(guildId);
|
||||
this.triggersCache.delete(guildId);
|
||||
} else {
|
||||
this.settingsCache.clear();
|
||||
this.triggersCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static get(query: string, ...params: any[]) {
|
||||
return db.prepare(query).get(...params);
|
||||
}
|
||||
@@ -171,7 +218,21 @@ export class DB {
|
||||
return db.prepare(query).all(...params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended run method that automatically clears relevant caches on write
|
||||
*/
|
||||
static run(query: string, ...params: any[]) {
|
||||
return db.prepare(query).run(...params);
|
||||
const result = db.prepare(query).run(...params);
|
||||
|
||||
// Simple heuristic to clear cache on updates to settings or triggers
|
||||
const lowerQuery = query.toLowerCase();
|
||||
if (lowerQuery.includes('guild_settings') || lowerQuery.includes('auto_responses')) {
|
||||
// If the query contains a guild_id in params, we could be more specific,
|
||||
// but for safety and simplicity, we clear the cache or just wait for next read.
|
||||
// We search for guild_id in params to be surgical if possible.
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -29,35 +29,58 @@ export class TwitchManager {
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchStreamData(channelName: string) {
|
||||
/**
|
||||
* Fetches stream data for multiple channels in a single batch (max 100).
|
||||
*/
|
||||
private static async fetchStreamsBatch(channelNames: string[]) {
|
||||
const token = await this.getAccessToken();
|
||||
if (!token) return null;
|
||||
if (!token || channelNames.length === 0) return [];
|
||||
|
||||
// Twitch API allows up to 100 user_login parameters
|
||||
const query = channelNames.map(name => `user_login=${encodeURIComponent(name)}`).join('&');
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.twitch.tv/helix/streams?user_login=${channelName}`, {
|
||||
const response = await fetch(`https://api.twitch.tv/helix/streams?${query}`, {
|
||||
headers: {
|
||||
'Client-ID': process.env.TWITCH_CLIENT_ID!,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const data: any = await response.json();
|
||||
return data.data?.[0] || null;
|
||||
return data.data || [];
|
||||
} catch (error) {
|
||||
console.error(`[TWITCH] Error fetching stream data for ${channelName}:`, error);
|
||||
return null;
|
||||
console.error(`[TWITCH] Error fetching batch (${channelNames.length} channels):`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async checkStreams(client: any) {
|
||||
const token = await this.getAccessToken();
|
||||
if (!token) return;
|
||||
/**
|
||||
* Legacy helper for single channel fetch (e.g. for commands)
|
||||
*/
|
||||
static async fetchStreamData(channelName: string) {
|
||||
const streams = await this.fetchStreamsBatch([channelName]);
|
||||
return streams[0] || null;
|
||||
}
|
||||
|
||||
static async checkStreams(client: any) {
|
||||
const monitors: any[] = (client as any).DB?.all('SELECT * FROM twitch_monitors') || [];
|
||||
if (monitors.length === 0) return;
|
||||
|
||||
const uniqueChannels = [...new Set(monitors.map(m => m.channel_name.toLowerCase()))];
|
||||
const onlineStreamsMap = new Map<string, any>();
|
||||
|
||||
// Process in chunks of 100 (Twitch API limit)
|
||||
for (let i = 0; i < uniqueChannels.length; i += 100) {
|
||||
const chunk = uniqueChannels.slice(i, i + 100);
|
||||
const streams = await this.fetchStreamsBatch(chunk);
|
||||
for (const stream of streams) {
|
||||
onlineStreamsMap.set(stream.user_login.toLowerCase(), stream);
|
||||
}
|
||||
}
|
||||
|
||||
for (const monitor of monitors) {
|
||||
try {
|
||||
const stream = await this.fetchStreamData(monitor.channel_name);
|
||||
const stream = onlineStreamsMap.get(monitor.channel_name.toLowerCase());
|
||||
|
||||
if (stream) {
|
||||
const isNewStream = monitor.last_stream_id !== stream.id;
|
||||
@@ -77,44 +100,49 @@ export class TwitchManager {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[TWITCH] Error checking stream ${monitor.channel_name}:`, error);
|
||||
console.error(`[TWITCH] Error processing monitor ${monitor.channel_name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async sendNotification(client: any, monitor: any, stream: any) {
|
||||
const guild = client.guilds.cache.get(monitor.guild_id);
|
||||
if (!guild || !monitor.discord_channel_id) return;
|
||||
try {
|
||||
const guild = client.guilds.cache.get(monitor.guild_id);
|
||||
if (!guild || !monitor.discord_channel_id) return;
|
||||
|
||||
const channel = guild.channels.cache.get(monitor.discord_channel_id) as TextChannel;
|
||||
if (!channel) return;
|
||||
const channel = guild.channels.cache.get(monitor.discord_channel_id) as TextChannel;
|
||||
if (!channel) return;
|
||||
|
||||
const thumbnailUrl = stream.thumbnail_url
|
||||
.replace('{width}', '1280')
|
||||
.replace('{height}', '720') + `?t=${Date.now()}`;
|
||||
const thumbnailUrl = stream.thumbnail_url
|
||||
.replace('{width}', '1280')
|
||||
.replace('{height}', '720') + `?t=${Date.now()}`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`${stream.user_name} ist jetzt LIVE auf Twitch!`)
|
||||
.setURL(`https://twitch.tv/${stream.user_login}`)
|
||||
.setColor(0x6441a5)
|
||||
.setImage(thumbnailUrl)
|
||||
.addFields(
|
||||
{ name: 'Titel', value: stream.title || 'Kein Titel', inline: false },
|
||||
{ name: 'Kategorie', value: stream.game_name || 'Unbekannt', inline: true },
|
||||
{ name: 'Zuschauer', value: stream.viewer_count.toString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`${stream.user_name} ist jetzt LIVE auf Twitch!`)
|
||||
.setURL(`https://twitch.tv/${stream.user_login}`)
|
||||
.setColor(0x6441a5)
|
||||
.setImage(thumbnailUrl)
|
||||
.addFields(
|
||||
{ name: 'Titel', value: stream.title || 'Kein Titel', inline: false },
|
||||
{ name: 'Kategorie', value: stream.game_name || 'Unbekannt', inline: true },
|
||||
{ name: 'Zuschauer', value: stream.viewer_count.toString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
let content = `📢 **${stream.user_name}** ist online!`;
|
||||
if (monitor.custom_message) {
|
||||
content += `\n\n${monitor.custom_message}`;
|
||||
let content = `📢 **${stream.user_name}** ist online!`;
|
||||
if (monitor.custom_message) {
|
||||
content += `\n\n${monitor.custom_message}`;
|
||||
}
|
||||
|
||||
await channel.send({ content, embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[TWITCH] Failed to send notification for ${monitor.channel_name}:`, error);
|
||||
}
|
||||
|
||||
await channel.send({ content, embeds: [embed] });
|
||||
}
|
||||
|
||||
static startPolling(client: any) {
|
||||
setInterval(() => this.checkStreams(client), 5 * 60 * 1000);
|
||||
console.log('[TWITCH] Polling started (5m interval).');
|
||||
// Polling every 2 minutes for faster notifications
|
||||
setInterval(() => this.checkStreams(client), 2 * 60 * 1000);
|
||||
console.log('[TWITCH] Batch-Polling started (2m interval).');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user