Add prices command listing every store a game is on sale at

This commit is contained in:
Justin Oro
2026-08-31 16:03:34 -07:00
parent b470cbe068
commit 260ceed9df
2 changed files with 57 additions and 2 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Discord Game Price Bot # Discord Game Price Bot
A Discord bot for your server. Type `!watch GAME NAME` in any channel to start tracking a PC game's price, `!remove GAME NAME` (or `!unwatch GAME NAME`) to stop, `!list` to see what is being watched, `!price GAME NAME` to check a price without watching it, `!history GAME NAME` to see every recorded sale over the past 12 months with date and price, `!predict GAME NAME` to guess when it might go on sale next, `!info GAME NAME` to see its platforms, multiplayer/co-op/cross-play info, a link to its store page, and how many people on the server own it, `!own GAME NAME` to mark a game as owned by you, `!unown GAME NAME` to remove it from your owned list, `!my-steam-profile LINK` to link your Steam profile so `!info` can count games you own automatically, `!who GAME NAME` to list who has confirmed they own a game, `!popular` to list games owned by more than one member, most owned first, `!remember SOMETHING` to permanently teach it a fact about you that survives restarts, `!forget SOMETHING` (or `!forget all`) to make it forget something you had it remember, `!memories` to see everything it remembers about you, `!check` to run the sale check on demand, `!shops` to see which stores it covers. Once a day it checks the best current price across PC stores for everything watched and posts in the channel it was added from when something is on sale, with the title, sale price, and which store it is at. A Discord bot for your server. Type `!watch GAME NAME` in any channel to start tracking a PC game's price, `!remove GAME NAME` (or `!unwatch GAME NAME`) to stop, `!list` to see what is being watched, `!price GAME NAME` to check the best price without watching it, `!prices GAME NAME` to list every store it's on sale at, cheapest first, `!history GAME NAME` to see every recorded sale over the past 12 months with date and price, `!predict GAME NAME` to guess when it might go on sale next, `!info GAME NAME` to see its platforms, multiplayer/co-op/cross-play info, a link to its store page, and how many people on the server own it, `!own GAME NAME` to mark a game as owned by you, `!unown GAME NAME` to remove it from your owned list, `!my-steam-profile LINK` to link your Steam profile so `!info` can count games you own automatically, `!who GAME NAME` to list who has confirmed they own a game, `!popular` to list games owned by more than one member, most owned first, `!remember SOMETHING` to permanently teach it a fact about you that survives restarts, `!forget SOMETHING` (or `!forget all`) to make it forget something you had it remember, `!memories` to see everything it remembers about you, `!check` to run the sale check on demand, `!shops` to see which stores it covers. Once a day it checks the best current price across PC stores for everything watched and posts in the channel it was added from when something is on sale, with the title, sale price, and which store it is at.
Mention the bot's trigger word anywhere in a message (that doesn't start with `!`) and, if Ollama is installed and running (and `AI_ENABLED` isn't turned off), it thinks up a short, in-character answer to whatever you said using a free AI model running right on your own computer - no API key, no cost, nothing sent anywhere over the internet. Without Ollama running (or with `AI_ENABLED=false`), it doesn't reply to mentions at all. It never fires on `!` commands, even ones that happen to contain the trigger word. The bot's name, trigger word, and personality are entirely up to you - edit `personality.yaml` to set them (see [Notes](#notes)); it ships pre-filled as BMO from Adventure Time as an example. Mention the bot's trigger word anywhere in a message (that doesn't start with `!`) and, if Ollama is installed and running (and `AI_ENABLED` isn't turned off), it thinks up a short, in-character answer to whatever you said using a free AI model running right on your own computer - no API key, no cost, nothing sent anywhere over the internet. Without Ollama running (or with `AI_ENABLED=false`), it doesn't reply to mentions at all. It never fires on `!` commands, even ones that happen to contain the trigger word. The bot's name, trigger word, and personality are entirely up to you - edit `personality.yaml` to set them (see [Notes](#notes)); it ships pre-filled as BMO from Adventure Time as an example.
+56 -1
View File
@@ -20,6 +20,7 @@ const AI_COOLDOWN_MS = 5000;
const GREETING_CHANNEL_NAME = "general"; const GREETING_CHANNEL_NAME = "general";
const EVENT_REMINDER_MINUTES = 15; const EVENT_REMINDER_MINUTES = 15;
const POPULAR_TOP_N = 10; const POPULAR_TOP_N = 10;
const ALL_PRICES_TOP_N = 10;
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");
@@ -470,6 +471,57 @@ async function handlePrice(message, query) {
} }
} }
async function handleAllPrices(message, query) {
if (!query) {
await message.reply("Usage: !prices GAME NAME");
return;
}
let results;
try {
results = await searchGame(query);
} catch (err) {
await message.reply("Search failed, try again in a moment.");
return;
}
if (!results || results.length === 0) {
await message.reply("Could not find a game called \"" + query + "\".");
return;
}
const match = pickSearchMatch(query, results);
try {
const priceData = await fetchPrices([match.id]);
const entry = priceData && priceData[0];
const deals = (entry && entry.deals) || [];
if (deals.length === 0) {
await message.reply("\"" + match.title + "\" - could not find any current listings for it.");
return;
}
const sorted = deals.slice().sort((a, b) => a.price.amountInt - b.price.amountInt);
const top = sorted.slice(0, ALL_PRICES_TOP_N);
const lines = ["\"" + match.title + "\" is on sale at:"];
top.forEach((deal) => {
const cutNote = deal.cut > 0 ? " (" + deal.cut + "% off)" : "";
lines.push(deal.shop.name + ": " + formatMoney(deal.price.amountInt) + cutNote);
});
const remaining = sorted.length - top.length;
if (remaining > 0) {
lines.push("...and " + remaining + " more store" + (remaining === 1 ? "" : "s") + ".");
}
await message.reply({ content: lines.join("\n"), flags: MessageFlags.SuppressEmbeds });
} catch (err) {
await message.reply("Could not load prices right now.");
}
}
async function handleHistory(message, query) { async function handleHistory(message, query) {
if (!query) { if (!query) {
await message.reply("Usage: !history GAME"); await message.reply("Usage: !history GAME");
@@ -1197,7 +1249,8 @@ async function handleHelp(message) {
"!watch GAME - start watching a game for sales\n" + "!watch GAME - start watching a game for sales\n" +
"!remove GAME - stop watching a game (!unwatch also works)\n" + "!remove GAME - stop watching a game (!unwatch also works)\n" +
"!list - show everything being watched\n" + "!list - show everything being watched\n" +
"!price GAME - check a game's current price without watching it\n" + "!price GAME - check a game's current best price without watching it\n" +
"!prices GAME - list every store the game is on sale at, cheapest first\n" +
"!history GAME - show every recorded sale over the past 12 months, with date and price\n" + "!history GAME - show every recorded sale over the past 12 months, with date and price\n" +
"!predict GAME - guess when it might go on sale next, based on its full sale history\n" + "!predict GAME - guess when it might go on sale next, based on its full sale history\n" +
"!info GAME - show price, platforms, multiplayer/co-op/cross-play info, a store link, and how many people here own it\n" + "!info GAME - show price, platforms, multiplayer/co-op/cross-play info, a store link, and how many people here own it\n" +
@@ -1397,6 +1450,8 @@ client.on("messageCreate", async (message) => {
await handleRemove(message, content.slice(8).trim()); await handleRemove(message, content.slice(8).trim());
} else if (lower.startsWith("!unwatch ")) { } else if (lower.startsWith("!unwatch ")) {
await handleRemove(message, content.slice(9).trim(), "!unwatch"); await handleRemove(message, content.slice(9).trim(), "!unwatch");
} else if (lower.startsWith("!prices ")) {
await handleAllPrices(message, content.slice(8).trim());
} else if (lower.startsWith("!price ")) { } else if (lower.startsWith("!price ")) {
await handlePrice(message, content.slice(7).trim()); await handlePrice(message, content.slice(7).trim());
} else if (lower.startsWith("!history ")) { } else if (lower.startsWith("!history ")) {