[deploy] add register module backend
Some checks failed
Deploy Discord Bot / deploy (push) Failing after 20s

This commit is contained in:
Pascal Prießnitz
2025-12-03 18:08:08 +01:00
parent b672e2c6a2
commit a8b4713ffe
9 changed files with 497 additions and 10 deletions

View File

@@ -26,8 +26,7 @@ model GuildSettings {
birthdayConfig Json? birthdayConfig Json?
reactionRolesEnabled Boolean? reactionRolesEnabled Boolean?
reactionRolesConfig Json? reactionRolesConfig Json?
eventsEnabled Boolean? eventsEnabled Boolean?\n registerEnabled Boolean?\n registerConfig Json?\n supportRoleId String?
supportRoleId String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
createdAt DateTime @default(now()) createdAt DateTime @default(now())
} }
@@ -161,3 +160,55 @@ model EventSignup {
@@unique([eventId, userId]) @@unique([eventId, userId])
@@index([guildId, eventId]) @@index([guildId, eventId])
} }
model RegisterForm {
id String @id @default(cuid())
guildId String
name String
description String?
reviewChannelId String?
notifyRoleIds String[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
fields RegisterFormField[]
@@index([guildId, isActive])
}
model RegisterFormField {
id String @id @default(cuid())
formId String
label String
type String
required Boolean @default(false)
"order" Int @default(0)
form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade)
}
model RegisterApplication {
id String @id @default(cuid())
guildId String
userId String
formId String
status String @default("pending")
reviewedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
answers RegisterApplicationAnswer[]
form RegisterForm @relation(fields: [formId], references: [id])
@@index([guildId, formId, status])
}
model RegisterApplicationAnswer {
id String @id @default(cuid())
applicationId String
fieldId String
value String
application RegisterApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade)
}

View File

@@ -14,6 +14,7 @@ import { ReactionRoleService } from '../services/reactionRoleService';
import { EventService } from '../services/eventService'; import { EventService } from '../services/eventService';
import { TicketAutomationService } from '../services/ticketAutomationService'; import { TicketAutomationService } from '../services/ticketAutomationService';
import { KnowledgeBaseService } from '../services/knowledgeBaseService'; import { KnowledgeBaseService } from '../services/knowledgeBaseService';
import { RegisterService } from '../services/registerService';
export const context = { export const context = {
client: null as Client | null, client: null as Client | null,
@@ -31,7 +32,8 @@ export const context = {
reactionRoles: new ReactionRoleService(), reactionRoles: new ReactionRoleService(),
events: new EventService(), events: new EventService(),
ticketAutomation: new TicketAutomationService(), ticketAutomation: new TicketAutomationService(),
knowledgeBase: new KnowledgeBaseService() knowledgeBase: new KnowledgeBaseService(),
register: new RegisterService()
}; };
context.modules.setHooks({ context.modules.setHooks({

View File

@@ -60,6 +60,11 @@ export interface GuildSettings {
}; };
reactionRolesEnabled?: boolean; reactionRolesEnabled?: boolean;
reactionRolesConfig?: any; reactionRolesConfig?: any;
registerEnabled?: boolean;
registerConfig?: {
reviewChannelId?: string;
notifyRoleIds?: string[];
};
supportRoleId?: string; supportRoleId?: string;
welcomeEnabled?: boolean; welcomeEnabled?: boolean;
} }
@@ -80,7 +85,8 @@ class SettingsStore {
'statuspageEnabled', 'statuspageEnabled',
'birthdayEnabled', 'birthdayEnabled',
'reactionRolesEnabled', 'reactionRolesEnabled',
'eventsEnabled' 'eventsEnabled',
'registerEnabled'
] as const ] as const
).forEach((key) => { ).forEach((key) => {
if (normalized[key] === undefined) normalized[key] = true; if (normalized[key] === undefined) normalized[key] = true;
@@ -116,6 +122,8 @@ class SettingsStore {
birthdayConfig: (row as any).birthdayConfig ?? undefined, birthdayConfig: (row as any).birthdayConfig ?? undefined,
reactionRolesEnabled: (row as any).reactionRolesEnabled ?? undefined, reactionRolesEnabled: (row as any).reactionRolesEnabled ?? undefined,
reactionRolesConfig: (row as any).reactionRolesConfig ?? undefined, reactionRolesConfig: (row as any).reactionRolesConfig ?? undefined,
registerEnabled: (row as any).registerEnabled ?? undefined,
registerConfig: (row as any).registerConfig ?? undefined,
supportRoleId: row.supportRoleId ?? undefined supportRoleId: row.supportRoleId ?? undefined
} satisfies GuildSettings; } satisfies GuildSettings;
this.cache.set(row.guildId, this.applyModuleDefaults(cfg)); this.cache.set(row.guildId, this.applyModuleDefaults(cfg));
@@ -147,6 +155,9 @@ class SettingsStore {
} else if (partial.reactionRolesConfig?.enabled !== undefined) { } else if (partial.reactionRolesConfig?.enabled !== undefined) {
partial.reactionRolesEnabled = partial.reactionRolesConfig.enabled; partial.reactionRolesEnabled = partial.reactionRolesConfig.enabled;
} }
if (!partial.registerConfig && partial.registerEnabled !== undefined) {
partial.registerConfig = { ...(partial.registerConfig ?? {}) };
}
const merged: GuildSettings = this.applyModuleDefaults({ ...(this.cache.get(guildId) ?? {}), ...partial }); const merged: GuildSettings = this.applyModuleDefaults({ ...(this.cache.get(guildId) ?? {}), ...partial });
const mergedAutomod = { const mergedAutomod = {
...(merged.automodConfig ?? {}), ...(merged.automodConfig ?? {}),
@@ -193,6 +204,8 @@ class SettingsStore {
birthdayConfig: merged.birthdayConfig ?? null, birthdayConfig: merged.birthdayConfig ?? null,
reactionRolesEnabled: merged.reactionRolesEnabled ?? null, reactionRolesEnabled: merged.reactionRolesEnabled ?? null,
reactionRolesConfig: merged.reactionRolesConfig ?? null, reactionRolesConfig: merged.reactionRolesConfig ?? null,
registerEnabled: merged.registerEnabled ?? null,
registerConfig: merged.registerConfig ?? null,
supportRoleId: merged.supportRoleId ?? null supportRoleId: merged.supportRoleId ?? null
}, },
create: { create: {
@@ -213,6 +226,8 @@ class SettingsStore {
birthdayConfig: merged.birthdayConfig ?? null, birthdayConfig: merged.birthdayConfig ?? null,
reactionRolesEnabled: merged.reactionRolesEnabled ?? null, reactionRolesEnabled: merged.reactionRolesEnabled ?? null,
reactionRolesConfig: merged.reactionRolesConfig ?? null, reactionRolesConfig: merged.reactionRolesConfig ?? null,
registerEnabled: merged.registerEnabled ?? null,
registerConfig: merged.registerConfig ?? null,
supportRoleId: merged.supportRoleId ?? null supportRoleId: merged.supportRoleId ?? null
} }
}); });

