Refactor giveaway job processing and enhance environment documentation

- Improved handling of the `giveawayCreate` job in the bot's job processing for better consistency.
- Updated `.env.example` to clarify the usage of `BOT_TOKEN` for both the bot and WebUI.
- Added `bullmq` dependency to the WebUI package for enhanced job management capabilities.
This commit is contained in:
smueller
2026-07-22 15:31:03 +02:00
parent 429f974bfc
commit 387fbf11da
32 changed files with 2620 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
import type { ScheduledMessageDashboard, ScheduledMessageDashboardCreate } from '@nexumi/shared';
import type { ScheduledMessage } from '@prisma/client';
import { prisma } from '../prisma';
import { scheduleQueue } from '../queues';
export class SchedulerValidationError extends Error {}
function isValidCron(pattern: string): boolean {
const parts = pattern.trim().split(/\s+/);
return parts.length >= 5 && parts.length <= 6;
}
function toDashboard(schedule: ScheduledMessage): ScheduledMessageDashboard {
return {
id: schedule.id,
channelId: schedule.channelId,
creatorId: schedule.creatorId,
content: schedule.content,
rolePingId: schedule.rolePingId,
cron: schedule.cron,
runAt: schedule.runAt?.toISOString() ?? null,
enabled: schedule.enabled,
createdAt: schedule.createdAt.toISOString()
};
}
export async function listSchedules(guildId: string): Promise<ScheduledMessageDashboard[]> {
const schedules = await prisma.scheduledMessage.findMany({
where: { guildId, enabled: true },
orderBy: { createdAt: 'desc' }
});
return schedules.map(toDashboard);
}
/**
* Creates a scheduled message and enqueues the exact `scheduleSend` BullMQ
* job the bot's `/schedule create` command would enqueue (same queue, job
* name and delay/repeat semantics) - see
* `apps/bot/src/modules/scheduler/service.ts#enqueueScheduleJob`. The bot's
* existing `scheduleQueueName` worker sends the message, so no bot-side
* changes are required.
*/
export async function createSchedule(
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 expression (expects 5-6 space separated fields)');
}
const runAt = !cron && input.runAt ? new Date(input.runAt) : null;
if (!cron && runAt && runAt.getTime() <= Date.now()) {
throw new SchedulerValidationError('runAt must be in the future');
}
const schedule = await prisma.scheduledMessage.create({
data: {
guildId,
channelId: input.channelId,
creatorId,
content: input.content?.trim() || null,
rolePingId: input.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 scheduleQueue.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 deleteSchedule(guildId: string, scheduleId: string): Promise<boolean> {
const schedule = await prisma.scheduledMessage.findFirst({ where: { id: scheduleId, guildId } });
if (!schedule) {
return false;
}
if (schedule.jobId) {
const job = await scheduleQueue.getJob(schedule.jobId);
await job?.remove();
}
await prisma.scheduledMessage.delete({ where: { id: scheduleId } });
return true;
}