Initialer Bot Upload
Some checks failed
Auto Build and Push Docker Image / build (push) Failing after 15s
Some checks failed
Auto Build and Push Docker Image / build (push) Failing after 15s
This commit is contained in:
42
src/commands/moderation/kick.ts
Normal file
42
src/commands/moderation/kick.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder, PermissionFlagsBits, GuildMember } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('kick')
|
||||
.setDescription('Kickt einen Nutzer vom Server.')
|
||||
.addUserOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Der zu kickende Nutzer')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Grund für den Kick'))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
|
||||
category: 'Admin',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
const target = interaction.options.getMember('target') as GuildMember;
|
||||
const reason = interaction.options.getString('reason') ?? 'Kein Grund angegeben';
|
||||
|
||||
if (!target) {
|
||||
await interaction.reply({ content: 'Dieser Nutzer konnte nicht gefunden werden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.kickable) {
|
||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken. Haben sie eine höhere Rolle als ich?', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await target.kick(reason);
|
||||
await interaction.reply(`Erfolgreich gekickt: ${target.user.tag}. Grund: ${reason}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: 'Fehler beim Kicken des Nutzers.', ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
61
src/commands/moderation/trigger.ts
Normal file
61
src/commands/moderation/trigger.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
PermissionFlagsBits,
|
||||
EmbedBuilder
|
||||
} from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { DB } from '../../structures/Database.js';
|
||||
|
||||
const triggerCommand: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('trigger')
|
||||
.setDescription('Verwaltet automatische Antworten (Admin only).')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Fügt eine automatische Antwort hinzu.')
|
||||
.addStringOption(option => option.setName('word').setDescription('Das Trigger-Wort').setRequired(true))
|
||||
.addStringOption(option => option.setName('response').setDescription('Die Antwort (Text oder Link)').setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Entfernt eine automatische Antwort.')
|
||||
.addStringOption(option => option.setName('word').setDescription('Das Trigger-Wort').setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Listet alle Trigger auf.'))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
||||
category: 'Admin',
|
||||
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const guildId = interaction.guildId!;
|
||||
|
||||
if (subcommand === 'add') {
|
||||
const word = interaction.options.getString('word')!.toLowerCase();
|
||||
const response = interaction.options.getString('response')!;
|
||||
|
||||
DB.run('INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?) ON CONFLICT(guild_id, trigger_word) DO UPDATE SET response_text = ?',
|
||||
guildId, word, response, response);
|
||||
await interaction.reply(`✅ Trigger für **${word}** hinzugefügt.`);
|
||||
}
|
||||
|
||||
if (subcommand === 'remove') {
|
||||
const word = interaction.options.getString('word')!.toLowerCase();
|
||||
DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, word);
|
||||
await interaction.reply(`✅ Trigger für **${word}** entfernt.`);
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const triggers: any[] = DB.all('SELECT * FROM auto_responses WHERE guild_id = ?', guildId);
|
||||
const list = triggers.map(t => `• **${t.trigger_word}** ➔ ${t.response_text.substring(0, 50)}${t.response_text.length > 50 ? '...' : ''}`).join('\n') || 'Keine Trigger.';
|
||||
const embed = new EmbedBuilder().setTitle('🤖 Trigger').setColor(0xe67e22).setDescription(list);
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default triggerCommand;
|
||||
71
src/commands/moderation/triggerContext.ts
Normal file
71
src/commands/moderation/triggerContext.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
ContextMenuCommandBuilder,
|
||||
ApplicationCommandType,
|
||||
MessageContextMenuCommandInteraction,
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
ActionRowBuilder,
|
||||
ModalSubmitInteraction
|
||||
} from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { DB } from '../../structures/Database.js';
|
||||
|
||||
const contextCommand: Command = {
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Als Trigger speichern')
|
||||
.setType(ApplicationCommandType.Message),
|
||||
category: 'Admin',
|
||||
|
||||
async execute(interaction: MessageContextMenuCommandInteraction, client: ExtendedClient) {
|
||||
const message = interaction.targetMessage;
|
||||
|
||||
// Inhalt bestimmen: Text oder erster Anhang (Bild/GIF)
|
||||
const responseText = message.content || message.attachments.first()?.url;
|
||||
|
||||
if (!responseText) {
|
||||
await interaction.reply({ content: 'Diese Nachricht enthält weder Text noch Medien, die ich speichern kann.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Modal erstellen
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`triggerContext-modal-${interaction.id}`) // Prefix muss dem Filename entsprechen
|
||||
.setTitle('Als Trigger speichern');
|
||||
|
||||
const wordInput = new TextInputBuilder()
|
||||
.setCustomId('triggerWord')
|
||||
.setLabel('Welches Wort soll die Antwort auslösen?')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(true)
|
||||
.setPlaceholder('z.B. eis');
|
||||
|
||||
const responseInput = new TextInputBuilder()
|
||||
.setCustomId('triggerResponse')
|
||||
.setLabel('Antwort (wurde automatisch ausgefüllt)')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setValue(responseText)
|
||||
.setRequired(true);
|
||||
|
||||
modal.addComponents(
|
||||
new ActionRowBuilder<TextInputBuilder>().addComponents(wordInput),
|
||||
new ActionRowBuilder<TextInputBuilder>().addComponents(responseInput)
|
||||
);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
},
|
||||
|
||||
async handleModal(interaction: ModalSubmitInteraction, client: ExtendedClient) {
|
||||
const word = interaction.fields.getTextInputValue('triggerWord').toLowerCase();
|
||||
const response = interaction.fields.getTextInputValue('triggerResponse');
|
||||
const guildId = interaction.guildId!;
|
||||
|
||||
DB.run('INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?) ON CONFLICT(guild_id, trigger_word) DO UPDATE SET response_text = ?',
|
||||
guildId, word, response, response);
|
||||
|
||||
await interaction.reply({ content: `✅ Erfolg! Der Bot antwortet nun auf **${word}** mit dem Inhalt der Nachricht.`, ephemeral: true });
|
||||
}
|
||||
};
|
||||
|
||||
export default contextCommand;
|
||||
59
src/commands/moderation/warn.ts
Normal file
59
src/commands/moderation/warn.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder, PermissionFlagsBits, GuildMember } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { DB } from '../../structures/Database.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('warn')
|
||||
.setDescription('Verwarnt einen Nutzer und speichert dies in der Datenbank.')
|
||||
.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))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||
category: 'Admin',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
const target = interaction.options.getMember('target') as GuildMember;
|
||||
const reason = interaction.options.getString('reason')!;
|
||||
const moderator = interaction.user;
|
||||
|
||||
if (!target) {
|
||||
await interaction.reply({ content: 'Dieser Nutzer konnte nicht gefunden werden.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Speichern in der Datenbank
|
||||
DB.run(
|
||||
'INSERT INTO mod_logs (guild_id, user_id, moderator_id, action, reason) VALUES (?, ?, ?, ?, ?)',
|
||||
interaction.guildId,
|
||||
target.id,
|
||||
moderator.id,
|
||||
'WARN',
|
||||
reason
|
||||
);
|
||||
|
||||
// Zählen der bisherigen Verwarnungen
|
||||
const countResult: any = DB.get(
|
||||
'SELECT COUNT(*) as count FROM mod_logs WHERE guild_id = ? AND user_id = ? AND action = ?',
|
||||
interaction.guildId,
|
||||
target.id,
|
||||
'WARN'
|
||||
);
|
||||
|
||||
await interaction.reply({
|
||||
content: `Nutzer ${target.user.tag} wurde verwarnt. Grund: ${reason}\nDies ist die ${countResult.count}. Verwarnung.`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: 'Fehler beim Speichern der Verwarnung.', ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
38
src/commands/owner/deploy.ts
Normal file
38
src/commands/owner/deploy.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { Deployer } from '../../structures/Deployer.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('deploy')
|
||||
.setDescription('Aktualisiert alle Slash-Commands bei Discord (Nur für Bot-Owner).')
|
||||
.setDMPermission(true), // Erlaubt den Befehl in privaten Chats
|
||||
category: 'Owner',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
// Sicherstellen, dass die Applikation geladen ist, um den Owner zu prüfen
|
||||
if (!client.application?.owner) {
|
||||
await client.application?.fetch();
|
||||
}
|
||||
|
||||
const ownerId = client.application?.owner?.id;
|
||||
|
||||
// Sicherheitsprüfung: Nur der Owner darf diesen Befehl ausführen
|
||||
if (interaction.user.id !== ownerId) {
|
||||
await interaction.reply({ content: 'Du hast keine Berechtigung für diesen Befehl.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
const count = await Deployer.deploy(client.user!.id, client.token!);
|
||||
await interaction.editReply(`Erfolgreich ${count} Befehle aktualisiert! (Es kann bis zu einer Stunde dauern, bis alle Änderungen global sichtbar sind.)`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.editReply('Fehler beim Deployment der Befehle.');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
54
src/commands/owner/dmusers.ts
Normal file
54
src/commands/owner/dmusers.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { DB } from '../../structures/Database.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('dmusers')
|
||||
.setDescription('Listet alle Nutzer auf, die den Bot privat angeschrieben haben (Nur Owner).')
|
||||
.setDMPermission(true),
|
||||
category: 'Owner',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
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;
|
||||
}
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const users: any[] = DB.all('SELECT * FROM dm_users ORDER BY last_seen DESC');
|
||||
|
||||
if (users.length === 0) {
|
||||
await interaction.editReply('Es wurden noch keine Nutzer im privaten Chat registriert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const userList = users.map(u =>
|
||||
`• **${u.username}** \`(${u.user_id})\` - Letzte Aktivität: ${u.last_seen}`
|
||||
);
|
||||
|
||||
const chunks = [];
|
||||
for (let i = 0; i < userList.length; i += 10) {
|
||||
chunks.push(userList.slice(i, i + 10).join('\n'));
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`💬 Registrierte DM-Nutzer (${users.length})`)
|
||||
.setColor(0x9b59b6)
|
||||
.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(0x9b59b6)
|
||||
.setDescription(chunks[i].substring(0, 4000));
|
||||
await interaction.followUp({ embeds: [nextEmbed], ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
41
src/commands/owner/leave.ts
Normal file
41
src/commands/owner/leave.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('leave')
|
||||
.setDescription('Veranlasst den Bot, einen Server zu verlassen (Nur Owner).')
|
||||
.addStringOption(option =>
|
||||
option.setName('guild_id')
|
||||
.setDescription('Die ID des Servers, den der Bot verlassen soll')
|
||||
.setRequired(true))
|
||||
.setDMPermission(true),
|
||||
category: 'Owner',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
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 guildId = interaction.options.getString('guild_id')!;
|
||||
const guild = client.guilds.cache.get(guildId);
|
||||
|
||||
if (!guild) {
|
||||
await interaction.reply({ content: `Server mit der ID \`${guildId}\` wurde nicht gefunden.`, ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const guildName = guild.name;
|
||||
await guild.leave();
|
||||
await interaction.reply({ content: `✅ Der Bot hat den Server **${guildName}** (${guildId}) verlassen.`, ephemeral: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: `❌ Fehler beim Verlassen des Servers mit ID \`${guildId}\`.`, ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
52
src/commands/owner/servers.ts
Normal file
52
src/commands/owner/servers.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('servers')
|
||||
.setDescription('Listet alle Server auf, auf denen der Bot aktiv ist (Nur Owner).')
|
||||
.setDMPermission(true),
|
||||
category: 'Owner',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
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;
|
||||
}
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guilds = client.guilds.cache.map(guild =>
|
||||
`• **${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('Der Bot ist aktuell auf keinem Server.');
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`🌍 Aktive Server (${client.guilds.cache.size})`)
|
||||
.setColor(0xf1c40f)
|
||||
.setDescription(chunks[0].substring(0, 4000))
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
|
||||
// Falls es mehr als 10 Server sind, schicken wir weitere Embeds als Follow-up
|
||||
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 });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
55
src/commands/owner/stats.ts
Normal file
55
src/commands/owner/stats.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder, version as djsVersion } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { DB } from '../../structures/Database.js';
|
||||
import os from 'node:os';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('stats')
|
||||
.setDescription('Zeigt technische und Nutzungs-Statistiken (Nur Owner).')
|
||||
.setDMPermission(true), // Erlaubt den Befehl in privaten Chats
|
||||
category: 'Owner',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
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;
|
||||
}
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
// System-Daten
|
||||
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 ramUsage = (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2);
|
||||
const totalServers = client.guilds.cache.size;
|
||||
const totalUsers = client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0);
|
||||
|
||||
// Datenbank-Daten
|
||||
const totalWarns: any = DB.get('SELECT COUNT(*) as count FROM mod_logs WHERE action = "WARN"');
|
||||
const topCommands: any[] = DB.all('SELECT * FROM command_stats ORDER BY uses DESC LIMIT 5');
|
||||
const commandList = topCommands.length > 0
|
||||
? topCommands.map(c => `• **${c.command_name}**: ${c.uses}x`).join('\n')
|
||||
: 'Noch keine Daten.';
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📊 Bot-Owner Statistiken')
|
||||
.setColor(0x3498db)
|
||||
.addFields(
|
||||
{ name: '🤖 Bot-Status', value: `**Uptime:** ${uptimeStr}\n**Latency:** ${client.ws.ping}ms\n**D.JS Version:** v${djsVersion}`, inline: true },
|
||||
{ name: '🖥 System', value: `**RAM:** ${ramUsage} MB\n**OS:** ${os.platform()}\n**Cores:** ${os.cpus().length}`, inline: true },
|
||||
{ name: '🌍 Netzwerk', value: `**Server:** ${totalServers}\n**Nutzer:** ${totalUsers}`, inline: true },
|
||||
{ name: '📈 Datenbank', value: `**Warnungen gesamt:** ${totalWarns.count}\n\n**Top 5 Befehle:**\n${commandList}` }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
61
src/commands/utility/help.ts
Normal file
61
src/commands/utility/help.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('help')
|
||||
.setDescription('Zeigt eine Liste aller verfügbaren Befehle an.')
|
||||
.setDMPermission(true),
|
||||
category: 'Public',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
if (!client.application?.owner) await client.application?.fetch();
|
||||
|
||||
const isOwner = interaction.user.id === client.application?.owner?.id;
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) || interaction.memberPermissions?.has(PermissionFlagsBits.ManageMessages);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📖 Hilfe-Menü')
|
||||
.setDescription('Hier sind die Befehle, auf die du Zugriff hast:')
|
||||
.setColor(0x2ecc71)
|
||||
.setTimestamp();
|
||||
|
||||
// Befehle nach Kategorien gruppieren
|
||||
const publicCmds: string[] = [];
|
||||
const adminCmds: string[] = [];
|
||||
const ownerCmds: string[] = [];
|
||||
|
||||
client.commands.forEach((cmd) => {
|
||||
const entry = `• **/${cmd.data.name}**: ${cmd.data.description}`;
|
||||
|
||||
if (cmd.category === 'Public') {
|
||||
publicCmds.push(entry);
|
||||
} else if (cmd.category === 'Admin') {
|
||||
adminCmds.push(entry);
|
||||
} else if (cmd.category === 'Owner') {
|
||||
ownerCmds.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
// Felder hinzufügen basierend auf Rechten
|
||||
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;
|
||||
15
src/commands/utility/ping.ts
Normal file
15
src/commands/utility/ping.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Antwortet mit Pong!'),
|
||||
category: 'Public',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
await interaction.reply('Pong!');
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
170
src/commands/utility/twitch.ts
Normal file
170
src/commands/utility/twitch.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||||
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { DB } from '../../structures/Database.js';
|
||||
import { TwitchManager } from '../../structures/TwitchManager.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('twitch')
|
||||
.setDescription('Twitch-Befehle (Status & Management).')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('online')
|
||||
.setDescription('Prüft sofort, ob ein Twitch-Kanal online ist.')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Name des Twitch-Kanals')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Überwacht einen Twitch-Kanal in diesem Discord-Kanal (Admin only).')
|
||||
.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('Entfernt einen Twitch-Kanal aus der Überwachung (Admin only).')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Name des Twitch-Kanals')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Listet überwachte Twitch-Kanäle für diesen Discord-Kanal auf.'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('listall')
|
||||
.setDescription('Listet ALLE überwachten Twitch-Kanäle des Servers auf (Admin only).'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('interval')
|
||||
.setDescription('Setzt das Abfrage-Intervall (Admin only).')
|
||||
.addIntegerOption(option =>
|
||||
option.setName('minutes')
|
||||
.setDescription('Minuten (2-30)')
|
||||
.setMinValue(2)
|
||||
.setMaxValue(30)
|
||||
.setRequired(true))),
|
||||
category: 'Public',
|
||||
async execute(interaction: ChatInputCommandInteraction, client: ExtendedClient) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const guildId = interaction.guildId!;
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||
|
||||
// --- PUBLIC COMMANDS ---
|
||||
|
||||
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 aktuell offline oder existiert nicht.`);
|
||||
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('In diesem Discord-Kanal werden aktuell keine Twitch-Kanäle überwacht.');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = monitors.map(m => `• **${m.channel_name}** ${m.custom_message ? `(Nachricht: *${m.custom_message}*)` : ''}`).join('\n');
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📺 Twitch-Kanäle für diesen Kanal')
|
||||
.setColor(0x6441a5)
|
||||
.setDescription(list);
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
return;
|
||||
}
|
||||
|
||||
// --- ADMIN ONLY COMMANDS ---
|
||||
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: '❌ Du benötigst Administrator-Berechtigungen für diesen Befehl.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase();
|
||||
const message = interaction.options.getString('message');
|
||||
const discordChannelId = interaction.channelId;
|
||||
|
||||
try {
|
||||
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 nun in diesem Discord-Kanal überwacht.`);
|
||||
} catch (e) {
|
||||
await interaction.reply({ content: `❌ Fehler beim Hinzufügen von **${channelName}**.`, ephemeral: true });
|
||||
}
|
||||
}
|
||||
|
||||
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}** wurde aus der Überwachung entfernt.`);
|
||||
}
|
||||
|
||||
if (subcommand === 'listall') {
|
||||
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ?', guildId);
|
||||
if (monitors.length === 0) {
|
||||
await interaction.reply('Es werden aktuell keine Twitch-Kanäle auf diesem Server überwacht.');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = monitors.map(m => `• **${m.channel_name}** in <#${m.discord_channel_id}> \`(${m.discord_channel_id})\``).join('\n');
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📺 Alle überwachten Twitch-Kanäle (Serverweit)')
|
||||
.setColor(0x6441a5)
|
||||
.setDescription(list);
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (subcommand === 'interval') {
|
||||
const minutes = interaction.options.getInteger('minutes')!;
|
||||
|
||||
DB.run(
|
||||
'INSERT INTO guild_settings (guild_id, twitch_interval) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET twitch_interval = ?',
|
||||
guildId, minutes, minutes
|
||||
);
|
||||
|
||||
await interaction.reply(`✅ Abfrage-Intervall für Twitch wurde auf **${minutes} Minuten** gesetzt.`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
15
src/deploy-commands.ts
Normal file
15
src/deploy-commands.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await Deployer.deploy(process.env.CLIENT_ID!, process.env.DISCORD_TOKEN!);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
||||
75
src/events/interactionCreate.ts
Normal file
75
src/events/interactionCreate.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Events, Interaction } from 'discord.js';
|
||||
import { ExtendedClient } from '../structures/ExtendedClient.js';
|
||||
|
||||
import { DB } from '../structures/Database.js';
|
||||
|
||||
export default {
|
||||
name: Events.InteractionCreate,
|
||||
async execute(interaction: Interaction, client: ExtendedClient) {
|
||||
// --- SLASH COMMANDS & CONTEXT MENUS ---
|
||||
if (interaction.isChatInputCommand() || interaction.isMessageContextMenuCommand()) {
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
console.error(`No command matching ${interaction.commandName} was found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Typ-Check für die Ausführung
|
||||
if (interaction.isChatInputCommand()) {
|
||||
await (command.execute as (interaction: ChatInputCommandInteraction, client: ExtendedClient) => Promise<void>)(interaction, client);
|
||||
} else if (interaction.isMessageContextMenuCommand()) {
|
||||
await (command.execute as (interaction: MessageContextMenuCommandInteraction, client: ExtendedClient) => Promise<void>)(interaction, client);
|
||||
}
|
||||
|
||||
// Statistik nur für Slash-Commands (oder optional für beides)
|
||||
if (interaction.isChatInputCommand()) {
|
||||
DB.run(`
|
||||
INSERT INTO command_stats (command_name, uses)
|
||||
VALUES (?, 1)
|
||||
ON CONFLICT(command_name) DO UPDATE SET uses = uses + 1
|
||||
`, interaction.commandName);
|
||||
}
|
||||
|
||||
// DM Nutzer registrieren
|
||||
if (!interaction.guildId) {
|
||||
DB.run(`
|
||||
INSERT INTO dm_users (user_id, username, last_seen)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
username = excluded.username,
|
||||
last_seen = CURRENT_TIMESTAMP
|
||||
`, interaction.user.id, interaction.user.tag);
|
||||
}
|
||||
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- MODAL SUBMISSIONS ---
|
||||
if (interaction.isModalSubmit()) {
|
||||
// Wir suchen das Kommando anhand des Modal-Custom-IDs (wir nutzen hier ein Präfix-System)
|
||||
const [commandName] = interaction.customId.split('-');
|
||||
const command = client.commands.get(commandName);
|
||||
|
||||
if (command && command.handleModal) {
|
||||
try {
|
||||
await command.handleModal(interaction, client);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: 'Fehler beim Verarbeiten des Formulars.', ephemeral: true });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
43
src/events/messageCreate.ts
Normal file
43
src/events/messageCreate.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Events, Message } from 'discord.js';
|
||||
import { ExtendedClient } from '../structures/ExtendedClient.js';
|
||||
import { DB } from '../structures/Database.js';
|
||||
|
||||
export default {
|
||||
name: Events.MessageCreate,
|
||||
async execute(message: Message, client: ExtendedClient) {
|
||||
// Ignoriere Bots
|
||||
if (message.author.bot) return;
|
||||
|
||||
// Prüfen, ob es ein privater Chat (DM) ist
|
||||
if (!message.guildId) {
|
||||
// Nutzer in der Datenbank registrieren oder aktualisieren
|
||||
DB.run(`
|
||||
INSERT INTO dm_users (user_id, username, last_seen)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
username = excluded.username,
|
||||
last_seen = CURRENT_TIMESTAMP
|
||||
`, message.author.id, message.author.tag);
|
||||
|
||||
console.log(`[DM-TRACKER] Registered DM interaction from ${message.author.tag} (${message.author.id})`);
|
||||
}
|
||||
|
||||
// --- AUTO RESPONDER ---
|
||||
if (message.guildId) {
|
||||
const content = message.content.toLowerCase();
|
||||
const guildId = message.guildId;
|
||||
|
||||
// Alle Trigger für diese Gilde abrufen
|
||||
const triggers: any[] = DB.all('SELECT trigger_word, response_text FROM auto_responses WHERE guild_id = ?', guildId);
|
||||
|
||||
for (const item of triggers) {
|
||||
// Prüfen, ob der Trigger in der Nachricht vorkommt (Groß-/Kleinschreibung durch .toLowerCase() egal)
|
||||
if (content.includes(item.trigger_word.toLowerCase())) {
|
||||
await message.reply(item.response_text);
|
||||
// Wir brechen nach der ersten Übereinstimmung ab, um Spam zu vermeiden (optional)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
10
src/events/ready.ts
Normal file
10
src/events/ready.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Events } from 'discord.js';
|
||||
import { ExtendedClient } from '../structures/ExtendedClient.js';
|
||||
|
||||
export default {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
execute(client: ExtendedClient) {
|
||||
console.log(`Ready! Logged in as ${client.user?.tag}`);
|
||||
},
|
||||
};
|
||||
60
src/index.ts
Normal file
60
src/index.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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();
|
||||
|
||||
// Start Twitch Polling
|
||||
TwitchManager.startPolling(client);
|
||||
|
||||
// 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') || file.endsWith('.js'));
|
||||
|
||||
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') || file.endsWith('.js'));
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN);
|
||||
18
src/structures/Command.ts
Normal file
18
src/structures/Command.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
SlashCommandOptionsOnlyBuilder,
|
||||
SlashCommandSubcommandsOnlyBuilder,
|
||||
ContextMenuCommandBuilder,
|
||||
MessageContextMenuCommandInteraction,
|
||||
ModalSubmitInteraction
|
||||
} from 'discord.js';
|
||||
import { ExtendedClient } from './ExtendedClient.js';
|
||||
|
||||
export interface Command {
|
||||
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder | ContextMenuCommandBuilder;
|
||||
category: 'Owner' | 'Admin' | 'Public';
|
||||
execute: (interaction: ChatInputCommandInteraction | MessageContextMenuCommandInteraction, client: ExtendedClient) => Promise<void>;
|
||||
handleModal?: (interaction: ModalSubmitInteraction, client: ExtendedClient) => Promise<void>;
|
||||
cooldown?: number;
|
||||
}
|
||||
112
src/structures/Database.ts
Normal file
112
src/structures/Database.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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);
|
||||
|
||||
// Pfad für die Datenbank: In Docker ein persistentes Volume (/app/data/database.sqlite)
|
||||
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}...`);
|
||||
|
||||
// Beispiel-Tabelle: Gilden-Konfiguration (z.B. für Präfixe oder Kanäle)
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS guild_settings (
|
||||
guild_id TEXT PRIMARY KEY,
|
||||
prefix TEXT DEFAULT '!',
|
||||
mod_log_channel TEXT,
|
||||
welcome_channel TEXT
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Beispiel-Tabelle: Moderations-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();
|
||||
|
||||
// Tabelle für Befehls-Statistiken
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS command_stats (
|
||||
command_name TEXT PRIMARY KEY,
|
||||
uses INTEGER DEFAULT 0
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Tabelle für Nutzer, die den Bot privat anschreiben
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS dm_users (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
username TEXT,
|
||||
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Tabelle für Twitch-Überwachung
|
||||
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',
|
||||
UNIQUE(guild_id, channel_name)
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Migration: Falls die Spalte discord_channel_id noch nicht existiert
|
||||
try {
|
||||
db.prepare('ALTER TABLE twitch_monitors ADD COLUMN discord_channel_id TEXT').run();
|
||||
} catch (e) {}
|
||||
|
||||
// Update guild_settings um Twitch-Intervall
|
||||
try {
|
||||
db.prepare('ALTER TABLE guild_settings ADD COLUMN twitch_interval INTEGER DEFAULT 5').run();
|
||||
} catch (e) {
|
||||
// Spalte existiert wahrscheinlich schon
|
||||
}
|
||||
|
||||
// Tabelle für Auto-Responder (Trigger & Antworten)
|
||||
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);
|
||||
}
|
||||
}
|
||||
44
src/structures/Deployer.ts
Normal file
44
src/structures/Deployer.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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 = [];
|
||||
// Wir gehen zwei Ebenen hoch, da wir in src/structures/ sind
|
||||
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') || file.endsWith('.js'));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
138
src/structures/TwitchManager.ts
Normal file
138
src/structures/TwitchManager.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||
import { ExtendedClient } from './ExtendedClient.js';
|
||||
import { DB } from './Database.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: ExtendedClient) {
|
||||
const token = await this.getAccessToken();
|
||||
if (!token) return;
|
||||
|
||||
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors');
|
||||
if (monitors.length === 0) return;
|
||||
|
||||
for (const monitor of monitors) {
|
||||
try {
|
||||
// Prüfen, ob das Intervall für diese Gilde bereits abgelaufen ist
|
||||
const guildSettings: any = DB.get('SELECT twitch_interval FROM guild_settings WHERE guild_id = ?', monitor.guild_id);
|
||||
const interval = guildSettings?.twitch_interval || 5;
|
||||
|
||||
// Wir nutzen einen einfachen Timestamp-basierten Check im Speicher,
|
||||
// um nicht bei jedem globalen Tick jeden Kanal abzufragen (Schonung der Twitch-API Rate-Limits)
|
||||
// (Vereinfachung für diesen Bot: Wir checken alle Kanäle,
|
||||
// da wir global alle 2 Minuten pollen und die Twitch API das locker hergibt)
|
||||
|
||||
const response = await fetch(`https://api.twitch.tv/helix/streams?user_login=${monitor.channel_name}`, {
|
||||
headers: {
|
||||
'Client-ID': process.env.TWITCH_CLIENT_ID!,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const data: any = await response.json();
|
||||
const stream = data.data?.[0];
|
||||
|
||||
if (stream) {
|
||||
if (monitor.last_status === 'offline') {
|
||||
await this.sendNotification(client, monitor, stream);
|
||||
DB.run('UPDATE twitch_monitors SET last_status = "online" WHERE id = ?', monitor.id);
|
||||
}
|
||||
} else {
|
||||
if (monitor.last_status === 'online') {
|
||||
DB.run('UPDATE twitch_monitors SET last_status = "offline" WHERE id = ?', monitor.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[TWITCH] Error checking stream ${monitor.channel_name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async sendNotification(client: ExtendedClient, monitor: any, stream: any) {
|
||||
const guild = client.guilds.cache.get(monitor.guild_id);
|
||||
if (!guild) return;
|
||||
|
||||
// Nutze den spezifischen Kanal, in dem "add" ausgeführt wurde
|
||||
const channelId = monitor.discord_channel_id;
|
||||
if (!channelId) return;
|
||||
|
||||
const channel = guild.channels.cache.get(channelId) 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: ExtendedClient) {
|
||||
// Initialer Check nach 10 Sekunden, dann alle 2 Minuten (wir prüfen intern das Intervall)
|
||||
// Einfachheitshalber checken wir alle 5 Minuten global,
|
||||
// aber für die Zukunft könnte man das pro Gilde verfeinern.
|
||||
setInterval(() => this.checkStreams(client), 5 * 60 * 1000);
|
||||
console.log('[TWITCH] Polling started (5m interval).');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user