View File

@@ -26,6 +26,8 @@ model GuildSettings {
reactionRolesEnabled Boolean? reactionRolesEnabled Boolean?
reactionRolesConfig Json? reactionRolesConfig Json?
eventsEnabled Boolean? eventsEnabled Boolean?
registerEnabled Boolean?
registerConfig Json?
supportRoleId String? supportRoleId String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -160,3 +162,54 @@ model EventSignup {
@@unique([eventId, userId]) @@unique([eventId, userId])
@@index([guildId, eventId]) @@index([guildId, eventId])
} }
model RegisterForm {
id String @id @default(cuid())
guildId String
name String
description String?
reviewChannelId String?
notifyRoleIds String[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
fields RegisterFormField[]
@@index([guildId, isActive])
}
model RegisterFormField {
id String @id @default(cuid())
formId String
label String
type String
required Boolean @default(false)
"order" Int @default(0)
form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade)
}
model RegisterApplication {
id String @id @default(cuid())
guildId String
userId String
formId String
status String @default("pending")
reviewedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
answers RegisterApplicationAnswer[]
form RegisterForm @relation(fields: [formId], references: [id])
@@index([guildId, formId, status])
}
model RegisterApplicationAnswer {
id String @id @default(cuid())
applicationId String
fieldId String
value String
application RegisterApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade)
}

View File

@@ -20,8 +20,19 @@ const event: EventHandler = {
return; return;
} }
} }
if (interaction.customId.startsWith('register:')) {
await context.register.handleButton(interaction as any);
return;
}
await context.tickets.handleButton(interaction); await context.tickets.handleButton(interaction);
} }
if (interaction.isModalSubmit()) {
if (interaction.customId.startsWith('register:submit:')) {
await context.register.handleModal(interaction as any);
return;
}
}
} }
}; };

View File

