Post a reminder 15 minutes before scheduled Discord events

This commit is contained in:
justin committed 2026-08-31 11:10:14 -07:00
1 parent f3f4e7ef73
commit 8b0316af74
2 files changed
+52 -2

No files matched your search

+1
View File
@@ -78,6 +78,7 @@ To change the token or key later, edit `.env` directly, then run the installer a
- To add, remove, or change greetings, edit `greetings.yaml`. Use `{user}` anywhere in a greeting and it gets replaced with a mention of the person who joined. - To add, remove, or change greetings, edit `greetings.yaml`. Use `{user}` anywhere in a greeting and it gets replaced with a mention of the person who joined.
- Same for goodbyes: edit `goodbyes.yaml`. Use `{user}` there too, it gets replaced with the username of whoever left (a plain name, not a mention, since Discord can't ping someone no longer in the server). - Same for goodbyes: edit `goodbyes.yaml`. Use `{user}` there too, it gets replaced with the username of whoever left (a plain name, not a mention, since Discord can't ping someone no longer in the server).
- The join greeting posts to whichever channel is named `general` in your server. To use a different channel, change `GREETING_CHANNEL_NAME` near the top of `index.js` to that channel's name (no `#`) and restart it (re-run the installer, or `pm2 restart discord-game-price-bot`). If no channel with that name exists, nothing posts and `bot.error.log` will note it. - The join greeting posts to whichever channel is named `general` in your server. To use a different channel, change `GREETING_CHANNEL_NAME` near the top of `index.js` to that channel's name (no `#`) and restart it (re-run the installer, or `pm2 restart discord-game-price-bot`). If no channel with that name exists, nothing posts and `bot.error.log` will note it.
- Any Discord Scheduled Event on your server gets an automatic reminder posted to that same channel 15 minutes before it starts, like "Movie Night starting in 15 minutes!" Each event only gets reminded once. To change how far ahead it reminds, edit `EVENT_REMINDER_MINUTES` near the top of `index.js` and restart it.
- `!info` gets platforms and Multiplayer/Co-Op/Cross-Play from that game's Steam store page when it has one. For non-Steam games (GOG/Epic exclusives etc.) there is no such lookup available, so "not listed" there means "couldn't confirm it," not "definitely single-player only." - `!info` gets platforms and Multiplayer/Co-Op/Cross-Play from that game's Steam store page when it has one. For non-Steam games (GOG/Epic exclusives etc.) there is no such lookup available, so "not listed" there means "couldn't confirm it," not "definitely single-player only."
- The ownership count on `!info` combines two sources: anyone who ran `!own` for that game, plus anyone who linked their Steam profile with `!my-steam-profile` and actually owns it there. Discord itself has no concept of game ownership, so there is no fully automatic way to get this - `!own` needs people to mark their own games by hand, and `!my-steam-profile` only auto-detects Steam copies (not GOG/Epic) and only if that person's Steam profile has "game details" set to public. Everything self-reported through `!own` is stored in `owned.json`; Steam profile links are stored in `steamlinks.json`, both in this folder, in plain text. - The ownership count on `!info` combines two sources: anyone who ran `!own` for that game, plus anyone who linked their Steam profile with `!my-steam-profile` and actually owns it there. Discord itself has no concept of game ownership, so there is no fully automatic way to get this - `!own` needs people to mark their own games by hand, and `!my-steam-profile` only auto-detects Steam copies (not GOG/Epic) and only if that person's Steam profile has "game details" set to public. Everything self-reported through `!own` is stored in `owned.json`; Steam profile links are stored in `steamlinks.json`, both in this folder, in plain text.
- If you don't want to make each person run `!my-steam-profile` themselves, you can link them yourself from the terminal (the easiest way if you already know your server members as Steam friends): run `bash install.sh --link-steam-profiles` (macOS/Linux) or `powershell -ExecutionPolicy Bypass -File install.ps1 -LinkSteamProfiles` (Windows). The first time, it asks for your own Steam profile once (to read your friends list from - it needs your Steam friends list privacy set to public, and remembers your profile after that so it won't ask again). Then it lists your Discord server's members for you to pick one by number, lists your Steam friends for you to pick one by number, links them, and asks if you want to link another (defaults to no). This does not restart the bot, so there is no need to run the installer again afterward. This is terminal-only by design - there's no Discord command for it, so members can't link (or relink) each other's profiles themselves. - If you don't want to make each person run `!my-steam-profile` themselves, you can link them yourself from the terminal (the easiest way if you already know your server members as Steam friends): run `bash install.sh --link-steam-profiles` (macOS/Linux) or `powershell -ExecutionPolicy Bypass -File install.ps1 -LinkSteamProfiles` (Windows). The first time, it asks for your own Steam profile once (to read your friends list from - it needs your Steam friends list privacy set to public, and remembers your profile after that so it won't ask again). Then it lists your Discord server's members for you to pick one by number, lists your Steam friends for you to pick one by number, links them, and asks if you want to link another (defaults to no). This does not restart the bot, so there is no need to run the installer again afterward. This is terminal-only by design - there's no Discord command for it, so members can't link (or relink) each other's profiles themselves.
+51 -2
View File
@@ -3,7 +3,7 @@ require("dotenv").config({ path: path.join(__dirname, ".env") });
const fs = require("fs"); const fs = require("fs");
const cron = require("node-cron"); const cron = require("node-cron");
const yaml = require("js-yaml"); const yaml = require("js-yaml");
const { Client, GatewayIntentBits, MessageFlags, EmbedBuilder } = require("discord.js"); const { Client, GatewayIntentBits, MessageFlags, EmbedBuilder, GuildScheduledEventStatus } = require("discord.js");
const rawLog = console.log.bind(console); const rawLog = console.log.bind(console);
const rawError = console.error.bind(console); const rawError = console.error.bind(console);
@@ -18,6 +18,7 @@ const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || "llama3.2"; const OLLAMA_MODEL = process.env.OLLAMA_MODEL || "llama3.2";
const AI_COOLDOWN_MS = 5000; const AI_COOLDOWN_MS = 5000;
const GREETING_CHANNEL_NAME = "general"; const GREETING_CHANNEL_NAME = "general";
const EVENT_REMINDER_MINUTES = 15;
const ITAD_BASE = "https://api.isthereanydeal.com"; const ITAD_BASE = "https://api.isthereanydeal.com";
const GAMES_PATH = path.join(__dirname, "games.json"); const GAMES_PATH = path.join(__dirname, "games.json");
const OWNED_PATH = path.join(__dirname, "owned.json"); const OWNED_PATH = path.join(__dirname, "owned.json");
@@ -1197,6 +1198,52 @@ async function checkPrices(client) {
return { checked: games.length, alerted: alerted }; return { checked: games.length, alerted: alerted };
} }
const notifiedEvents = new Set();
async function checkUpcomingEvents(client) {
const stillScheduled = new Set();
for (const guild of client.guilds.cache.values()) {
let events;
try {
events = await guild.scheduledEvents.fetch();
} catch (err) {
console.error("Could not fetch scheduled events for " + guild.name + ":", err.message);
continue;
}
for (const event of events.values()) {
if (event.status !== GuildScheduledEventStatus.Scheduled || !event.scheduledStartAt) continue;
stillScheduled.add(event.id);
if (notifiedEvents.has(event.id)) continue;
const minutesUntil = (event.scheduledStartAt.getTime() - Date.now()) / 60000;
if (minutesUntil <= 0 || minutesUntil > EVENT_REMINDER_MINUTES) continue;
const channel = guild.channels.cache.find(
(c) => c.name === GREETING_CHANNEL_NAME && c.isTextBased && c.isTextBased()
);
if (!channel) {
console.error("Could not find a #" + GREETING_CHANNEL_NAME + " channel in " + guild.name);
notifiedEvents.add(event.id);
continue;
}
try {
await channel.send(event.name + " starting in " + EVENT_REMINDER_MINUTES + " minutes!");
} catch (err) {
console.error("Could not send event reminder:", err.message);
}
notifiedEvents.add(event.id);
}
}
for (const id of Array.from(notifiedEvents)) {
if (!stillScheduled.has(id)) notifiedEvents.delete(id);
}
}
async function handleCheck(message, client) { async function handleCheck(message, client) {
await message.reply("Checking prices now..."); await message.reply("Checking prices now...");
const result = await checkPrices(client); const result = await checkPrices(client);
@@ -1215,13 +1262,15 @@ const client = new Client({
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent, GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildScheduledEvents
] ]
}); });
client.once("clientReady", () => { client.once("clientReady", () => {
console.log("Logged in as " + client.user.tag); console.log("Logged in as " + client.user.tag);
cron.schedule("0 15 * * *", () => checkPrices(client)); cron.schedule("0 15 * * *", () => checkPrices(client));
cron.schedule("* * * * *", () => checkUpcomingEvents(client));
}); });
client.on("guildMemberAdd", async (member) => { client.on("guildMemberAdd", async (member) => {