Initial: Pixelpöbel Discord Bot
Some checks failed
Auto Build and Push Docker Image / build (push) Failing after 8s
Some checks failed
Auto Build and Push Docker Image / build (push) Failing after 8s
- Modular Discord Bot mit TypeScript und discord.js - Grouped Commands (/admin, /owner subcommands) - SQLite Datenbank mit Persistenz - Twitch Monitoring mit Stream-ID Tracking - Konfigurierbares Warn-System (Auto-Kick/Mute/Ban) - Mute mit verschiedenen Zeiteinheiten (sec, min, hour, day, week, month, year, perm) - Auto-Deploy beim Start (Option 3) - Docker Support Features: - /ping, /help, /twitch (online, add, remove, list) - /admin (kick, warn, unwarn, mute, unmute, ban, unban, config) - /owner (deploy, stats, servers) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
419
src/commands/utility/admin.ts
Normal file
419
src/commands/utility/admin.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('admin')
|
||||
.setDescription('Admin-Commands')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('kick')
|
||||
.setDescription('Kickt einen Nutzer')
|
||||
.addUserOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Der zu kickende Nutzer')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Grund für den Kick')))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('warn')
|
||||
.setDescription('Verwarnt einen Nutzer')
|
||||
.addUserOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Der zu verwarnende Nutzer')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Grund für die Verwarnung')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('unwarn')
|
||||
.setDescription('Entfernt die letzte Warnung')
|
||||
.addUserOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Der Nutzer')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('mute')
|
||||
.setDescription('Muted einen Nutzer')
|
||||
.addUserOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Der zu mutende Nutzer')
|
||||
.setRequired(true))
|
||||
.addIntegerOption(option =>
|
||||
option.setName('duration')
|
||||
.setDescription('Dauer (Zahl)')
|
||||
.setMinValue(1))
|
||||
.addStringOption(option =>
|
||||
option.setName('unit')
|
||||
.setDescription('Zeiteinheit')
|
||||
.addChoices(
|
||||
{ name: 'Sekunden', value: 'sec' },
|
||||
{ name: 'Minuten', value: 'min' },
|
||||
{ name: 'Stunden', value: 'hour' },
|
||||
{ name: 'Tage', value: 'day' },
|
||||
{ name: 'Wochen', value: 'week' },
|
||||
{ name: 'Monate', value: 'month' },
|
||||
{ name: 'Jahre', value: 'year' },
|
||||
{ name: 'Permanent', value: 'perm' }
|
||||
))
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Grund für den Mute')))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('unmute')
|
||||
.setDescription('Entmutet einen Nutzer')
|
||||
.addUserOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Der zu entmutende Nutzer')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('ban')
|
||||
.setDescription('Bannt einen Nutzer')
|
||||
.addUserOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Der zu bannende Nutzer')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Grund für den Ban'))
|
||||
.addIntegerOption(option =>
|
||||
option.setName('delete_days')
|
||||
.setDescription('Nachrichten der letzten X Tage löschen')
|
||||
.setMinValue(0)
|
||||
.setMaxValue(7)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('unban')
|
||||
.setDescription('Entbannt einen Nutzer')
|
||||
.addStringOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Nutzer-ID oder @username')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('config')
|
||||
.setDescription('Konfiguriert das Warn-System')
|
||||
.addStringOption(option =>
|
||||
option.setName('setting')
|
||||
.setDescription('Welche Einstellung ändern?')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: 'warn_threshold', value: 'warn_threshold' },
|
||||
{ name: 'warn_action', value: 'warn_action' },
|
||||
{ name: 'warn_mute_duration', value: 'warn_mute_duration' },
|
||||
{ name: 'show', value: 'show' }))
|
||||
.addStringOption(option =>
|
||||
option.setName('value')
|
||||
.setDescription('Neuer Wert'))
|
||||
.addIntegerOption(option =>
|
||||
option.setName('duration_value')
|
||||
.setDescription('Dauer in Sekunden')
|
||||
.setMinValue(0)))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const DB = interaction.client.DB;
|
||||
const guild = interaction.guild;
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
// Get or create guild settings
|
||||
let settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!settings) {
|
||||
DB.run('INSERT INTO guild_settings (guild_id) VALUES (?)', guildId);
|
||||
settings = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
}
|
||||
|
||||
if (subcommand === 'kick') {
|
||||
const target = interaction.options.getMember('target') as GuildMember;
|
||||
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
|
||||
|
||||
if (!target) {
|
||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.kickable) {
|
||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await target.kick(reason);
|
||||
await interaction.reply(`✅ Gekickt: ${target.user.tag}\nGrund: ${reason}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'warn') {
|
||||
const target = interaction.options.getMember('target') as GuildMember;
|
||||
const reason = interaction.options.getString('reason')!;
|
||||
|
||||
if (!target) {
|
||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
DB.run(
|
||||
'INSERT INTO mod_logs (guild_id, user_id, moderator_id, action, reason) VALUES (?, ?, ?, ?, ?)',
|
||||
guildId,
|
||||
target.id,
|
||||
interaction.user.id,
|
||||
'WARN',
|
||||
reason
|
||||
);
|
||||
|
||||
const countResult: any = DB.get(
|
||||
'SELECT COUNT(*) as count FROM mod_logs WHERE guild_id = ? AND user_id = ? AND action = ?',
|
||||
guildId,
|
||||
target.id,
|
||||
'WARN'
|
||||
);
|
||||
|
||||
const warnCount = countResult?.count ?? 0;
|
||||
const warnThreshold = settings?.warn_threshold ?? 3;
|
||||
const warnAction = settings?.warn_action ?? 'ban';
|
||||
|
||||
let message = `✅ Verwarnt: ${target.user.tag}\nGrund: ${reason}\nDas ist die ${warnCount}. Verwarnung.`;
|
||||
|
||||
// Check if threshold reached
|
||||
if (warnCount >= warnThreshold) {
|
||||
message += `\n\n⚠️ **Warn-Limit erreicht (${warnThreshold}/${warnThreshold})!**`;
|
||||
|
||||
try {
|
||||
if (warnAction === 'kick') {
|
||||
await target.kick(`Auto-Kick: ${warnThreshold} Warnungen`);
|
||||
message += `\n👢 **Automatisch gekickt!**`;
|
||||
} else if (warnAction === 'mute') {
|
||||
const muteDuration = settings?.warn_mute_duration ?? 1800; // 30 min default
|
||||
await target.timeout(muteDuration * 1000, `Auto-Mute: ${warnThreshold} Warnungen`);
|
||||
const durationMins = Math.floor(muteDuration / 60);
|
||||
message += `\n🔇 **Automatisch gemuted (${durationMins} Min)!**`;
|
||||
} else if (warnAction === 'ban') {
|
||||
await target.ban({ reason: `Auto-Ban: ${warnThreshold} Warnungen` });
|
||||
message += `\n🔨 **Automatisch gebannt!**`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ADMIN] Auto-action failed:', error);
|
||||
message += `\n❌ Auto-Aktion fehlgeschlagen.`;
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.reply({ content: message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'unwarn') {
|
||||
const target = interaction.options.getMember('target') as GuildMember;
|
||||
|
||||
if (!target) {
|
||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const latestWarn: any = DB.get(
|
||||
'SELECT * FROM mod_logs WHERE guild_id = ? AND user_id = ? AND action = ? ORDER BY id DESC LIMIT 1',
|
||||
guildId,
|
||||
target.id,
|
||||
'WARN'
|
||||
);
|
||||
|
||||
if (!latestWarn) {
|
||||
await interaction.reply({ content: 'Keine Warnung für diesen Nutzer gefunden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
DB.run('DELETE FROM mod_logs WHERE id = ?', latestWarn.id);
|
||||
await interaction.reply(`✅ Letzte Warnung für ${target.user.tag} entfernt.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'mute') {
|
||||
const target = interaction.options.getMember('target') as GuildMember;
|
||||
const duration = interaction.options.getInteger('duration');
|
||||
const unit = interaction.options.getString('unit');
|
||||
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
|
||||
|
||||
if (!target) {
|
||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse duration
|
||||
let durationMs: number | null = null;
|
||||
let durationText = '';
|
||||
|
||||
if (unit === 'perm') {
|
||||
durationMs = null; // Permanent = null
|
||||
durationText = 'Permanent';
|
||||
} else if (duration) {
|
||||
const unitSeconds: Record<string, number> = {
|
||||
sec: 1,
|
||||
min: 60,
|
||||
hour: 60 * 60,
|
||||
day: 60 * 60 * 24,
|
||||
week: 60 * 60 * 24 * 7,
|
||||
month: 60 * 60 * 24 * 30,
|
||||
year: 60 * 60 * 24 * 365
|
||||
};
|
||||
|
||||
const multiplier = unitSeconds[unit!];
|
||||
if (!multiplier) {
|
||||
await interaction.reply({ content: '❌ Ungültige Zeiteinheit.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
durationMs = duration * multiplier * 1000;
|
||||
|
||||
// Human-readable duration
|
||||
const unitNames: Record<string, string> = {
|
||||
sec: 'Sekunden',
|
||||
min: 'Minuten',
|
||||
hour: 'Stunden',
|
||||
day: 'Tage',
|
||||
week: 'Wochen',
|
||||
month: 'Monate',
|
||||
year: 'Jahre'
|
||||
};
|
||||
durationText = `${duration} ${unitNames[unit!]}`;
|
||||
} else {
|
||||
await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await target.timeout(durationMs, reason);
|
||||
await interaction.reply(`✅ Gemuted: ${target.user.tag}\nDauer: ${durationText}\nGrund: ${reason}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'unmute') {
|
||||
const target = interaction.options.getMember('target') as GuildMember;
|
||||
|
||||
if (!target) {
|
||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await target.timeout(null, 'Unmute');
|
||||
await interaction.reply(`✅ Entmutet: ${target.user.tag}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: '❌ Fehler beim Entmuten.', ephemeral: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'ban') {
|
||||
const target = interaction.options.getMember('target') as GuildMember;
|
||||
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
|
||||
const deleteDays = interaction.options.getInteger('delete_days') ?? 0;
|
||||
|
||||
if (!target) {
|
||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.bannable) {
|
||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht bannen.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await target.ban({ deleteMessageSeconds: deleteDays * 86400, reason });
|
||||
await interaction.reply(`✅ Gebannt: ${target.user.tag}\nGrund: ${reason}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'unban') {
|
||||
let targetId = interaction.options.getString('target')!;
|
||||
|
||||
if (targetId.startsWith('<@') && targetId.endsWith('>')) {
|
||||
targetId = targetId.replace(/[<@!>]/g, '');
|
||||
}
|
||||
|
||||
try {
|
||||
await guild.bans.remove(targetId, 'Unban durch Admin');
|
||||
await interaction.reply(`✅ Entbannt: ${targetId}`);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: `❌ Fehler beim Entbannen: ${error.message}`, ephemeral: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'config') {
|
||||
const setting = interaction.options.getString('setting')!;
|
||||
const value = interaction.options.getString('value');
|
||||
const durationValue = interaction.options.getInteger('duration_value');
|
||||
|
||||
if (setting === 'show') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('⚙️ Warn-System Konfiguration')
|
||||
.setColor(0x3498db)
|
||||
.addFields(
|
||||
{ name: 'Warn-Limit', value: `${settings?.warn_threshold ?? 3} Warnungen`, inline: true },
|
||||
{ name: 'Auto-Aktion', value: settings?.warn_action ?? 'ban', inline: true },
|
||||
{ name: 'Mute-Dauer', value: `${(settings?.warn_mute_duration ?? 1800) / 60} Min`, inline: true }
|
||||
);
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (setting === 'warn_threshold') {
|
||||
const threshold = parseInt(value!);
|
||||
if (isNaN(threshold) || threshold < 1) {
|
||||
await interaction.reply({ content: '❌ Ungültiger Wert. Mindestens 1.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
DB.run('UPDATE guild_settings SET warn_threshold = ? WHERE guild_id = ?', threshold, guildId);
|
||||
await interaction.reply(`✅ Warn-Limit auf ${threshold} gesetzt.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (setting === 'warn_action') {
|
||||
const validActions = ['none', 'kick', 'mute', 'ban'];
|
||||
if (!validActions.includes(value!)) {
|
||||
await interaction.reply({ content: `❌ Ungültige Aktion. Möglich: ${validActions.join(', ')}`, ephemeral: true });
|
||||
return;
|
||||
}
|
||||
DB.run('UPDATE guild_settings SET warn_action = ? WHERE guild_id = ?', value, guildId);
|
||||
await interaction.reply(`✅ Auto-Aktion auf ${value} gesetzt.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (setting === 'warn_mute_duration') {
|
||||
const duration = durationValue ?? 1800;
|
||||
if (duration < 0) {
|
||||
await interaction.reply({ content: '❌ Ungültige Dauer.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
DB.run('UPDATE guild_settings SET warn_mute_duration = ? WHERE guild_id = ?', duration, guildId);
|
||||
await interaction.reply(`✅ Mute-Dauer auf ${duration} Sekunden (${Math.floor(duration/60)} Min) gesetzt.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
59
src/commands/utility/help.ts
Normal file
59
src/commands/utility/help.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('help')
|
||||
.setDescription('Zeigt alle verfügbaren Befehle an.')
|
||||
.setDMPermission(true),
|
||||
category: 'Public',
|
||||
async execute(interaction: any, client: any) {
|
||||
if (!client.application?.owner) await client.application?.fetch();
|
||||
|
||||
const isOwner = interaction.user.id === client.application?.owner?.id;
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📖 Hilfe-Menü')
|
||||
.setDescription('Hier sind die Befehle:')
|
||||
.setColor(0x2ecc71)
|
||||
.setTimestamp();
|
||||
|
||||
const publicCmds: string[] = [];
|
||||
const adminCmds: string[] = [];
|
||||
const ownerCmds: string[] = [];
|
||||
|
||||
client.commands.forEach((cmd: Command) => {
|
||||
const desc = 'description' in cmd.data ? cmd.data.description : 'Kontextmenü';
|
||||
const entry = `• **/${cmd.data.name}**: ${desc}`;
|
||||
|
||||
if (cmd.category === 'Public') {
|
||||
publicCmds.push(entry);
|
||||
} else if (cmd.category === 'Admin') {
|
||||
adminCmds.push(entry);
|
||||
} else if (cmd.category === 'Owner') {
|
||||
ownerCmds.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
if (publicCmds.length > 0) {
|
||||
embed.addFields({ name: '🌍 Öffentliche Befehle', value: publicCmds.join('\n') });
|
||||
}
|
||||
|
||||
if (isAdmin || isOwner) {
|
||||
if (adminCmds.length > 0) {
|
||||
embed.addFields({ name: '🛡️ Admin-Befehle', value: adminCmds.join('\n') });
|
||||
}
|
||||
}
|
||||
|
||||
if (isOwner) {
|
||||
if (ownerCmds.length > 0) {
|
||||
embed.addFields({ name: '🔑 Owner-Befehle', value: ownerCmds.join('\n') });
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
107
src/commands/utility/owner.ts
Normal file
107
src/commands/utility/owner.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { Deployer } from '../../structures/Deployer.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('owner')
|
||||
.setDescription('Bot-Owner Commands')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('deploy')
|
||||
.setDescription('Aktualisiert Commands'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('stats')
|
||||
.setDescription('Zeigt Bot-Statistiken'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('servers')
|
||||
.setDescription('Listet alle Server'))
|
||||
.setDMPermission(true),
|
||||
category: 'Owner',
|
||||
async execute(interaction: any, client: any) {
|
||||
if (!client.application?.owner) await client.application?.fetch();
|
||||
|
||||
if (interaction.user.id !== client.application?.owner?.id) {
|
||||
await interaction.reply({ content: 'Keine Berechtigung.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
if (subcommand === 'deploy') {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
const count = await Deployer.deploy(client.user!.id, client.token!);
|
||||
await interaction.editReply(`✅ ${count} Befehle aktualisiert!`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.editReply('❌ Fehler beim Deployment.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'stats') {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const uptime = process.uptime();
|
||||
const days = Math.floor(uptime / 86400);
|
||||
const hours = Math.floor(uptime / 3600) % 24;
|
||||
const minutes = Math.floor(uptime / 60) % 60;
|
||||
const uptimeStr = `${days}d ${hours}h ${minutes}m`;
|
||||
|
||||
const totalServers = client.guilds.cache.size;
|
||||
const totalUsers = client.guilds.cache.reduce((acc: number, guild: any) => acc + guild.memberCount, 0);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📊 Bot-Statistiken')
|
||||
.setColor(0x3498db)
|
||||
.addFields(
|
||||
{ name: '🤖 Bot', value: `**Uptime:** ${uptimeStr}\n**Latency:** ${client.ws.ping}ms`, inline: true },
|
||||
{ name: '🌍 Netzwerk', value: `**Server:** ${totalServers}\n**Nutzer:** ${totalUsers}`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'servers') {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guilds = client.guilds.cache.map((guild: any) =>
|
||||
`• **${guild.name}** \`(${guild.id})\` - ${guild.memberCount} Mitglieder`
|
||||
);
|
||||
|
||||
const chunks = [];
|
||||
for (let i = 0; i < guilds.length; i += 10) {
|
||||
chunks.push(guilds.slice(i, i + 10).join('\n'));
|
||||
}
|
||||
|
||||
if (chunks.length === 0) {
|
||||
await interaction.editReply('Keine Server.');
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`🌍 Server (${client.guilds.cache.size})`)
|
||||
.setColor(0xf1c40f)
|
||||
.setDescription(chunks[0].substring(0, 4000))
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
|
||||
for (let i = 1; i < chunks.length; i++) {
|
||||
const nextEmbed = new EmbedBuilder()
|
||||
.setColor(0xf1c40f)
|
||||
.setDescription(chunks[i].substring(0, 4000));
|
||||
await interaction.followUp({ embeds: [nextEmbed], ephemeral: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
14
src/commands/utility/ping.ts
Normal file
14
src/commands/utility/ping.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Testet die Latenz.'),
|
||||
category: 'Public',
|
||||
async execute(interaction: any) {
|
||||
await interaction.reply(`🏓 Pong! Latenz: ${interaction.client.ws.ping}ms`);
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
121
src/commands/utility/twitch.ts
Normal file
121
src/commands/utility/twitch.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { TwitchManager } from '../../structures/TwitchManager.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('twitch')
|
||||
.setDescription('Twitch-Befehle.')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('online')
|
||||
.setDescription('Prüft, ob ein Kanal online ist.')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Name des Twitch-Kanals')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Kanal überwachen (Admin).')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Name des Twitch-Kanals')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('message')
|
||||
.setDescription('Optionale Nachricht')))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Kanal aus Überwachung entfernen (Admin).')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Name des Twitch-Kanals')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Listet überwachte Kanäle.')),
|
||||
category: 'Public',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const guildId = interaction.guildId;
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||
const DB = interaction.client.DB;
|
||||
|
||||
if (subcommand === 'online') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase();
|
||||
await interaction.deferReply();
|
||||
|
||||
const stream = await TwitchManager.fetchStreamData(channelName);
|
||||
|
||||
if (!stream) {
|
||||
await interaction.editReply(`🔴 Der Kanal **${channelName}** ist offline.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const thumbnailUrl = stream.thumbnail_url
|
||||
.replace('{width}', '1280')
|
||||
.replace('{height}', '720') + `?t=${Date.now()}`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`🟢 ${stream.user_name} ist ONLINE!`)
|
||||
.setURL(`https://twitch.tv/${stream.user_login}`)
|
||||
.setColor(0x00FF00)
|
||||
.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();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? AND discord_channel_id = ?', guildId, interaction.channelId);
|
||||
if (monitors.length === 0) {
|
||||
await interaction.reply('Keine Twitch-Kanäle überwacht.');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = monitors.map((m: any) => `• **${m.channel_name}**`).join('\n');
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📺 Überwachte Twitch-Kanäle')
|
||||
.setColor(0x6441a5)
|
||||
.setDescription(list);
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin only
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: '❌ Keine Berechtigung.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase();
|
||||
const message = interaction.options.getString('message');
|
||||
const discordChannelId = interaction.channelId;
|
||||
|
||||
DB.run(
|
||||
'INSERT INTO twitch_monitors (guild_id, channel_name, discord_channel_id, custom_message) VALUES (?, ?, ?, ?) ON CONFLICT(guild_id, channel_name) DO UPDATE SET discord_channel_id = ?, custom_message = ?',
|
||||
guildId, channelName, discordChannelId, message, discordChannelId, message
|
||||
);
|
||||
await interaction.reply(`✅ Kanal **${channelName}** wird überwacht.`);
|
||||
}
|
||||
|
||||
if (subcommand === 'remove') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase();
|
||||
DB.run('DELETE FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, channelName);
|
||||
await interaction.reply(`✅ Kanal **${channelName}** entfernt.`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
9
src/deploy-commands.ts
Normal file
9
src/deploy-commands.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'dotenv/config';
|
||||
import { Deployer } from './structures/Deployer.js';
|
||||
|
||||
if (!process.env.CLIENT_ID || !process.env.DISCORD_TOKEN) {
|
||||
console.error('Missing CLIENT_ID or DISCORD_TOKEN in .env');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Deployer.deploy(process.env.CLIENT_ID!, process.env.DISCORD_TOKEN!);
|
||||
32
src/events/interactionCreate.ts
Normal file
32
src/events/interactionCreate.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Events } from 'discord.js';
|
||||
|
||||
export default {
|
||||
name: Events.InteractionCreate,
|
||||
async execute(interaction: any, client: any) {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
console.error(`No command matching ${interaction.commandName} was found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.execute(interaction, client);
|
||||
client.DB.run(`
|
||||
INSERT INTO command_stats (command_name, uses)
|
||||
VALUES (?, 1)
|
||||
ON CONFLICT(command_name) DO UPDATE SET uses = uses + 1
|
||||
`, interaction.commandName);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const replyOptions = { content: 'There was an error while executing this command!', ephemeral: true };
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp(replyOptions);
|
||||
} else {
|
||||
await interaction.reply(replyOptions);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
10
src/events/ready.ts
Normal file
10
src/events/ready.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Events } from 'discord.js';
|
||||
|
||||
export default {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
execute(client: any) {
|
||||
console.log(`[READY] Logged in as ${client.user.tag}`);
|
||||
console.log(`[READY] Serving ${client.guilds.cache.size} servers`);
|
||||
},
|
||||
};
|
||||
62
src/index.ts
Normal file
62
src/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'dotenv/config';
|
||||
import { ExtendedClient } from './structures/ExtendedClient.js';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { Command } from './structures/Command.js';
|
||||
import { DB } from './structures/Database.js';
|
||||
import { TwitchManager } from './structures/TwitchManager.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Initialize Database
|
||||
DB.init();
|
||||
|
||||
const client = new ExtendedClient();
|
||||
// Make DB available on client
|
||||
(client as any).DB = DB;
|
||||
|
||||
// Load Commands
|
||||
const commandsPath = join(__dirname, 'commands');
|
||||
const commandFolders = readdirSync(commandsPath);
|
||||
|
||||
for (const folder of commandFolders) {
|
||||
const folderPath = join(commandsPath, folder);
|
||||
const commandFiles = readdirSync(folderPath).filter(file => file.endsWith('.ts'));
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const filePath = join(folderPath, file);
|
||||
const fileUrl = pathToFileURL(filePath).href;
|
||||
const command: Command = (await import(fileUrl)).default;
|
||||
|
||||
if (command && 'data' in command && 'execute' in command) {
|
||||
client.commands.set(command.data.name, command);
|
||||
console.log(`[COMMAND] Loaded: ${command.data.name}`);
|
||||
} else {
|
||||
console.warn(`[COMMAND] The command at ${filePath} is missing a required "data" or "execute" property.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load Events
|
||||
const eventsPath = join(__dirname, 'events');
|
||||
const eventFiles = readdirSync(eventsPath).filter(file => file.endsWith('.ts'));
|
||||
|
||||
for (const file of eventFiles) {
|
||||
const filePath = join(eventsPath, file);
|
||||
const fileUrl = pathToFileURL(filePath).href;
|
||||
const event = (await import(fileUrl)).default;
|
||||
|
||||
if (event.once) {
|
||||
client.once(event.name, (...args) => event.execute(...args, client));
|
||||
} else {
|
||||
client.on(event.name, (...args) => event.execute(...args, client));
|
||||
}
|
||||
console.log(`[EVENT] Loaded: ${event.name}`);
|
||||
}
|
||||
|
||||
// Start Twitch Polling
|
||||
TwitchManager.startPolling(client);
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN);
|
||||
6
src/structures/Command.ts
Normal file
6
src/structures/Command.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Command {
|
||||
data: any;
|
||||
category: 'Owner' | 'Admin' | 'Public';
|
||||
execute: (interaction: any, client: any) => Promise<void> | void;
|
||||
cooldown?: number;
|
||||
}
|
||||
103
src/structures/Database.ts
Normal file
103
src/structures/Database.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const dataDir = join(process.cwd(), 'data');
|
||||
if (!existsSync(dataDir)) {
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
const dbPath = join(dataDir, 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
export class DB {
|
||||
static init() {
|
||||
console.log(`[DATABASE] Initializing at ${dbPath}...`);
|
||||
|
||||
// Guild Settings
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS guild_settings (
|
||||
guild_id TEXT PRIMARY KEY,
|
||||
prefix TEXT DEFAULT '!',
|
||||
mod_log_channel TEXT,
|
||||
welcome_channel TEXT,
|
||||
warn_threshold INTEGER DEFAULT 3,
|
||||
warn_action TEXT DEFAULT 'ban',
|
||||
warn_mute_duration INTEGER DEFAULT 1800
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Mod Logs
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS mod_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
user_id TEXT,
|
||||
moderator_id TEXT,
|
||||
action TEXT,
|
||||
reason TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Command Stats
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS command_stats (
|
||||
command_name TEXT PRIMARY KEY,
|
||||
uses INTEGER DEFAULT 0
|
||||
)
|
||||
`).run();
|
||||
|
||||
// DM Users
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS dm_users (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
username TEXT,
|
||||
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Twitch Monitors
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS twitch_monitors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
channel_name TEXT,
|
||||
discord_channel_id TEXT,
|
||||
custom_message TEXT,
|
||||
last_status TEXT DEFAULT 'offline',
|
||||
last_stream_id TEXT,
|
||||
UNIQUE(guild_id, channel_name)
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Auto Responses
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS auto_responses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
trigger_word TEXT,
|
||||
response_text TEXT,
|
||||
UNIQUE(guild_id, trigger_word)
|
||||
)
|
||||
`).run();
|
||||
|
||||
console.log('[DATABASE] Initialization complete.');
|
||||
}
|
||||
|
||||
static get(query: string, ...params: any[]) {
|
||||
return db.prepare(query).get(...params);
|
||||
}
|
||||
|
||||
static all(query: string, ...params: any[]) {
|
||||
return db.prepare(query).all(...params);
|
||||
}
|
||||
|
||||
static run(query: string, ...params: any[]) {
|
||||
return db.prepare(query).run(...params);
|
||||
}
|
||||
}
|
||||
59
src/structures/Deployer.ts
Normal file
59
src/structures/Deployer.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { REST, Routes } from 'discord.js';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export class Deployer {
|
||||
static async deploy(clientId: string, token: string) {
|
||||
const commands = [];
|
||||
const rootDir = join(__dirname, '..');
|
||||
const commandsPath = join(rootDir, 'commands');
|
||||
const commandFolders = readdirSync(commandsPath);
|
||||
|
||||
for (const folder of commandFolders) {
|
||||
const folderPath = join(commandsPath, folder);
|
||||
const commandFiles = readdirSync(folderPath).filter(file => file.endsWith('.ts'));
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const filePath = join(folderPath, file);
|
||||
const fileUrl = pathToFileURL(filePath).href;
|
||||
const command = (await import(fileUrl)).default;
|
||||
|
||||
if (command && 'data' in command) {
|
||||
commands.push(command.data.toJSON());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rest = new REST().setToken(token);
|
||||
|
||||
console.log(`[DEPLOYER] Refreshing ${commands.length} application (/) commands.`);
|
||||
|
||||
const data: any = await rest.put(
|
||||
Routes.applicationCommands(clientId),
|
||||
{ body: commands },
|
||||
);
|
||||
|
||||
console.log(`[DEPLOYER] Successfully reloaded ${data.length} application (/) commands.`);
|
||||
return data.length;
|
||||
}
|
||||
|
||||
static async deployIfMissing(clientId: string, token: string) {
|
||||
const rest = new REST().setToken(token);
|
||||
|
||||
try {
|
||||
const data: any = await rest.get(Routes.applicationCommands(clientId));
|
||||
if (data.length > 0) {
|
||||
console.log('[DEPLOYER] Commands already registered. Skipping deployment.');
|
||||
return data.length;
|
||||
}
|
||||
} catch (error) {
|
||||
// Commands don't exist yet
|
||||
}
|
||||
|
||||
return this.deploy(clientId, token);
|
||||
}
|
||||
}
|
||||
17
src/structures/ExtendedClient.ts
Normal file
17
src/structures/ExtendedClient.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Client, Collection, GatewayIntentBits } from 'discord.js';
|
||||
import { Command } from './Command.js';
|
||||
|
||||
export class ExtendedClient extends Client {
|
||||
public commands: Collection<string, Command> = new Collection();
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
120
src/structures/TwitchManager.ts
Normal file
120
src/structures/TwitchManager.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||
|
||||
export class TwitchManager {
|
||||
private static accessToken: string | null = null;
|
||||
private static tokenExpires: number = 0;
|
||||
|
||||
private static async getAccessToken() {
|
||||
if (this.accessToken && Date.now() < this.tokenExpires) return this.accessToken;
|
||||
|
||||
const clientId = process.env.TWITCH_CLIENT_ID;
|
||||
const clientSecret = process.env.TWITCH_CLIENT_SECRET;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
console.error('[TWITCH] Missing Client ID or Secret in .env');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data: any = await response.json();
|
||||
this.accessToken = data.access_token;
|
||||
this.tokenExpires = Date.now() + (data.expires_in * 1000) - 60000;
|
||||
return this.accessToken;
|
||||
} catch (error) {
|
||||
console.error('[TWITCH] Error getting access token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchStreamData(channelName: string) {
|
||||
const token = await this.getAccessToken();
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.twitch.tv/helix/streams?user_login=${channelName}`, {
|
||||
headers: {
|
||||
'Client-ID': process.env.TWITCH_CLIENT_ID!,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const data: any = await response.json();
|
||||
return data.data?.[0] || null;
|
||||
} catch (error) {
|
||||
console.error(`[TWITCH] Error fetching stream data for ${channelName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async checkStreams(client: any) {
|
||||
const token = await this.getAccessToken();
|
||||
if (!token) return;
|
||||
|
||||
const monitors: any[] = (client as any).DB?.all('SELECT * FROM twitch_monitors') || [];
|
||||
if (monitors.length === 0) return;
|
||||
|
||||
for (const monitor of monitors) {
|
||||
try {
|
||||
const stream = await this.fetchStreamData(monitor.channel_name);
|
||||
|
||||
if (stream) {
|
||||
const isNewStream = monitor.last_stream_id !== stream.id;
|
||||
const wasOffline = monitor.last_status === 'offline';
|
||||
|
||||
if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
|
||||
await this.sendNotification(client, monitor, stream);
|
||||
(client as any).DB?.run(
|
||||
'UPDATE twitch_monitors SET last_status = "online", last_stream_id = ? WHERE id = ?',
|
||||
stream.id,
|
||||
monitor.id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (monitor.last_status === 'online') {
|
||||
(client as any).DB?.run('UPDATE twitch_monitors SET last_status = "offline", last_stream_id = NULL WHERE id = ?', monitor.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[TWITCH] Error checking stream ${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;
|
||||
|
||||
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 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}`;
|
||||
}
|
||||
|
||||
await channel.send({ content, embeds: [embed] });
|
||||
}
|
||||
|
||||
static startPolling(client: any) {
|
||||
setInterval(() => this.checkStreams(client), 5 * 60 * 1000);
|
||||
console.log('[TWITCH] Polling started (5m interval).');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user