Files
Nexumi/apps/webui/src/lib/module-configs/scheduler.ts
TheOnlyMace 4e135bcf43 Enhance component message handling and introduce new features
- 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.
2026-07-22 22:55:37 +02:00

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;
}