- Added `ComponentMessageBinding` model to manage message components in the database. - Updated `WelcomeConfig`, `Tag`, and `ScheduledMessage` models to support new component fields. - Integrated component handling in various modules, including Scheduler and Tags, to improve message customization. - Enhanced user experience by implementing new commands for creating and managing component messages in the utility module. - Updated localization files to reflect new component-related features and improve user guidance.
145 lines
4.5 KiB
TypeScript
145 lines
4.5 KiB
TypeScript
import {
|
|
MessageComponentsV2Schema,
|
|
WelcomeEmbedSchema,
|
|
type MessageComponentsV2,
|
|
type ScheduledMessageDashboard,
|
|
type ScheduledMessageDashboardCreate,
|
|
type WelcomeEmbed
|
|
} from '@nexumi/shared';
|
|
import { Prisma, type ScheduledMessage } from '@prisma/client';
|
|
import { prisma } from '../prisma';
|
|
import { getScheduleQueue } from '../queues';
|
|
|
|
export class SchedulerValidationError extends Error {
|
|
constructor(public readonly code: string) {
|
|
super(code);
|
|
}
|
|
}
|
|
|
|
function parseEmbed(raw: unknown): WelcomeEmbed | null {
|
|
const parsed = WelcomeEmbedSchema.safeParse(raw);
|
|
return parsed.success ? parsed.data : null;
|
|
}
|
|
|
|
function parseComponents(raw: unknown): MessageComponentsV2 | null {
|
|
const parsed = MessageComponentsV2Schema.safeParse(raw);
|
|
return parsed.success ? parsed.data : null;
|
|
}
|
|
|
|
function toDashboard(schedule: ScheduledMessage): ScheduledMessageDashboard {
|
|
return {
|
|
id: schedule.id,
|
|
channelId: schedule.channelId,
|
|
creatorId: schedule.creatorId,
|
|
content: schedule.content,
|
|
embed: parseEmbed(schedule.embed),
|
|
components: parseComponents(schedule.components),
|
|
rolePingId: schedule.rolePingId,
|
|
cron: schedule.cron,
|
|
runAt: schedule.runAt?.toISOString() ?? null,
|
|
enabled: schedule.enabled,
|
|
createdAt: schedule.createdAt.toISOString()
|
|
};
|
|
}
|
|
|
|
export async function listScheduledMessagesDashboard(guildId: string): Promise<ScheduledMessageDashboard[]> {
|
|
const schedules = await prisma.scheduledMessage.findMany({
|
|
where: { guildId, enabled: true },
|
|
orderBy: { createdAt: 'asc' },
|
|
take: 50
|
|
});
|
|
return schedules.map(toDashboard);
|
|
}
|
|
|
|
function isValidCron(pattern: string): boolean {
|
|
const parts = pattern.trim().split(/\s+/);
|
|
return parts.length >= 5 && parts.length <= 6;
|
|
}
|
|
|
|
/**
|
|
* Mirrors `apps/bot/src/modules/scheduler/service.ts#createScheduledMessage`
|
|
* job scheduling exactly (same queue, same `scheduleSend` job name, same
|
|
* `schedule-{id}` job id convention) so schedules created from the
|
|
* dashboard are picked up by the bot's existing `scheduleWorker` without any
|
|
* changes on the bot side. Channel permission checks are skipped here since
|
|
* the WebUI has no discord.js Client - the bot validates the channel again
|
|
* when the job fires and disables the schedule if it is unusable.
|
|
*/
|
|
export async function createScheduledMessageDashboard(
|
|
guildId: string,
|
|
creatorId: string,
|
|
input: ScheduledMessageDashboardCreate
|
|
): Promise<ScheduledMessageDashboard> {
|
|
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
|
|
|
const cron = input.cron?.trim() || null;
|
|
if (cron && !isValidCron(cron)) {
|
|
throw new SchedulerValidationError('invalid_cron');
|
|
}
|
|
|
|
const runAt = !cron && input.runAt ? new Date(input.runAt) : null;
|
|
if (!cron && !runAt) {
|
|
throw new SchedulerValidationError('missing_schedule_time');
|
|
}
|
|
|
|
const schedule = await prisma.scheduledMessage.create({
|
|
data: {
|
|
guildId,
|
|
channelId: input.channelId,
|
|
creatorId,
|
|
content: input.content?.trim() || null,
|
|
embed:
|
|
input.embed === undefined || input.embed === null
|
|
? Prisma.JsonNull
|
|
: (input.embed as Prisma.InputJsonValue),
|
|
components:
|
|
input.components === undefined || input.components === null
|
|
? Prisma.JsonNull
|
|
: (input.components as Prisma.InputJsonValue),
|
|
rolePingId: input.rolePingId || null,
|
|
cron,
|
|
runAt
|
|
}
|
|
});
|
|
|
|
const jobOptions: {
|
|
jobId: string;
|
|
removeOnComplete: boolean;
|
|
removeOnFail: number;
|
|
delay?: number;
|
|
repeat?: { pattern: string };
|
|
} = {
|
|
jobId: `schedule-${schedule.id}`,
|
|
removeOnComplete: true,
|
|
removeOnFail: 50
|
|
};
|
|
if (cron) {
|
|
jobOptions.repeat = { pattern: cron };
|
|
} else if (runAt) {
|
|
jobOptions.delay = Math.max(0, runAt.getTime() - Date.now());
|
|
}
|
|
|
|
const job = await getScheduleQueue().add('scheduleSend', { scheduleId: schedule.id }, jobOptions);
|
|
const updated = await prisma.scheduledMessage.update({
|
|
where: { id: schedule.id },
|
|
data: { jobId: String(job.id) }
|
|
});
|
|
|
|
return toDashboard(updated);
|
|
}
|
|
|
|
export async function deleteScheduledMessageDashboard(guildId: string, scheduleId: string): Promise<boolean> {
|
|
const existing = await prisma.scheduledMessage.findFirst({ where: { id: scheduleId, guildId } });
|
|
if (!existing) {
|
|
return false;
|
|
}
|
|
|
|
if (existing.jobId) {
|
|
const job = await getScheduleQueue().getJob(existing.jobId);
|
|
await job?.remove();
|
|
}
|
|
|
|
await prisma.scheduledMessage.delete({ where: { id: scheduleId } });
|
|
return true;
|
|
}
|