Add events module with dashboard UI, scheduling, signups, and settings updates; extend env/readme.

This commit is contained in:
Pascal Prießnitz
2025-12-02 23:52:10 +01:00
parent 874b01c999
commit 829d160164
578 changed files with 37647 additions and 11590 deletions

View File

@@ -1,15 +1,63 @@
import { CommandHandler } from '../services/commandHandler.js';
import { AutoModService } from '../services/automodService.js';
import { LoggingService } from '../services/loggingService.js';
import { MusicService } from '../services/musicService.js';
import { TicketService } from '../services/ticketService.js';
import { LevelService } from '../services/levelService.js';
import { CommandHandler } from '../services/commandHandler';
import { AutoModService } from '../services/automodService';
import { LoggingService, setLoggingAdmin } from '../services/loggingService';
import { MusicService } from '../services/musicService';
import { TicketService } from '../services/ticketService';
import { LevelService } from '../services/levelService';
import { Client } from 'discord.js';
import { BotModuleService } from '../services/moduleService';
import { DynamicVoiceService } from '../services/dynamicVoiceService';
import { AdminService } from '../services/adminService';
import { StatuspageService } from '../services/statuspageService';
import { BirthdayService } from '../services/birthdayService';
import { ReactionRoleService } from '../services/reactionRoleService';
import { EventService } from '../services/eventService';
export const context = {
client: null as Client | null,
commandHandler: null as CommandHandler | null,
automod: new AutoModService(true, true),
logging: new LoggingService(),
music: new MusicService(),
tickets: new TicketService(),
leveling: new LevelService()
leveling: new LevelService(),
dynamicVoice: new DynamicVoiceService(),
modules: new BotModuleService(),
admin: new AdminService(),
statuspage: new StatuspageService(),
birthdays: new BirthdayService(),
reactionRoles: new ReactionRoleService(),
events: new EventService()
};
context.modules.setHooks({
musicEnabled: {
onDisable: async () => context.music.stopAll()
},
dynamicVoiceEnabled: {
onDisable: async (guildId: string) => context.dynamicVoice.cleanupGuild(guildId, context.client)
},
statuspageEnabled: {
onEnable: async (guildId: string) => {
const cfg = await context.statuspage.getConfig(guildId);
await context.statuspage.saveConfig(guildId, { ...cfg, enabled: true });
},
onDisable: async (guildId: string) => {
const cfg = await context.statuspage.getConfig(guildId);
await context.statuspage.saveConfig(guildId, { ...cfg, enabled: false });
}
},
birthdayEnabled: {
onEnable: async (guildId: string) => context.birthdays.invalidate(guildId),
onDisable: async (guildId: string) => context.birthdays.invalidate(guildId)
},
reactionRolesEnabled: {
onEnable: async (guildId: string) => context.reactionRoles.resyncGuild(guildId),
onDisable: async (guildId: string) => context.reactionRoles.resyncGuild(guildId)
},
eventsEnabled: {
onEnable: async (guildId: string) => context.events.tick().catch(() => undefined)
}
});
setLoggingAdmin(context.admin);

View File

