feat: initial Papo bot scaffold

This commit is contained in:
Pascal.P
2025-11-30 11:04:41 +01:00
commit 000481a3b0
12168 changed files with 1584750 additions and 0 deletions

30
src/commands/admin/ban.ts Normal file
View File

@@ -0,0 +1,30 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Bannt einen Nutzer.')
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Grund'))
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
async execute(interaction: ChatInputCommandInteraction) {
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason') ?? 'Kein Grund angegeben';
if (!interaction.guild) return;
const member = await interaction.guild.members.fetch(user.id).catch(() => null);
if (!member) {
await interaction.reply({ content: 'Mitglied nicht gefunden.', ephemeral: true });
return;
}
await member.ban({ reason }).catch(() => null);
await interaction.reply({ content: `${user.tag} wurde gebannt. Grund: ${reason}` });
context.logging.logAction(user, 'Ban', reason);
}
};
export default command;

View File

@@ -0,0 +1,22 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('clear')
.setDescription('Löscht Nachrichten (max 100).')
.addIntegerOption((opt) => opt.setName('amount').setDescription('Anzahl').setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
async execute(interaction: ChatInputCommandInteraction) {
const amount = interaction.options.getInteger('amount', true);
if (!interaction.channel || amount < 1 || amount > 100) {
await interaction.reply({ content: 'Anzahl muss zwischen 1 und 100 liegen.', ephemeral: true });
return;
}
const messages = await interaction.channel.bulkDelete(amount, true);
await interaction.reply({ content: `Gelöschte Nachrichten: ${messages.size}`, ephemeral: true });
}
};
export default command;

View File

@@ -0,0 +1,28 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('kick')
.setDescription('Kickt einen Nutzer vom Server.')
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Grund'))
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guild) return;
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason') ?? 'Kein Grund angegeben';
const member = await interaction.guild.members.fetch(user.id).catch(() => null);
if (!member) {
await interaction.reply({ content: 'Mitglied nicht gefunden.', ephemeral: true });
return;
}
await member.kick(reason);
await interaction.reply({ content: `${user.tag} wurde gekickt.` });
context.logging.logAction(user, 'Kick', reason);
}
};
export default command;

View File

@@ -0,0 +1,30 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('mute')
.setDescription('Stummschalten per Timeout (Minuten).')
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer').setRequired(true))
.addIntegerOption((opt) => opt.setName('minutes').setDescription('Dauer').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Grund'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guild) return;
const user = interaction.options.getUser('user', true);
const minutes = interaction.options.getInteger('minutes', true);
const reason = interaction.options.getString('reason') ?? 'Kein Grund angegeben';
const member = await interaction.guild.members.fetch(user.id).catch(() => null);
if (!member) {
await interaction.reply({ content: 'Mitglied nicht gefunden.', ephemeral: true });
return;
}
await member.timeout(minutes * 60 * 1000, reason).catch(() => null);
await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten gemutet.` });
context.logging.logAction(user, 'Mute', reason);
}
};
export default command;

View File

@@ -0,0 +1,36 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('tempban')
.setDescription('Bannt einen Nutzer temporär (Angabe in Minuten).')
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer').setRequired(true))
.addIntegerOption((opt) => opt.setName('minutes').setDescription('Dauer').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Grund'))
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guild) return;
const user = interaction.options.getUser('user', true);
const minutes = interaction.options.getInteger('minutes', true);
const reason = interaction.options.getString('reason') ?? 'Kein Grund angegeben';
const member = await interaction.guild.members.fetch(user.id).catch(() => null);
if (!member) {
await interaction.reply({ content: 'Mitglied nicht gefunden.', ephemeral: true });
return;
}
await member.ban({ reason: `${reason} | ${minutes} Minuten` });
await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten gebannt.` });
context.logging.logAction(user, 'Tempban', reason);
setTimeout(async () => {
await interaction.guild?.members.unban(user.id, 'Tempban abgelaufen').catch(() => null);
}, minutes * 60 * 1000);
}
};
export default command;

View File

