// Recurring + one-off scheduled channel messages. Recurring rows are each // registered as their own node-cron task; one-off rows are picked up by a // once-a-minute sweep that checks for anything due and marks it sent so it // never reposts. Needs a live discord.js Client to actually send — wired up // by discordManager.js (start() once the client is ready, stop() alongside // client teardown). const cron = require('node-cron') const scheduledMessages = require('../model/scheduledMessages') const createLogger = require('../utils/logger') const log = createLogger('scheduler') let discordClient = null const recurringTasks = new Map() // id -> node-cron ScheduledTask let sweepTask = null async function sendToChannel(channelId, content) { try { const channel = await discordClient.channels.fetch(channelId) if (!channel || !channel.isTextBased()) { log.warn('scheduled message skipped — channel missing or not text-based', { channelId }) return } await channel.send({ content }) log.info('sent scheduled message', { channelId }) } catch (err) { log.warn('failed to send scheduled message', { channelId, message: err.message }) } } async function loadRecurring() { for (const task of recurringTasks.values()) task.stop() recurringTasks.clear() const rows = await scheduledMessages.listEnabledRecurring() for (const row of rows) { if (!cron.validate(row.cron_expression)) { log.warn('skipping scheduled message with invalid cron expression', { id: row.id, cron: row.cron_expression }) continue } const task = cron.schedule(row.cron_expression, () => sendToChannel(row.channel_id, row.content)) recurringTasks.set(row.id, task) } log.info('loaded recurring scheduled messages', { count: recurringTasks.size }) } async function sweepDueOneOff() { try { const due = await scheduledMessages.listDueOneOff() for (const row of due) { await sendToChannel(row.channel_id, row.content) await scheduledMessages.markSent(row.id) } } catch (err) { log.error('one-off sweep failed', { message: err.message }) } } async function start(client) { discordClient = client await loadRecurring() sweepTask = cron.schedule('* * * * *', sweepDueOneOff) log.info('scheduler started') } // Called by /schedule after any add/remove so changes apply without a restart. async function refresh() { if (!discordClient) return await loadRecurring() } function stop() { for (const task of recurringTasks.values()) task.stop() recurringTasks.clear() if (sweepTask) { sweepTask.stop() sweepTask = null } discordClient = null } module.exports = { start, stop, refresh }