From 260ceed9dfd891d135908fc4559ab191a21ec6ca Mon Sep 17 00:00:00 2001 From: Justin Oro Date: Mon, 31 Aug 2026 16:03:34 -0700 Subject: [PATCH] Add prices command listing every store a game is on sale at --- README.md | 2 +- index.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e5bcc83..410e26f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. diff --git a/index.js b/index.js index 2e5f02e..3ed9718 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ const AI_COOLDOWN_MS = 5000; const GREETING_CHANNEL_NAME = "general"; const EVENT_REMINDER_MINUTES = 15; const POPULAR_TOP_N = 10; +const ALL_PRICES_TOP_N = 10; const ITAD_BASE = "https://api.isthereanydeal.com"; const GAMES_PATH = path.join(__dirname, "games.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) { if (!query) { await message.reply("Usage: !history GAME"); @@ -1197,7 +1249,8 @@ async function handleHelp(message) { "!watch GAME - start watching a game for sales\n" + "!remove GAME - stop watching a game (!unwatch also works)\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" + "!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" + @@ -1397,6 +1450,8 @@ client.on("messageCreate", async (message) => { await handleRemove(message, content.slice(8).trim()); } else if (lower.startsWith("!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 ")) { await handlePrice(message, content.slice(7).trim()); } else if (lower.startsWith("!history ")) {