Enhance giveaway job handling and update environment configuration

- Added support for a new `giveawayCreate` job in the bot's job processing, allowing for giveaways to be created via the WebUI.
- Introduced `runGiveawayCreateJob` function to handle the creation of giveaways, ensuring consistent behavior with existing commands.
- Updated `.env.example` to clarify the usage of `BOT_TOKEN` for both the bot and WebUI, enhancing documentation for environment variables.
- Added `bullmq` dependency in the WebUI package for job management capabilities.
This commit is contained in:
smueller
2026-07-22 15:19:04 +02:00
parent 2ef6b23136
commit 677142672f
7 changed files with 133 additions and 5 deletions

View File

@@ -21,6 +21,7 @@
"@radix-ui/react-separator": "^1.1.12",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.4",
"bullmq": "^5.34.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^16.4.7",

View File

@@ -9,6 +9,14 @@ const EnvSchema = z.object({
REDIS_URL: z.string().url(),
BOT_CLIENT_ID: z.string().min(1),
BOT_CLIENT_SECRET: z.string().min(1),
/**
* Discord bot token. The WebUI does not run a discord.js Client/gateway
* connection, but uses this token for narrow, on-demand Discord REST calls
* (e.g. building a guild backup snapshot) and to enqueue BullMQ jobs that
* the bot process picks up for actions that require a live Client
* (posting messages, restoring a backup).
*/
BOT_TOKEN: z.string().min(1),
WEBUI_URL: z.string().url().default('http://localhost:3000'),
SESSION_SECRET: z.string().min(32, 'SESSION_SECRET must be at least 32 characters long'),
SENTRY_DSN: z.string().optional(),

View File

@@ -0,0 +1,76 @@
import { Queue, QueueEvents } from 'bullmq';
import { redis } from './redis';
/**
* BullMQ queue/job names mirrored 1:1 from `apps/bot/src/queues.ts` and
* `apps/bot/src/jobs.ts`. The WebUI has no discord.js Client, so any
* dashboard action that needs to touch Discord (posting a giveaway message,
* drawing winners, restoring a guild backup) is enqueued here and picked up
* by the bot's existing workers - the WebUI never talks to Discord directly
* for these actions.
*/
export const giveawayQueueName = 'giveaways';
export const scheduleQueueName = 'schedules';
export const guildBackupQueueName = 'guild-backups';
const globalForQueues = globalThis as unknown as {
__nexumiGiveawayQueue?: Queue;
__nexumiScheduleQueue?: Queue;
__nexumiGuildBackupQueue?: Queue;
__nexumiGiveawayQueueEvents?: QueueEvents;
__nexumiGuildBackupQueueEvents?: QueueEvents;
};
export const giveawayQueue: Queue =
globalForQueues.__nexumiGiveawayQueue ?? new Queue(giveawayQueueName, { connection: redis });
export const scheduleQueue: Queue =
globalForQueues.__nexumiScheduleQueue ?? new Queue(scheduleQueueName, { connection: redis });
export const guildBackupQueue: Queue =
globalForQueues.__nexumiGuildBackupQueue ?? new Queue(guildBackupQueueName, { connection: redis });
/**
* QueueEvents instances are only needed for jobs where the dashboard needs
* to wait for the bot to finish (e.g. ending a giveaway so the winners list
* is immediately visible). They open their own Redis connection, so they are
* cached on the global object the same way as the queues themselves.
*/
export const giveawayQueueEvents: QueueEvents =
globalForQueues.__nexumiGiveawayQueueEvents ??
new QueueEvents(giveawayQueueName, { connection: redis.duplicate() });
export const guildBackupQueueEvents: QueueEvents =
globalForQueues.__nexumiGuildBackupQueueEvents ??
new QueueEvents(guildBackupQueueName, { connection: redis.duplicate() });
globalForQueues.__nexumiGiveawayQueue = giveawayQueue;
globalForQueues.__nexumiScheduleQueue = scheduleQueue;
globalForQueues.__nexumiGuildBackupQueue = guildBackupQueue;
globalForQueues.__nexumiGiveawayQueueEvents = giveawayQueueEvents;
globalForQueues.__nexumiGuildBackupQueueEvents = guildBackupQueueEvents;
const DEFAULT_WAIT_TIMEOUT_MS = 15_000;
/**
* Adds a job and waits (bounded) for the bot worker to finish it, so the
* dashboard can immediately reflect the result instead of polling. Falls
* back to "not confirmed" (does not throw) on timeout - the DB row created
* by the worker is still the source of truth and will show up on next load.
*/
export async function addJobAndAwait<T = unknown>(
queue: Queue,
queueEvents: QueueEvents,
jobName: string,
data: unknown,
options: Parameters<Queue['add']>[2] = {},
timeoutMs = DEFAULT_WAIT_TIMEOUT_MS
): Promise<{ confirmed: boolean; result: T | null }> {
const job = await queue.add(jobName, data, options);
try {
const result = (await job.waitUntilFinished(queueEvents, timeoutMs)) as T;
return { confirmed: true, result };
} catch {
return { confirmed: false, result: null };
}
}