@@ -0,0 +1,30 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('timeout')
.setDescription('Timeout in Minuten setzen.')
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer').setRequired(true))
.addIntegerOption((opt) => opt.setName('minutes').setDescription('Dauer').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Grund'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guild) return;
const user = interaction.options.getUser('user', true);
const minutes = interaction.options.getInteger('minutes', true);
const reason = interaction.options.getString('reason') ?? 'Kein Grund angegeben';
const member = await interaction.guild.members.fetch(user.id).catch(() => null);
if (!member) {
await interaction.reply({ content: 'Mitglied nicht gefunden.', ephemeral: true });
return;
}
await member.timeout(minutes * 60 * 1000, reason).catch(() => null);
await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten in Timeout gesetzt.` });
context.logging.logAction(user, 'Timeout', reason);
}
};
export default command;

View File

@@ -0,0 +1,26 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('unmute')
.setDescription('Hebt einen Timeout auf.')
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer').setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guild) return;
const user = interaction.options.getUser('user', true);
const member = await interaction.guild.members.fetch(user.id).catch(() => null);
if (!member) {
await interaction.reply({ content: 'Mitglied nicht gefunden.', ephemeral: true });
return;
}
await member.timeout(null).catch(() => null);
await interaction.reply({ content: `${user.tag} ist nun entmuted.` });
context.logging.logAction(user, 'Unmute');
}
};
export default command;

View File

@@ -0,0 +1,30 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { LoopMode } from '../../services/musicService.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('loop')
.setDescription('Loop-Modus setzen (off/song/queue).')
.addStringOption((opt) =>
opt
.setName('mode')
.setDescription('Loop Modus')
.addChoices(
{ name: 'aus', value: 'off' },
{ name: 'song', value: 'song' },
{ name: 'queue', value: 'queue' }
)
.setRequired(true)
),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
const mode = interaction.options.getString('mode', true) as LoopMode;
context.music.setLoop(interaction.guildId, mode);
await interaction.reply({ content: `Loop-Modus: ${mode}` });
}
};
export default command;

View File

@@ -0,0 +1,15 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('pause').setDescription('Pausiert die Wiedergabe.'),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
context.music.pause(interaction.guildId);
await interaction.reply({ content: 'Pausiert.' });
}
};
export default command;

View File

@@ -0,0 +1,17 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('play')
.setDescription('Spielt einen Song oder fügt ihn zur Queue hinzu.')
.addStringOption((opt) => opt.setName('query').setDescription('Link oder Suchbegriff').setRequired(true)),
async execute(interaction: ChatInputCommandInteraction) {
const query = interaction.options.getString('query', true);
await context.music.play(interaction, query);
}
};
export default command;

View File

@@ -0,0 +1,23 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('queue').setDescription('Zeigt die aktuelle Queue.'),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
const info = context.music.getQueueInfo(interaction.guildId);
if (!info) {
await interaction.reply({ content: 'Keine Queue vorhanden.', ephemeral: true });
return;
}
const embed = new EmbedBuilder()
.setTitle('Aktuelle Queue')
.addFields({ name: 'Jetzt', value: info.nowPlaying ? info.nowPlaying.title : 'Nichts' })
.setDescription(info.upcoming.map((s, i) => `${i + 1}. ${s.title}${s.requester}`).join('\n') || 'Keine weiteren Songs');
await interaction.reply({ embeds: [embed] });
}
};
export default command;

View File

@@ -0,0 +1,15 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('resume').setDescription('Setzt die Wiedergabe fort.'),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
context.music.resume(interaction.guildId);
await interaction.reply({ content: 'Weiter gehts.' });
}
};
export default command;

View File

@@ -0,0 +1,15 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('skip').setDescription('Überspringt den aktuellen Song.'),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
context.music.skip(interaction.guildId);
await interaction.reply({ content: 'Übersprungen.' });
}
};
export default command;

View File

@@ -0,0 +1,15 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('stop').setDescription('Stoppt die Wiedergabe und leert die Queue.'),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
context.music.stop(interaction.guildId);
await interaction.reply({ content: 'Musik gestoppt und Queue geleert.' });
}
};
export default command;

View File

@@ -0,0 +1,18 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('claim').setDescription('Übernimmt das aktuelle Ticket.'),
async execute(interaction: ChatInputCommandInteraction) {
const ok = await context.tickets.claimTicket(interaction);
if (!ok) {
await interaction.reply({ content: 'Kein Ticket in diesem Kanal gefunden.', ephemeral: true });
return;
}
await interaction.reply({ content: 'Ticket übernommen.' });
}
};
export default command;

View File

@@ -0,0 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('close')
.setDescription('Schließt das aktuelle Ticket.')
.addStringOption((opt) => opt.setName('reason').setDescription('Grund')),
async execute(interaction: ChatInputCommandInteraction) {
const reason = interaction.options.getString('reason') ?? undefined;
const ok = await context.tickets.closeTicket(interaction, reason);
if (!ok) {
await interaction.reply({ content: 'Kein Ticket in diesem Kanal gefunden.', ephemeral: true });
return;
}
await interaction.reply({ content: 'Ticket geschlossen.' });
}
};
export default command;

View File

@@ -0,0 +1,26 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, PermissionFlagsBits, ChannelType, TextChannel } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('ticketpanel')
.setDescription('Sendet ein Ticket-Panel in einen Kanal.')
.addChannelOption((opt) =>
opt.setName('channel').setDescription('Zielkanal').addChannelTypes(ChannelType.GuildText)
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels),
async execute(interaction: ChatInputCommandInteraction) {
const target = (interaction.options.getChannel('channel') as TextChannel | null) ?? interaction.channel;
if (!target || !target.isTextBased()) {
await interaction.reply({ content: 'Bitte wähle einen Textkanal.', ephemeral: true });
return;
}
const { embed, buttons } = context.tickets.buildPanelEmbed();
await target.send({ embeds: [embed], components: [buttons] });
await interaction.reply({ content: `Ticket-Panel gesendet in ${target}`, ephemeral: true });
}
};
export default command;

View File

@@ -0,0 +1,34 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { prisma } from '../../database/index.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('ticketpriority')
.setDescription('Setzt die Priorität des aktuellen Tickets.')
.addStringOption((opt) =>
opt
.setName('level')
.setDescription('Priorität')
.addChoices(
{ name: 'Low', value: 'low' },
{ name: 'Normal', value: 'normal' },
{ name: 'High', value: 'high' }
)
.setRequired(true)
),
async execute(interaction: ChatInputCommandInteraction) {
const channelId = interaction.channelId;
const ticket = await prisma.ticket.findFirst({ where: { channelId } });
if (!ticket) {
await interaction.reply({ content: 'Kein Ticket in diesem Kanal.', ephemeral: true });
return;
}
const level = interaction.options.getString('level', true) as 'low' | 'normal' | 'high';
await prisma.ticket.update({ where: { id: ticket.id }, data: { priority: level } });
await interaction.reply({ content: `Ticket-Priorität gesetzt auf **${level}**.` });
}
};
export default command;

View File

@@ -0,0 +1,33 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { prisma } from '../../database/index.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('ticketstatus')
.setDescription('Ändert den Status des aktuellen Tickets.')
.addStringOption((opt) =>
opt
.setName('status')
.setDescription('Neuer Status')
.addChoices(
{ name: 'Offen', value: 'open' },
{ name: 'In Bearbeitung', value: 'in-progress' },
{ name: 'Geschlossen', value: 'closed' }
)
.setRequired(true)
),
async execute(interaction: ChatInputCommandInteraction) {
const status = interaction.options.getString('status', true) as 'open' | 'in-progress' | 'closed';
const ticket = await prisma.ticket.findFirst({ where: { channelId: interaction.channelId } });
if (!ticket) {
await interaction.reply({ content: 'Kein Ticket in diesem Kanal.', ephemeral: true });
return;
}
await prisma.ticket.update({ where: { id: ticket.id }, data: { status } });
await interaction.reply({ content: `Ticket-Status geändert zu **${status}**.` });
}
};
export default command;

View File

@@ -0,0 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('ticket').setDescription('Erstellt ein persönliches Ticket.'),
async execute(interaction: ChatInputCommandInteraction) {
const ticket = await context.tickets.createTicket(interaction);
if (!ticket) {
if (!interaction.replied) {
await interaction.reply({ content: 'Ticket konnte nicht erstellt werden.', ephemeral: true });
}
return;
}
if (!interaction.replied) {
await interaction.reply({ content: 'Ticket erstellt! Schau in deinem neuen Kanal nach.', ephemeral: true });
}
}
};
export default command;

View File

@@ -0,0 +1,25 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { prisma } from '../../database/index.js';
import path from 'path';
import fs from 'fs';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('transcript').setDescription('Exportiert das Transcript dieses Tickets.'),
async execute(interaction: ChatInputCommandInteraction) {
const channelId = interaction.channelId;
const ticket = await prisma.ticket.findFirst({ where: { channelId } });
if (!ticket) {
await interaction.reply({ content: 'Kein Ticket in diesem Kanal.', ephemeral: true });
return;
}
const filePath = await context.tickets.exportTranscript(interaction.channel!, ticket.id);
const fileName = path.basename(filePath);
await interaction.reply({ content: `Transcript exportiert: ${fileName}`, files: [fs.createReadStream(filePath)] });
}
};
export default command;

View File

@@ -0,0 +1,36 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, PermissionFlagsBits, ChannelType } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { settings } from '../../config/state.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('configure')
.setDescription('Setzt Basis-Einstellungen des Bots (gildenspezifisch).')
.addChannelOption((opt) => opt.setName('welcome_channel').setDescription('Kanal für Willkommensnachrichten').addChannelTypes(ChannelType.GuildText))
.addChannelOption((opt) => opt.setName('log_channel').setDescription('Kanal für Logs').addChannelTypes(ChannelType.GuildText))
.addBooleanOption((opt) => opt.setName('automod').setDescription('Automod an/aus'))
.addBooleanOption((opt) => opt.setName('leveling').setDescription('Level-System an/aus'))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
const guildSetting = settings.get(interaction.guildId) ?? {};
const welcome = interaction.options.getChannel('welcome_channel');
const logChannel = interaction.options.getChannel('log_channel');
const automod = interaction.options.getBoolean('automod');
const leveling = interaction.options.getBoolean('leveling');
if (welcome) guildSetting.welcomeChannelId = welcome.id;
if (logChannel) guildSetting.logChannelId = logChannel.id;
if (automod !== null) guildSetting.automodEnabled = automod;
if (leveling !== null) guildSetting.levelingEnabled = leveling;
settings.set(interaction.guildId, guildSetting);
context.logging = new (context.logging.constructor as any)(guildSetting.logChannelId);
await interaction.reply({ content: 'Einstellungen gespeichert.', ephemeral: true });
}
};
export default command;

View File

@@ -0,0 +1,20 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
const command: SlashCommand = {
data: new SlashCommandBuilder().setName('help').setDescription('Zeigt Befehle und Module.'),
async execute(interaction: ChatInputCommandInteraction) {
const embed = new EmbedBuilder()
.setTitle('Papo Hilfe')
.setDescription('Multi-Guild ready | Admin, Tickets, Musik, Automod, Dashboard')
.addFields(
{ name: 'Admin', value: '/ban /kick /mute /timeout /clear', inline: false },
{ name: 'Tickets', value: '/ticket /ticketpanel /ticketpriority /ticketstatus /transcript', inline: false },
{ name: 'Musik', value: '/play /pause /resume /skip /stop /queue /loop', inline: false },
{ name: 'Utility', value: '/ping /configure /serverinfo /rank', inline: false }
);
await interaction.reply({ embeds: [embed], ephemeral: true });
}
};
export default command;

View File

@@ -0,0 +1,11 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
const command: SlashCommand = {
data: new SlashCommandBuilder().setName('ping').setDescription('Antwortet mit Pong!'),
async execute(interaction: ChatInputCommandInteraction) {
await interaction.reply({ content: 'Pong!' });
}
};
export default command;

View File

@@ -0,0 +1,19 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('rank')
.setDescription('Zeigt deinen XP-Status an.')
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer (optional)')),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
const user = interaction.options.getUser('user') ?? interaction.user;
const level = context.leveling.getLevel(user.id, interaction.guildId);
await interaction.reply({ content: `${user.tag}: Level ${level.level}, XP ${level.xp}` });
}
};
export default command;

View File

@@ -0,0 +1,21 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder().setName('serverinfo').setDescription('Zeigt Infos zum aktuellen Server.'),
async execute(interaction: ChatInputCommandInteraction) {
const guild = interaction.guild!;
const embed = new EmbedBuilder()
.setTitle(guild.name)
.setThumbnail(guild.iconURL() ?? null)
.addFields(
{ name: 'Mitglieder', value: `${guild.memberCount}`, inline: true },
{ name: 'Erstellt', value: `<t:${Math.floor(guild.createdTimestamp / 1000)}:R>`, inline: true },
{ name: 'ID', value: guild.id, inline: true }
);
await interaction.reply({ embeds: [embed] });
}
};
export default command;