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

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;