@@ -34,6 +34,7 @@ async function bootstrap() {
context.reactionRoles.setClient(client); context.reactionRoles.setClient(client);
context.events.setClient(client); context.events.setClient(client);
context.events.startScheduler(); context.events.startScheduler();
context.register.setClient(client);
await context.reactionRoles.loadCache(); await context.reactionRoles.loadCache();
logger.setSink((entry) => context.admin.pushLog(entry)); logger.setSink((entry) => context.admin.pushLog(entry));
for (const gid of settingsStore.all().keys()) { for (const gid of settingsStore.all().keys()) {

View File

@@ -10,7 +10,8 @@ export type ModuleKey =
| 'statuspageEnabled' | 'statuspageEnabled'
| 'birthdayEnabled' | 'birthdayEnabled'
| 'reactionRolesEnabled' | 'reactionRolesEnabled'
| 'eventsEnabled'; | 'eventsEnabled'
| 'registerEnabled';
export interface GuildModuleState { export interface GuildModuleState {
key: ModuleKey; key: ModuleKey;
@@ -29,7 +30,8 @@ const MODULES: Record<ModuleKey, { name: string; description: string }> = {
statuspageEnabled: { name: 'Statuspage', description: 'Service Checks, Uptime und Status-Embed.' }, statuspageEnabled: { name: 'Statuspage', description: 'Service Checks, Uptime und Status-Embed.' },
birthdayEnabled: { name: 'Birthday', description: 'Geburtstage speichern und Glueckwuensche senden.' }, birthdayEnabled: { name: 'Birthday', description: 'Geburtstage speichern und Glueckwuensche senden.' },
reactionRolesEnabled: { name: 'Reaction Roles', description: 'Reaktionen vergeben und entfernen Rollen.' }, reactionRolesEnabled: { name: 'Reaction Roles', description: 'Reaktionen vergeben und entfernen Rollen.' },
eventsEnabled: { name: 'Termine', description: 'Events planen, erinnern und Anmeldungen sammeln.' } eventsEnabled: { name: 'Termine', description: 'Events planen, erinnern und Anmeldungen sammeln.' },
registerEnabled: { name: 'Register', description: 'Registrierungsformulare und Bewerbungen.' }
}; };
export class BotModuleService { export class BotModuleService {

View File

@@ -0,0 +1,256 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
Client,
EmbedBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ModalSubmitInteraction,
GuildMember
} from 'discord.js';
import { prisma } from '../database';
import { settingsStore } from '../config/state';
import { env } from '../config/env';
export class RegisterService {
private client: Client | null = null;
public setClient(client: Client) {
this.client = client;
}
public async listForms(guildId: string) {
return prisma.registerForm.findMany({ where: { guildId }, include: { fields: { orderBy: { order: 'asc' } } }, orderBy: { createdAt: 'desc' } });
}
public async saveForm(form: {
id?: string;
guildId: string;
name: string;
description?: string;
reviewChannelId?: string;
notifyRoleIds?: string[];
isActive?: boolean;
fields: { id?: string; label: string; type: string; required?: boolean; order?: number }[];
}) {
const notify = (form.notifyRoleIds || []).filter(Boolean);
if (form.id) {
await prisma.registerForm.update({
where: { id: form.id },
data: {
name: form.name,
description: form.description,
reviewChannelId: form.reviewChannelId,
notifyRoleIds: notify,
isActive: form.isActive ?? true
}
});
await prisma.registerFormField.deleteMany({ where: { formId: form.id } });
await prisma.registerFormField.createMany({
data: (form.fields || []).map((f, idx) => ({
formId: form.id as string,
label: f.label,
type: f.type,
required: f.required ?? false,
order: f.order ?? idx
}))
});
return prisma.registerForm.findUnique({ where: { id: form.id }, include: { fields: { orderBy: { order: 'asc' } } } });
}
const created = await prisma.registerForm.create({
data: {
guildId: form.guildId,
name: form.name,
description: form.description,
reviewChannelId: form.reviewChannelId,
notifyRoleIds: notify,
isActive: form.isActive ?? true,
fields: {
create: (form.fields || []).map((f, idx) => ({
label: f.label,
type: f.type,
required: f.required ?? false,
order: f.order ?? idx
}))
}
},
include: { fields: { orderBy: { order: 'asc' } } }
});
return created;
}
public async deleteForm(guildId: string, id: string) {
const form = await prisma.registerForm.findFirst({ where: { id, guildId } });
if (!form) return false;
await prisma.registerFormField.deleteMany({ where: { formId: id } });
await prisma.registerForm.delete({ where: { id } });
return true;
}
public async sendPanel(guildId: string, formId: string, channelId?: string, message?: string) {
if (!this.client) return null;
const form = await prisma.registerForm.findFirst({ where: { id: formId, guildId }, include: { fields: true } });
if (!form) return null;
const targetChannelId = channelId || form.reviewChannelId || settingsStore.get(guildId)?.registerConfig?.reviewChannelId;
if (!targetChannelId) return null;
const guild = this.client.guilds.cache.get(guildId) ?? (await this.client.guilds.fetch(guildId).catch(() => null));
if (!guild) return null;
const channel = await guild.channels.fetch(targetChannelId).catch(() => null);
if (!channel || !channel.isTextBased()) return null;
const embed = new EmbedBuilder()
.setTitle(form.name)
.setDescription(message || 'Klicke auf Registrieren, um das Formular auszufüllen.')
.setColor(0xf97316);
const btn = new ButtonBuilder().setCustomId(`register:form:${form.id}`).setLabel('Registrieren').setStyle(ButtonStyle.Primary);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(btn);
const sent = await (channel as any).send({ embeds: [embed], components: [row] });
return sent.id;
}
public async handleButton(interaction: ButtonInteraction) {
if (interaction.customId.startsWith('register:form:')) {
const formId = interaction.customId.split(':')[2];
const form = await prisma.registerForm.findFirst({ where: { id: formId }, include: { fields: { orderBy: { order: 'asc' } } } });
if (!form) return interaction.reply({ content: 'Formular nicht gefunden.', ephemeral: true });
const modal = new ModalBuilder().setTitle(form.name).setCustomId(`register:submit:${form.id}`);
const components: any[] = [];
form.fields.slice(0, 5).forEach((f) => {
const input = new TextInputBuilder()
.setCustomId(f.id)
.setLabel(f.label.slice(0, 45) || 'Feld')
.setStyle(f.type === 'longText' ? TextInputStyle.Paragraph : TextInputStyle.Short)
.setRequired(f.required ?? false);
components.push(new ActionRowBuilder<TextInputBuilder>().addComponents(input));
});
modal.addComponents(components as any);
await interaction.showModal(modal);
return;
}
if (interaction.customId.startsWith('register:review:')) {
const [, , action, appId] = interaction.customId.split(':');
const app = await prisma.registerApplication.findUnique({ where: { id: appId }, include: { form: true } });
if (!app) {
await interaction.reply({ content: 'Antrag nicht gefunden.', ephemeral: true });
return;
}
const statusMap: any = { accept: 'accepted', invite: 'invited', reject: 'rejected' };
const newStatus = statusMap[action] || 'pending';
const updated = await prisma.registerApplication.update({
where: { id: appId },
data: { status: newStatus, reviewedBy: interaction.user.id }
});
await this.updateReviewMessage(interaction, updated);
const user = await this.client?.users.fetch(app.userId).catch(() => null);
if (user) {
const msg =
newStatus === 'accepted'
? 'Deine Registrierung wurde akzeptiert.'
: newStatus === 'invited'
? 'Bitte komm für ein Gespräch vorbei.'
: 'Deine Registrierung wurde abgelehnt.';
user.send(msg).catch(() => undefined);
}
await interaction.reply({ content: 'Status aktualisiert.', ephemeral: true });
return;
}
}
public async handleModal(interaction: ModalSubmitInteraction) {
if (!interaction.customId.startsWith('register:submit:')) return;
const formId = interaction.customId.split(':')[2];
const form = await prisma.registerForm.findFirst({
where: { id: formId },
include: { fields: { orderBy: { order: 'asc' } } }
});
if (!form) {
await interaction.reply({ content: 'Formular nicht gefunden.', ephemeral: true });
return;
}
const answersPayload = form.fields.map((f) => ({
fieldId: f.id,
value: interaction.fields.getTextInputValue(f.id) || ''
}));
const app = await prisma.registerApplication.create({
data: {
guildId: interaction.guildId ?? '',
userId: interaction.user.id,
formId: form.id,
status: 'pending',
answers: {
create: answersPayload
}
},
include: { answers: true }
});
await interaction.reply({ content: 'Registrierung gesendet.', ephemeral: true });
await this.postReviewEmbed(form, app, interaction.user.id, interaction.guildId || '');
}
private async postReviewEmbed(form: any, app: any, userId: string, guildId: string) {
if (!this.client) return;
const cfg = settingsStore.get(guildId);
const channelId = form.reviewChannelId || cfg?.registerConfig?.reviewChannelId;
if (!channelId) return;
const guild = this.client.guilds.cache.get(guildId) ?? (await this.client.guilds.fetch(guildId).catch(() => null));
if (!guild) return;
const channel = await guild.channels.fetch(channelId).catch(() => null);
if (!channel || !channel.isTextBased()) return;
const member = await guild.members.fetch(userId).catch(() => null);
const fields = await prisma.registerFormField.findMany({ where: { formId: form.id }, orderBy: { order: 'asc' } });
const answers = await prisma.registerApplicationAnswer.findMany({ where: { applicationId: app.id } });
const embed = new EmbedBuilder()
.setTitle(`Registrierung: ${form.name}`)
.setDescription(form.description || '')
.setColor(0xf97316)
.addFields(
...fields.map((f) => ({
name: f.label,
value: answers.find((a) => a.fieldId === f.id)?.value || '-',
inline: false
}))
)
.setFooter({ text: `Status: ${app.status}` })
.setTimestamp(new Date(app.createdAt));
if (member) embed.setAuthor({ name: member.user.tag, iconURL: member.user.displayAvatarURL() });
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId(`register:review:accept:${app.id}`).setLabel('Akzeptieren').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId(`register:review:invite:${app.id}`).setLabel('Gespräch einladen').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId(`register:review:reject:${app.id}`).setLabel('Ablehnen').setStyle(ButtonStyle.Danger)
);
const notifyRoles = form.notifyRoleIds || cfg?.registerConfig?.notifyRoleIds || [];
const content = notifyRoles.length ? notifyRoles.map((id: string) => `<@&${id}>`).join(' ') : null;
await (channel as any).send({ content: content || undefined, embeds: [embed], components: [row] });
}
private async updateReviewMessage(interaction: ButtonInteraction, app: any) {
if (!interaction.message || !interaction.message.editable) return;
const embed = (interaction.message.embeds?.[0] as any) ?? null;
if (embed) {
embed.data = { ...(embed.data || {}), footer: { text: `Status: ${app.status} | Reviewer: ${interaction.user.tag}` } };
}
const components = interaction.message.components;
await interaction.message.edit({ embeds: embed ? [embed] : interaction.message.embeds, components });
}
public async listApplications(guildId: string, status?: string, formId?: string) {
const where: any = { guildId };
if (status) where.status = status;
if (formId) where.formId = formId;
return prisma.registerApplication.findMany({
where,
orderBy: { createdAt: 'desc' },
include: { form: true }
});
}
public async getApplication(id: string) {
return prisma.registerApplication.findUnique({
where: { id },
include: { form: true, answers: true }
});
}
}

View File

@@ -543,6 +543,82 @@ router.delete('/reactionroles/:id', requireAuth, async (req, res) => {
} }
}); });
router.get('/register/forms', requireAuth, async (req, res) => {
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
if (!guildId) return res.status(400).json({ error: 'guildId required' });
const forms = await context.register.listForms(guildId);
res.json({ forms });
});
router.post('/register/forms', requireAuth, async (req, res) => {
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
if (!guildId) return res.status(400).json({ error: 'guildId required' });
const data = req.body || {};
const form = await context.register.saveForm({
guildId,
name: data.name || 'Formular',
description: data.description || '',
reviewChannelId: data.reviewChannelId || undefined,
notifyRoleIds: Array.isArray(data.notifyRoleIds) ? data.notifyRoleIds : typeof data.notifyRoleIds === 'string' ? data.notifyRoleIds.split(',').map((s: string) => s.trim()).filter(Boolean) : [],
isActive: data.isActive !== false,
fields: Array.isArray(data.fields) ? data.fields : []
});
res.json({ form });
});
router.put('/register/forms/:id', requireAuth, async (req, res) => {
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
const id = req.params.id;
if (!guildId) return res.status(400).json({ error: 'guildId required' });
const data = req.body || {};
const form = await context.register.saveForm({
id,
guildId,
name: data.name || 'Formular',
description: data.description || '',
reviewChannelId: data.reviewChannelId || undefined,
notifyRoleIds: Array.isArray(data.notifyRoleIds) ? data.notifyRoleIds : typeof data.notifyRoleIds === 'string' ? data.notifyRoleIds.split(',').map((s: string) => s.trim()).filter(Boolean) : [],
isActive: data.isActive !== false,
fields: Array.isArray(data.fields) ? data.fields : []
});
res.json({ form });
});
router.delete('/register/forms/:id', requireAuth, async (req, res) => {
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
const id = req.params.id;
if (!guildId) return res.status(400).json({ error: 'guildId required' });
const ok = await context.register.deleteForm(guildId, id);
if (!ok) return res.status(404).json({ error: 'not found' });
res.json({ ok: true });
});
router.post('/register/forms/:id/panel', requireAuth, async (req, res) => {
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
const id = req.params.id;
if (!guildId) return res.status(400).json({ error: 'guildId required' });
const channelId = typeof req.body.channelId === 'string' ? req.body.channelId : undefined;
const message = typeof req.body.message === 'string' ? req.body.message : undefined;
const msgId = await context.register.sendPanel(guildId, id, channelId, message);
if (!msgId) return res.status(400).json({ error: 'panel failed' });
res.json({ ok: true, messageId: msgId });
});
router.get('/register/apps', requireAuth, async (req, res) => {
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
const status = typeof req.query.status === 'string' ? req.query.status : undefined;
const formId = typeof req.query.formId === 'string' ? req.query.formId : undefined;
if (!guildId) return res.status(400).json({ error: 'guildId required' });
const apps = await context.register.listApplications(guildId, status, formId);
res.json({ applications: apps });
});
router.get('/register/apps/:id', requireAuth, async (req, res) => {
const app = await context.register.getApplication(req.params.id);
if (!app) return res.status(404).json({ error: 'not found' });
res.json({ application: app });
});
router.get('/automations', requireAuth, async (req, res) => { router.get('/automations', requireAuth, async (req, res) => {
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined; const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
if (!guildId) return res.status(400).json({ error: 'guildId required' }); if (!guildId) return res.status(400).json({ error: 'guildId required' });
@@ -700,9 +776,11 @@ router.post('/settings', requireAuth, async (req, res) => {
eventsEnabled, eventsEnabled,
birthdayEnabled, birthdayEnabled,
birthdayConfig, birthdayConfig,
reactionRolesEnabled, reactionRolesEnabled,
reactionRolesConfig reactionRolesConfig,
} = req.body; registerEnabled,
registerConfig
} = req.body;
if (!guildId) return res.status(400).json({ error: 'guildId required' }); if (!guildId) return res.status(400).json({ error: 'guildId required' });
const normalizeArray = (val: any) => const normalizeArray = (val: any) =>
Array.isArray(val) Array.isArray(val)
@@ -798,6 +876,22 @@ router.post('/settings', requireAuth, async (req, res) => {
channelId: reactionRolesConfig?.channelId ?? existingReactionRoles.channelId channelId: reactionRolesConfig?.channelId ?? existingReactionRoles.channelId
}; };
const parsedRegister = {
enabled:
registerConfig?.enabled ??
(typeof registerEnabled === 'string' ? registerEnabled === 'true' : registerEnabled ?? (current as any).registerEnabled),
reviewChannelId: registerConfig?.reviewChannelId ?? (current as any).registerConfig?.reviewChannelId,
notifyRoleIds:
Array.isArray(registerConfig?.notifyRoleIds) && registerConfig?.notifyRoleIds.length
? registerConfig.notifyRoleIds
: typeof registerConfig?.notifyRoleIds === 'string'
? registerConfig.notifyRoleIds
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
: (current as any).registerConfig?.notifyRoleIds || []
};
const updated = await settingsStore.set(guildId, { const updated = await settingsStore.set(guildId, {
welcomeChannelId: welcomeChannelId ?? undefined, welcomeChannelId: welcomeChannelId ?? undefined,
logChannelId: logChannelId ?? undefined, logChannelId: logChannelId ?? undefined,
@@ -817,7 +911,9 @@ router.post('/settings', requireAuth, async (req, res) => {
birthdayEnabled: parsedBirthday.enabled, birthdayEnabled: parsedBirthday.enabled,
birthdayConfig: parsedBirthday, birthdayConfig: parsedBirthday,
reactionRolesEnabled: parsedReactionRoles.enabled, reactionRolesEnabled: parsedReactionRoles.enabled,
reactionRolesConfig: parsedReactionRoles reactionRolesConfig: parsedReactionRoles,
registerEnabled: parsedRegister.enabled,
registerConfig: parsedRegister
}); });
// Live update logging target // Live update logging target
context.logging = new LoggingService(updated.logChannelId); context.logging = new LoggingService(updated.logChannelId);