diff --git a/events/vrcTlEvents.js b/events/vrcTlEvents.js new file mode 100644 index 0000000..cf8b20e --- /dev/null +++ b/events/vrcTlEvents.js @@ -0,0 +1,62 @@ +const { Events } = require('discord.js'); +const axios = require('axios').default; + +const SWARM_EVENTS_CHANNEL_ID = '1536067515115905155'; +const VRC_TL_EVENT_REGEX = /https:\/\/vrc\.tl\/event\/(\d+)/g; +const REMINDER_EMOTE = '🔔'; + +module.exports = { + name: Events.MessageCreate, + async execute(message) { + if (message.author.bot) return; + if (message.channel.id !== SWARM_EVENTS_CHANNEL_ID) return; + + const matches = [...message.content.matchAll(VRC_TL_EVENT_REGEX)]; + if (matches.length === 0) return; + + const eventIds = [...new Set(matches.map(match => match[1]))]; + + for (const eventId of eventIds) { + try { + const { data } = await axios.get(`https://vrc.tl/api/v1/events/${eventId}`); + const event = data.events?.[0]; + if (!event) continue; + + const performersById = new Map(data.performers.map(performer => [performer.id, performer.name])); + + const lineup = [...event.eventSlots] + .sort((a, b) => a.start - b.start) + .flatMap(slot => [...slot.performers] + .sort((a, b) => a.order - b.order) + .map(slotPerformer => performersById.get(slotPerformer.performerId))) + .filter(Boolean); + + const lines = [ + `**${event.name}**`, + ` ()`, + ]; + + if (lineup.length > 0) { + lines.push(`DJ Lineup: ${lineup.join(', ')}`); + } + + if (event.urls?.vrcGroup) { + lines.push(`Group: ${event.urls.vrcGroup}`); + } + + lines.push(`\nReact with ${REMINDER_EMOTE} to get pinged 1 hour before this starts.`); + + const reply = await message.reply(lines.join('\n')); + await reply.react(REMINDER_EMOTE); + + const db = await message.client.localDB; + db.prepare(` + INSERT INTO vrctl_event_reminders (vrctl_event_id, message_id, channel_id, event_name, event_start) + VALUES (?, ?, ?, ?, ?) + `).run(eventId, reply.id, reply.channel.id, event.name, event.start); + } catch (error) { + console.error(`Failed to fetch vrc.tl event ${eventId}:`, error?.response?.data ?? error); + } + } + }, +}; diff --git a/migrations/01_vrctl_event_reminders.js b/migrations/01_vrctl_event_reminders.js new file mode 100644 index 0000000..0380914 --- /dev/null +++ b/migrations/01_vrctl_event_reminders.js @@ -0,0 +1,20 @@ +module.exports = { + up: async ({ context: db }) => { + db.exec(` + CREATE TABLE IF NOT EXISTS vrctl_event_reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vrctl_event_id TEXT NOT NULL, + message_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + event_name TEXT, + event_start INTEGER NOT NULL, + reminder_sent INTEGER NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); + db.exec(`CREATE INDEX IF NOT EXISTS idx_vrctl_reminders_pending ON vrctl_event_reminders (reminder_sent, event_start);`); + }, + down: async ({ context: db }) => { + db.exec(`DROP TABLE IF EXISTS vrctl_event_reminders;`); + } +}; diff --git a/timers/vrcTlReminder.js b/timers/vrcTlReminder.js new file mode 100644 index 0000000..2f97fe1 --- /dev/null +++ b/timers/vrcTlReminder.js @@ -0,0 +1,55 @@ +const REMINDER_EMOTE = '🔔'; +const REMINDER_LEAD_SECONDS = 1 * 60 * 60; + +module.exports = { + timeout: 60000, + immediate: true, + name: 'VRC.TL Event Reminders', + async tick(client, timer) { + try { + const db = await client.localDB; + const nowUnix = Math.floor(Date.now() / 1000); + + const dueReminders = db.prepare(` + SELECT id, message_id, channel_id, event_name, event_start + FROM vrctl_event_reminders + WHERE reminder_sent = 0 AND event_start - ? <= ? + `).all(REMINDER_LEAD_SECONDS, nowUnix); + + for (const reminder of dueReminders) { + const markSent = db.prepare(`UPDATE vrctl_event_reminders SET reminder_sent = 1 WHERE id = ?`); + + if (reminder.event_start <= nowUnix) { + // Event already started (e.g. bot was down) - don't send a stale reminder. + markSent.run(reminder.id); + continue; + } + + try { + const channel = await client.channels.fetch(reminder.channel_id); + const message = await channel.messages.fetch(reminder.message_id); + const reaction = message.reactions.cache.get(REMINDER_EMOTE); + + const users = reaction ? await reaction.users.fetch() : new Map(); + const mentions = [...users.values()] + .filter(user => !user.bot) + .map(user => `<@${user.id}>`); + + if (mentions.length > 0) { + await channel.send({ + content: `⏰ **${reminder.event_name}** starts ! ${mentions.join(' ')}`, + reply: { messageReference: reminder.message_id }, + allowedMentions: { parse: [], users: mentions.map(m => m.slice(2, -1)) } + }); + } + + markSent.run(reminder.id); + } catch (error) { + console.error(`[TIMER] Failed to send vrc.tl reminder for message ${reminder.message_id}:`, error); + } + } + } catch (error) { + console.error('[TIMER] VRC.TL Event Reminders error:', error); + } + }, +};