Compare commits
6 Commits
9579dc7510
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b95e7fd85 | ||
|
|
8c53160812 | ||
|
|
c18441eb9a | ||
|
|
c95444feac | ||
|
|
544f04655c | ||
|
|
85951ecfb4 |
@@ -1,6 +1,6 @@
|
||||
# Papo Discord Bot
|
||||
|
||||
Discord-Bot (discord.js 14, TypeScript) mit Web-Dashboard, Prisma/PostgreSQL und Docker-Support.
|
||||
Discord-Bot (discord.js 14, TypeScript) mit Web-Dashboard, Prisma/PostgreSQL und Docker-Support..
|
||||
|
||||
## Highlights
|
||||
- Ticketsystem mit Panels, Transcripts und Support-Login (Slash-Commands wie `/ticket`, `/claim`, `/close`).
|
||||
|
||||
@@ -23,7 +23,7 @@ const command: SlashCommand = {
|
||||
|
||||
await member.ban({ reason }).catch(() => null);
|
||||
await interaction.reply({ content: `${user.tag} wurde gebannt. Grund: ${reason}` });
|
||||
context.logging.logAction(user, 'Ban', reason);
|
||||
context.logging.logAction(user, 'Ban', reason, interaction.guild);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const command: SlashCommand = {
|
||||
}
|
||||
await member.kick(reason);
|
||||
await interaction.reply({ content: `${user.tag} wurde gekickt.` });
|
||||
context.logging.logAction(user, 'Kick', reason);
|
||||
context.logging.logAction(user, 'Kick', reason, interaction.guild);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const command: SlashCommand = {
|
||||
}
|
||||
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);
|
||||
context.logging.logAction(user, 'Mute', reason, interaction.guild);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const command: SlashCommand = {
|
||||
|
||||
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);
|
||||
context.logging.logAction(user, 'Tempban', reason, interaction.guild);
|
||||
|
||||
setTimeout(async () => {
|
||||
await interaction.guild?.members.unban(user.id, 'Tempban abgelaufen').catch(() => null);
|
||||
|
||||
@@ -23,7 +23,7 @@ const command: SlashCommand = {
|
||||
}
|
||||
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);
|
||||
context.logging.logAction(user, 'Timeout', reason, interaction.guild);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const command: SlashCommand = {
|
||||
}
|
||||
await member.timeout(null).catch(() => null);
|
||||
await interaction.reply({ content: `${user.tag} ist nun entmuted.` });
|
||||
context.logging.logAction(user, 'Unmute');
|
||||
context.logging.logAction(user, 'Unmute', undefined, interaction.guild);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,15 +4,19 @@ import { SlashCommand } from '../../utils/types';
|
||||
const command: SlashCommand = {
|
||||
data: new SlashCommandBuilder().setName('help').setDescription('Zeigt Befehle und Module.'),
|
||||
async execute(interaction: ChatInputCommandInteraction) {
|
||||
const avatar = interaction.client.user?.displayAvatarURL({ size: 256 }) ?? null;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Papo Hilfe')
|
||||
.setDescription('Multi-Guild ready | Admin, Tickets, Musik, Automod, Dashboard')
|
||||
.setTitle('✨ Papo Hilfe')
|
||||
.setColor(0xf97316)
|
||||
.setThumbnail(avatar)
|
||||
.setDescription('Dein All-in-One Assistant: Tickets, Automod, Musik, Stats, 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 }
|
||||
);
|
||||
{ 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: '📊 Server-Tools', value: '`/configure` `/serverinfo` `/rank`', inline: false }
|
||||
)
|
||||
.setFooter({ text: 'Tipp: Nutze /configure für Module & Dashboard-Link' });
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,11 +17,13 @@ import { KnowledgeBaseService } from '../services/knowledgeBaseService';
|
||||
import { RegisterService } from '../services/registerService';
|
||||
import { StatsService } from '../services/statsService';
|
||||
|
||||
const logging = new LoggingService();
|
||||
|
||||
export const context = {
|
||||
client: null as Client | null,
|
||||
commandHandler: null as CommandHandler | null,
|
||||
automod: new AutoModService(true, true),
|
||||
logging: new LoggingService(),
|
||||
logging,
|
||||
automod: new AutoModService(logging, true, true),
|
||||
music: new MusicService(),
|
||||
tickets: new TicketService(),
|
||||
leveling: new LevelService(),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { context } from '../config/context';
|
||||
const event: EventHandler = {
|
||||
name: 'guildBanAdd',
|
||||
execute(ban: GuildBan) {
|
||||
context.logging.logAction(ban.user, 'Ban');
|
||||
context.logging.logAction(ban.user, 'Ban', undefined, ban.guild);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,8 +16,11 @@ const event: EventHandler = {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(welcomeCfg.embedTitle || 'Willkommen!')
|
||||
.setDescription(welcomeCfg.embedDescription || `${member} ist beigetreten.`)
|
||||
.setColor(isNaN(colorVal) ? 0x00ff99 : colorVal)
|
||||
.setFooter({ text: welcomeCfg.embedFooter || '' });
|
||||
.setColor(isNaN(colorVal) ? 0x00ff99 : colorVal);
|
||||
const footerText = (welcomeCfg.embedFooter || '').trim();
|
||||
if (footerText) {
|
||||
embed.setFooter({ text: footerText });
|
||||
}
|
||||
if (welcomeCfg.embedThumbnailData && welcomeCfg.embedThumbnailData.startsWith('data:')) {
|
||||
const [meta, b64] = welcomeCfg.embedThumbnailData.split(',');
|
||||
const ext = meta.includes('gif') ? 'gif' : 'png';
|
||||
|
||||
@@ -8,7 +8,7 @@ const event: EventHandler = {
|
||||
async execute(message: Message) {
|
||||
const cfg = message.guildId ? settingsStore.get(message.guildId) : undefined;
|
||||
if (message.guildId) context.admin.trackEvent('message', message.guildId);
|
||||
if (cfg?.automodEnabled === true) context.automod.checkMessage(message, cfg.automodConfig);
|
||||
if (cfg?.automodEnabled === true) context.automod.checkMessage(message, cfg);
|
||||
if (cfg?.levelingEnabled === true) context.leveling.handleMessage(message);
|
||||
// Ticket SLA + KB
|
||||
await context.tickets.trackFirstResponse(message);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Collection, Message, PermissionFlagsBits } from 'discord.js';
|
||||
import { Collection, Message } from 'discord.js';
|
||||
import { logger } from '../utils/logger';
|
||||
import { GuildSettings } from '../config/state';
|
||||
import { LoggingService } from './loggingService';
|
||||
|
||||
export interface AutomodConfig {
|
||||
spamThreshold?: number;
|
||||
@@ -37,11 +39,13 @@ export class AutoModService {
|
||||
};
|
||||
private defaultBadwords = ['badword', 'spamword'];
|
||||
|
||||
constructor(private linkFilterEnabled = true, private antiSpamEnabled = true) {}
|
||||
constructor(private logging?: LoggingService, private linkFilterEnabled = true, private antiSpamEnabled = true) {}
|
||||
|
||||
public async checkMessage(message: Message, cfg?: AutomodConfig) {
|
||||
if (message.author.bot) return;
|
||||
const config = { ...this.defaults, ...(cfg ?? {}) };
|
||||
public async checkMessage(message: Message, cfg?: AutomodConfig | GuildSettings) {
|
||||
if (message.author.bot || message.webhookId) return;
|
||||
if (!message.inGuild()) return;
|
||||
const guildConfig = (cfg as GuildSettings)?.automodConfig ? (cfg as GuildSettings).automodConfig : cfg;
|
||||
const config = { ...this.defaults, ...(guildConfig ?? {}) };
|
||||
const member = message.member;
|
||||
|
||||
if (member?.roles.cache.size && Array.isArray(config.whitelistRoles) && config.whitelistRoles.length) {
|
||||
@@ -50,23 +54,16 @@ export class AutoModService {
|
||||
}
|
||||
|
||||
if (this.linkFilterEnabled && config.deleteLinks !== false && this.containsLink(message.content, config.linkWhitelist)) {
|
||||
if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false;
|
||||
message.delete().catch(() => undefined);
|
||||
message.channel
|
||||
.send({ content: `${message.author}, Links sind hier nicht erlaubt.` })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
|
||||
await this.deleteMessageWithReason(message, `${message.author}, Links sind hier nicht erlaubt.`);
|
||||
const reason = `Link gefunden (nicht freigegeben)${config.linkWhitelist?.length ? ` | Whitelist: ${config.linkWhitelist.join(', ')}` : ''}`;
|
||||
logger.info(`Deleted link from ${message.author.tag}`);
|
||||
await this.logAutomodAction(message, config, 'link_filter');
|
||||
await this.logAutomodAction(message, config, 'link_filter', reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config.badWordFilter !== false && this.containsBadword(message.content, config.customBadwords)) {
|
||||
if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false;
|
||||
message.delete().catch(() => undefined);
|
||||
message.channel
|
||||
.send({ content: `${message.author}, bitte auf deine Wortwahl achten.` })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
|
||||
await this.logAutomodAction(message, config, 'badword', message.content);
|
||||
await this.deleteMessageWithReason(message, `${message.author}, bitte auf deine Wortwahl achten.`);
|
||||
await this.logAutomodAction(message, config, 'badword', 'Badword erkannt', message.content);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -74,11 +71,9 @@ export class AutoModService {
|
||||
const letters = message.content.replace(/[^a-zA-Z]/g, '');
|
||||
const upper = letters.replace(/[^A-Z]/g, '');
|
||||
if (letters.length >= 10 && upper.length / letters.length > 0.7) {
|
||||
message.delete().catch(() => undefined);
|
||||
message.channel
|
||||
.send({ content: `${message.author}, bitte weniger Capslock nutzen.` })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
|
||||
await this.logAutomodAction(message, config, 'capslock', message.content);
|
||||
await this.deleteMessageWithReason(message, `${message.author}, bitte weniger Capslock nutzen.`);
|
||||
const ratio = Math.round((upper.length / letters.length) * 100);
|
||||
await this.logAutomodAction(message, config, 'capslock', `Caps Anteil ${ratio}%`, message.content);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -98,12 +93,11 @@ export class AutoModService {
|
||||
if (tracker.count >= threshold) {
|
||||
const timeoutMs = (config.spamTimeoutMinutes ?? this.defaults.spamTimeoutMinutes!) * 60 * 1000;
|
||||
message.member?.timeout(timeoutMs, 'Automod: Spam').catch(() => undefined);
|
||||
message.channel
|
||||
.send({ content: `${message.author}, bitte langsamer schreiben (Spam-Schutz).` })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
|
||||
await this.deleteMessageWithReason(message, `${message.author}, bitte langsamer schreiben (Spam-Schutz).`);
|
||||
logger.warn(`Timed out ${message.author.tag} for spam`);
|
||||
this.spamTracker.delete(message.author.id);
|
||||
await this.logAutomodAction(message, config, 'spam', `Count ${tracker.count}`);
|
||||
const reason = `Spam erkannt (${tracker.count}/${threshold} Nachrichten innerhalb ${config.windowMs ?? this.windowMs}ms)`;
|
||||
await this.logAutomodAction(message, config, 'spam', reason);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -111,24 +105,52 @@ export class AutoModService {
|
||||
}
|
||||
|
||||
private containsBadword(content: string, custom: string[] = []) {
|
||||
const combined = [...this.defaultBadwords, ...(custom || [])].filter(Boolean).map((w) => w.toLowerCase());
|
||||
const combined = [...this.defaultBadwords, ...(custom || [])]
|
||||
.map((w) => w?.toString().trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
if (!combined.length) return false;
|
||||
const lower = content.toLowerCase();
|
||||
return combined.some((w) => lower.includes(w));
|
||||
return combined.some((w) => {
|
||||
// Try to match word boundaries first, fall back to substring to remain permissive
|
||||
const escaped = w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`\\b${escaped}\\b`, 'i');
|
||||
return regex.test(lower) || lower.includes(w);
|
||||
});
|
||||
}
|
||||
|
||||
private containsLink(content: string, whitelist: string[] = []) {
|
||||
const normalized = whitelist.map((w) => w.toLowerCase()).filter(Boolean);
|
||||
const match = /(https?:\/\/[^\s]+|discord\.gg\/[^\s]+|www\.[^\s]+)/i.exec(content);
|
||||
// Match common link formats, even without protocol
|
||||
const match = /(https?:\/\/[^\s]+|discord\.gg\/[^\s]+|www\.[^\s]+|[a-z0-9.-]+\.[a-z]{2,}\/?[^\s]*)/i.exec(content);
|
||||
if (!match) return false;
|
||||
const url = match[0].toLowerCase();
|
||||
return !normalized.some((w) => url.includes(w));
|
||||
}
|
||||
|
||||
private async logAutomodAction(message: Message, config: AutomodConfig, action: string, details?: string) {
|
||||
private async deleteMessageWithReason(message: Message, response: string) {
|
||||
await message.delete().catch(() => undefined);
|
||||
await message.channel
|
||||
.send({ content: response })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
private async logAutomodAction(message: Message, config: AutomodConfig, action: string, reason: string, content?: string) {
|
||||
try {
|
||||
const guild = message.guild;
|
||||
if (!guild) return;
|
||||
if (this.logging) {
|
||||
this.logging.logAutomodAction(guild, {
|
||||
userTag: message.author.tag,
|
||||
userId: message.author.id,
|
||||
action,
|
||||
reason,
|
||||
content,
|
||||
channel: guild.channels.cache.get(message.channelId) ?? null,
|
||||
messageUrl: message.url
|
||||
});
|
||||
return;
|
||||
}
|
||||
const loggingCfg = config.loggingConfig || {};
|
||||
const flags = loggingCfg.categories || {};
|
||||
if (flags.automodActions === false) return;
|
||||
@@ -136,8 +158,8 @@ export class AutoModService {
|
||||
if (!channelId) return;
|
||||
const channel = await guild.channels.fetch(channelId).catch(() => null);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
const content = `[Automod] ${action} by ${message.author.tag}${details ? ` | ${details}` : ''}`;
|
||||
await channel.send({ content });
|
||||
const body = `[Automod] ${action} by ${message.author.tag} | ${reason}${content ? ` | ${content.slice(0, 1800)}` : ''}`;
|
||||
await channel.send({ content: body });
|
||||
} catch (err) {
|
||||
logger.error('Automod log failed', err);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class LoggingService {
|
||||
private resolve(guild: Guild) {
|
||||
const cfg = settingsStore.get(guild.id);
|
||||
const loggingCfg = cfg?.loggingConfig || cfg?.automodConfig?.loggingConfig || {};
|
||||
const logChannelId = loggingCfg.logChannelId || cfg?.logChannelId || this.fallbackLogChannelId;
|
||||
const logChannelId = loggingCfg.logChannelId || cfg?.automodConfig?.logChannelId || cfg?.logChannelId || this.fallbackLogChannelId;
|
||||
const flags = loggingCfg.categories || {};
|
||||
const channel = logChannelId ? guild.channels.cache.get(logChannelId) : null;
|
||||
return { channel: channel && channel.type === 0 ? (channel as TextChannel) : null, flags };
|
||||
@@ -128,11 +128,11 @@ export class LoggingService {
|
||||
});
|
||||
}
|
||||
|
||||
logAction(user: User, action: string, reason?: string) {
|
||||
const guild = user instanceof GuildMember ? user.guild : null;
|
||||
if (!guild) return;
|
||||
if (!this.shouldLog(guild, 'automodActions')) return;
|
||||
const { channel } = this.resolve(guild);
|
||||
logAction(user: User | GuildMember, action: string, reason?: string, guild?: Guild) {
|
||||
const resolvedGuild = guild ?? (user instanceof GuildMember ? user.guild : null);
|
||||
if (!resolvedGuild) return;
|
||||
if (!this.shouldLog(resolvedGuild, 'automodActions')) return;
|
||||
const { channel } = this.resolve(resolvedGuild);
|
||||
if (!channel) return;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Moderation')
|
||||
@@ -141,7 +141,7 @@ export class LoggingService {
|
||||
.setColor(0x7289da)
|
||||
.setTimestamp();
|
||||
channel.send({ embeds: [embed] }).catch((err) => logger.error('Failed to log action', err));
|
||||
const guildId = (user as GuildMember)?.guild?.id;
|
||||
const guildId = resolvedGuild.id;
|
||||
if (guildId) {
|
||||
adminSink?.pushGuildLog({
|
||||
guildId,
|
||||
@@ -154,6 +154,36 @@ export class LoggingService {
|
||||
}
|
||||
}
|
||||
|
||||
logAutomodAction(guild: Guild, options: { userTag: string; userId: string; action: string; reason: string; content?: string; channel?: GuildChannel | null; messageUrl?: string }) {
|
||||
if (!this.shouldLog(guild, 'automodActions')) return;
|
||||
const { channel } = this.resolve(guild);
|
||||
if (!channel) return;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Automod')
|
||||
.setDescription(`${options.userTag} (${options.userId}) -> ${options.action}`)
|
||||
.addFields(
|
||||
{ name: 'Grund', value: this.safeField(options.reason) },
|
||||
{ name: 'Kanal', value: options.channel ? `<#${options.channel.id}>` : 'Unbekannt' }
|
||||
)
|
||||
.setColor(0xff006e)
|
||||
.setTimestamp();
|
||||
if (options.content) {
|
||||
embed.addFields({ name: 'Nachricht', value: this.safeField(options.content) });
|
||||
}
|
||||
if (options.messageUrl) {
|
||||
embed.addFields({ name: 'Link', value: options.messageUrl });
|
||||
}
|
||||
channel.send({ embeds: [embed] }).catch((err) => logger.error('Failed to log automod action', err));
|
||||
adminSink?.pushGuildLog({
|
||||
guildId: guild.id,
|
||||
level: 'INFO',
|
||||
message: `Automod: ${options.action} (${options.userTag})`,
|
||||
timestamp: Date.now(),
|
||||
category: 'automodActions'
|
||||
});
|
||||
adminSink?.trackGuildEvent(guild.id, 'automod');
|
||||
}
|
||||
|
||||
logRoleUpdate(member: GuildMember, added: string[], removed: string[]) {
|
||||
const guildId = member.guild.id;
|
||||
adminSink?.pushGuildLog({
|
||||
|
||||
@@ -49,6 +49,7 @@ export class StatsService {
|
||||
private client: Client | null = null;
|
||||
private interval?: NodeJS.Timeout;
|
||||
private lastRun = new Map<string, number>();
|
||||
private syncLocks = new Map<string, Promise<void>>();
|
||||
|
||||
public setClient(client: Client) {
|
||||
this.client = client;
|
||||
@@ -72,21 +73,25 @@ export class StatsService {
|
||||
}
|
||||
|
||||
public async saveConfig(guildId: string, config: Partial<ServerStatsConfig>) {
|
||||
return this.withGuildLock(guildId, async () => {
|
||||
const previous = await this.getConfig(guildId);
|
||||
const normalized = this.normalizeConfig({ ...previous, ...config });
|
||||
const synced = await this.syncGuild(guildId, normalized, previous);
|
||||
await settingsStore.set(guildId, { serverStatsEnabled: synced.enabled, serverStatsConfig: synced } as any);
|
||||
this.lastRun.set(guildId, Date.now());
|
||||
return synced;
|
||||
});
|
||||
}
|
||||
|
||||
public async refreshGuild(guildId: string) {
|
||||
return this.withGuildLock(guildId, async () => {
|
||||
const cfg = await this.getConfig(guildId);
|
||||
if (!cfg.enabled) return cfg;
|
||||
const synced = await this.syncGuild(guildId, cfg, cfg);
|
||||
await settingsStore.set(guildId, { serverStatsEnabled: synced.enabled, serverStatsConfig: synced } as any);
|
||||
this.lastRun.set(guildId, Date.now());
|
||||
return synced;
|
||||
});
|
||||
}
|
||||
|
||||
public async disableGuild(guildId: string) {
|
||||
@@ -248,4 +253,19 @@ export class StatsService {
|
||||
await this.refreshGuild(guildId).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private async withGuildLock<T>(guildId: string, task: () => Promise<T>): Promise<T> {
|
||||
const waitFor = this.syncLocks.get(guildId) || Promise.resolve();
|
||||
const run = (async () => {
|
||||
await waitFor.catch(() => undefined);
|
||||
return task();
|
||||
})();
|
||||
this.syncLocks.set(guildId, run.then(() => undefined, () => undefined));
|
||||
try {
|
||||
return await run;
|
||||
} finally {
|
||||
const current = this.syncLocks.get(guildId);
|
||||
if (current === run) this.syncLocks.delete(guildId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,19 +43,19 @@ router.get('/', (req, res) => {
|
||||
<aside class="sidebar">
|
||||
<div class="brand">Papo Control</div>
|
||||
<div class="nav">
|
||||
<a class="active" href="#overview" data-target="overview"><span class="icon">[*]</span> Uebersicht</a>
|
||||
<a href="#tickets" data-target="tickets"><span class="icon">[*]</span> Ticketsystem</a>
|
||||
<a href="#automod" data-target="automod" class="automod-link"><span class="icon">[*]</span> Automod</a>
|
||||
<a href="#welcome" data-target="welcome" class="welcome-link"><span class="icon">[*]</span> Willkommen</a>
|
||||
<a href="#dynamicvoice" data-target="dynamicvoice" class="dynamicvoice-link"><span class="icon">[*]</span> Dynamic Voice</a>
|
||||
<a href="#birthday" data-target="birthday" class="birthday-link"><span class="icon">[*]</span> Birthday</a>
|
||||
<a href="#reactionroles" data-target="reactionroles" class="reactionroles-link"><span class="icon">[*]</span> Reaction Roles</a>
|
||||
<a href="#statuspage" data-target="statuspage" class="statuspage-link"><span class="icon">[*]</span> Statuspage</a>
|
||||
<a href="#serverstats" data-target="serverstats" class="serverstats-link"><span class="icon">[*]</span> Server Stats</a>
|
||||
<a href="#settings" data-target="settings"><span class="icon">[*]</span> Einstellungen</a>
|
||||
<a href="#modules" data-target="modules"><span class="icon">[*]</span> Module</a>
|
||||
<a href="#events" data-target="events" class="events-link"><span class="icon">[*]</span> Events</a>
|
||||
<a href="#admin" data-target="admin" class="admin-link hidden"><span class="icon">[*]</span> Admin</a>
|
||||
<a class="active" href="#overview" data-target="overview"><span class="icon">🏠</span> Uebersicht</a>
|
||||
<a href="#tickets" data-target="tickets"><span class="icon">🎫</span> Ticketsystem</a>
|
||||
<a href="#automod" data-target="automod" class="automod-link"><span class="icon">🛡️</span> Automod</a>
|
||||
<a href="#welcome" data-target="welcome" class="welcome-link"><span class="icon">👋</span> Willkommen</a>
|
||||
<a href="#dynamicvoice" data-target="dynamicvoice" class="dynamicvoice-link"><span class="icon">🎙️</span> Dynamic Voice</a>
|
||||
<a href="#birthday" data-target="birthday" class="birthday-link"><span class="icon">🎂</span> Birthday</a>
|
||||
<a href="#reactionroles" data-target="reactionroles" class="reactionroles-link"><span class="icon">🎭</span> Reaction Roles</a>
|
||||
<a href="#statuspage" data-target="statuspage" class="statuspage-link"><span class="icon">📡</span> Statuspage</a>
|
||||
<a href="#serverstats" data-target="serverstats" class="serverstats-link"><span class="icon">📈</span> Server Stats</a>
|
||||
<a href="#settings" data-target="settings"><span class="icon">⚙️</span> Einstellungen</a>
|
||||
<a href="#modules" data-target="modules"><span class="icon">🧩</span> Module</a>
|
||||
<a href="#events" data-target="events" class="events-link"><span class="icon">📅</span> Events</a>
|
||||
<a href="#admin" data-target="admin" class="admin-link hidden"><span class="icon">🛠️</span> Admin</a>
|
||||
</div>
|
||||
<div class="muted">Angemeldet als <span id="userInfo"></span></div>
|
||||
<button id="logoutBtn" class="logout">Logout</button>
|
||||
|
||||
Reference in New Issue
Block a user