@@ -3,7 +3,7 @@ import path from 'path';
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
const required = ['DISCORD_TOKEN', 'DISCORD_CLIENT_ID', 'DISCORD_GUILD_ID', 'DATABASE_URL'] as const;
const required = ['DISCORD_TOKEN', 'DISCORD_CLIENT_ID', 'DISCORD_CLIENT_SECRET', 'DATABASE_URL'] as const;
required.forEach((key) => {
if (!process.env[key]) {
@@ -11,9 +11,16 @@ required.forEach((key) => {
}
});
const normalizeBasePath = (raw?: string) => {
if (!raw) return '';
const withSlash = raw.startsWith('/') ? raw : `/${raw}`;
return withSlash.replace(/\/+$/, '');
};
export const env = {
token: process.env.DISCORD_TOKEN ?? '',
clientId: process.env.DISCORD_CLIENT_ID ?? '',
clientSecret: process.env.DISCORD_CLIENT_SECRET ?? '',
guildId: process.env.DISCORD_GUILD_ID ?? '',
guildIds: (process.env.DISCORD_GUILD_IDS ?? process.env.DISCORD_GUILD_ID ?? '')
.split(',')
@@ -21,5 +28,12 @@ export const env = {
.filter(Boolean),
databaseUrl: process.env.DATABASE_URL ?? '',
port: Number(process.env.PORT ?? 3000),
sessionSecret: process.env.SESSION_SECRET ?? 'papo_dev_secret'
sessionSecret: process.env.SESSION_SECRET ?? 'papo_dev_secret',
supportRoleId: process.env.SUPPORT_ROLE_ID ?? '',
webBasePath: normalizeBasePath(process.env.WEB_BASE_PATH ?? '/ucp'),
publicBaseUrl: process.env.DASHBOARD_BASE_URL ? process.env.DASHBOARD_BASE_URL.replace(/\/+$/, '') : undefined,
ownerIds: (process.env.OWNER_IDS ?? process.env.OWNER_ID ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
};

View File

@@ -1,8 +1,225 @@
import { prisma } from '../database';
export interface GuildSettings {
welcomeChannelId?: string;
logChannelId?: string;
automodEnabled?: boolean;
automodConfig?: any;
statuspageEnabled?: boolean;
statuspageConfig?: any;
welcomeConfig?: {
enabled?: boolean;
channelId?: string;
embedTitle?: string;
embedDescription?: string;
embedColor?: string;
embedFooter?: string;
embedThumbnail?: string;
embedImage?: string;
embedThumbnailData?: string;
embedImageData?: string;
};
loggingConfig?: {
logChannelId?: string;
categories?: {
joinLeave?: boolean;
messageEdit?: boolean;
messageDelete?: boolean;
automodActions?: boolean;
ticketActions?: boolean;
musicEvents?: boolean;
};
};
levelingEnabled?: boolean;
ticketsEnabled?: boolean;
musicEnabled?: boolean;
dynamicVoiceEnabled?: boolean;
dynamicVoiceConfig?: {
lobbyChannelId?: string;
categoryId?: string;
userLimit?: number;
bitrate?: number;
template?: string;
};
eventsEnabled?: boolean;
supportLoginConfig?: {
panelChannelId?: string;
panelMessageId?: string;
title?: string;
description?: string;
loginLabel?: string;
logoutLabel?: string;
autoRefresh?: boolean;
};
birthdayEnabled?: boolean;
birthdayConfig?: {
enabled?: boolean;
channelId?: string;
sendHour?: number;
messageTemplate?: string;
};
reactionRolesEnabled?: boolean;
reactionRolesConfig?: any;
supportRoleId?: string;
welcomeEnabled?: boolean;
}
export const settings = new Map<string, GuildSettings>();
class SettingsStore {
private cache = new Map<string, GuildSettings>();
private applyModuleDefaults(cfg: GuildSettings): GuildSettings {
const normalized: GuildSettings = { ...cfg };
(
[
'ticketsEnabled',
'automodEnabled',
'welcomeEnabled',
'levelingEnabled',
'musicEnabled',
'dynamicVoiceEnabled',
'statuspageEnabled',
'birthdayEnabled',
'reactionRolesEnabled',
'eventsEnabled'
] as const
).forEach((key) => {
if (normalized[key] === undefined) normalized[key] = true;
});
// keep welcomeConfig flag in sync when present
if (normalized.welcomeConfig && normalized.welcomeConfig.enabled === undefined && normalized.welcomeEnabled !== undefined) {
normalized.welcomeConfig = { ...normalized.welcomeConfig, enabled: normalized.welcomeEnabled };
}
return normalized;
}
public async init() {
const rows = await prisma.guildSettings.findMany();
rows.forEach((row) => {
const cfg = {
welcomeChannelId: row.welcomeChannelId ?? undefined,
logChannelId: row.logChannelId ?? undefined,
automodEnabled: row.automodEnabled ?? undefined,
automodConfig: (row as any).automodConfig ?? undefined,
statuspageEnabled: (row as any).statuspageEnabled ?? (row as any).automodConfig?.statuspageEnabled ?? undefined,
statuspageConfig: (row as any).statuspageConfig ?? (row as any).automodConfig?.statuspageConfig ?? undefined,
welcomeEnabled: (row as any).welcomeEnabled ?? undefined,
welcomeConfig: (row as any).automodConfig?.welcomeConfig ?? undefined,
loggingConfig: (row as any).automodConfig?.loggingConfig ?? undefined,
levelingEnabled: row.levelingEnabled ?? undefined,
ticketsEnabled: row.ticketsEnabled ?? undefined,
musicEnabled: (row as any).musicEnabled ?? undefined,
dynamicVoiceEnabled: (row as any).dynamicVoiceEnabled ?? undefined,
dynamicVoiceConfig: (row as any).dynamicVoiceConfig ?? undefined,
eventsEnabled: (row as any).eventsEnabled ?? undefined,
supportLoginConfig: (row as any).supportLoginConfig ?? undefined,
birthdayEnabled: (row as any).birthdayEnabled ?? undefined,
birthdayConfig: (row as any).birthdayConfig ?? undefined,
reactionRolesEnabled: (row as any).reactionRolesEnabled ?? undefined,
reactionRolesConfig: (row as any).reactionRolesConfig ?? undefined,
supportRoleId: row.supportRoleId ?? undefined
} satisfies GuildSettings;
this.cache.set(row.guildId, this.applyModuleDefaults(cfg));
});
return this.cache;
}
public get(guildId: string) {
const cached = this.cache.get(guildId);
return cached ? this.applyModuleDefaults(cached) : undefined;
}
public all() {
return this.cache;
}
public async set(guildId: string, partial: GuildSettings) {
if ((partial as any).welcomeEnabled !== undefined) {
partial.welcomeConfig = { ...(partial.welcomeConfig ?? {}), enabled: (partial as any).welcomeEnabled };
delete (partial as any).welcomeEnabled;
}
if (partial.birthdayEnabled !== undefined) {
partial.birthdayConfig = { ...(partial.birthdayConfig ?? {}), enabled: partial.birthdayEnabled };
} else if (partial.birthdayConfig?.enabled !== undefined) {
partial.birthdayEnabled = partial.birthdayConfig.enabled;
}
if (partial.reactionRolesEnabled !== undefined) {
partial.reactionRolesConfig = { ...(partial.reactionRolesConfig ?? {}), enabled: partial.reactionRolesEnabled };
} else if (partial.reactionRolesConfig?.enabled !== undefined) {
partial.reactionRolesEnabled = partial.reactionRolesConfig.enabled;
}
const merged: GuildSettings = this.applyModuleDefaults({ ...(this.cache.get(guildId) ?? {}), ...partial });
const mergedAutomod = {
...(merged.automodConfig ?? {}),
...(partial.automodConfig ?? {}),
loggingConfig: partial.loggingConfig ?? merged.loggingConfig ?? merged.automodConfig?.loggingConfig,
welcomeConfig: partial.welcomeConfig ?? merged.welcomeConfig ?? merged.automodConfig?.welcomeConfig,
statuspageEnabled: merged.statuspageEnabled,
statuspageConfig: partial.statuspageConfig ?? merged.statuspageConfig ?? merged.automodConfig?.statuspageConfig
};
if (partial.dynamicVoiceConfig) {
merged.dynamicVoiceConfig = { ...(merged.dynamicVoiceConfig ?? {}), ...partial.dynamicVoiceConfig };
}
if (partial.supportLoginConfig) {
merged.supportLoginConfig = { ...(merged.supportLoginConfig ?? {}), ...partial.supportLoginConfig };
}
if (partial.birthdayConfig) {
merged.birthdayConfig = { ...(merged.birthdayConfig ?? {}), ...partial.birthdayConfig };
}
if (partial.reactionRolesConfig) {
merged.reactionRolesConfig = { ...(merged.reactionRolesConfig ?? {}), ...partial.reactionRolesConfig };
}
merged.automodConfig = { ...mergedAutomod, supportLoginConfig: merged.supportLoginConfig ?? mergedAutomod['supportLoginConfig'] };
merged.statuspageEnabled = mergedAutomod.statuspageEnabled;
merged.statuspageConfig = mergedAutomod.statuspageConfig;
merged.loggingConfig = mergedAutomod.loggingConfig;
merged.welcomeConfig = mergedAutomod.welcomeConfig;
this.cache.set(guildId, merged);
await prisma.guildSettings.upsert({
where: { guildId },
update: {
welcomeChannelId: merged.welcomeChannelId ?? null,
logChannelId: merged.logChannelId ?? null,
automodEnabled: merged.automodEnabled ?? null,
automodConfig: merged.automodConfig ?? null,
levelingEnabled: merged.levelingEnabled ?? null,
ticketsEnabled: merged.ticketsEnabled ?? null,
musicEnabled: merged.musicEnabled ?? null,
statuspageEnabled: merged.statuspageEnabled ?? null,
statuspageConfig: merged.statuspageConfig ?? null,
dynamicVoiceEnabled: merged.dynamicVoiceEnabled ?? null,
dynamicVoiceConfig: merged.dynamicVoiceConfig ?? null,
eventsEnabled: (merged as any).eventsEnabled ?? null,
birthdayEnabled: merged.birthdayEnabled ?? null,
birthdayConfig: merged.birthdayConfig ?? null,
reactionRolesEnabled: merged.reactionRolesEnabled ?? null,
reactionRolesConfig: merged.reactionRolesConfig ?? null,
supportRoleId: merged.supportRoleId ?? null
},
create: {
guildId,
welcomeChannelId: merged.welcomeChannelId ?? null,
logChannelId: merged.logChannelId ?? null,
automodEnabled: merged.automodEnabled ?? null,
automodConfig: merged.automodConfig ?? null,
levelingEnabled: merged.levelingEnabled ?? null,
ticketsEnabled: merged.ticketsEnabled ?? null,
musicEnabled: merged.musicEnabled ?? null,
statuspageEnabled: merged.statuspageEnabled ?? null,
statuspageConfig: merged.statuspageConfig ?? null,
dynamicVoiceEnabled: merged.dynamicVoiceEnabled ?? null,
dynamicVoiceConfig: merged.dynamicVoiceConfig ?? null,
eventsEnabled: (merged as any).eventsEnabled ?? null,
birthdayEnabled: merged.birthdayEnabled ?? null,
birthdayConfig: merged.birthdayConfig ?? null,
reactionRolesEnabled: merged.reactionRolesEnabled ?? null,
reactionRolesConfig: merged.reactionRolesConfig ?? null,
supportRoleId: merged.supportRoleId ?? null
}
});
return merged;
}
}
export const settingsStore = new SettingsStore();
// Backwards compatibility for existing imports (read-only)
export const settings = settingsStore.all();