Fix popular command to include Steam auto-detected ownership
This commit is contained in:
@@ -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, `!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 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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -81,6 +81,7 @@ To change the token or key later, edit `.env` directly, then run the installer a
|
||||
- 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."
|
||||
- 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.
|
||||
- `!popular` combines both sources across every game, not just one at a time: anything self-reported with `!own`, plus a full scan of every Steam-linked member's library (Steam already hands back the game name with that lookup, so no extra API calls are needed). The same game reported through `!own` and detected in someone's Steam library counts as one shared total, not two. Games with only one confirmed owner are left out, and everything is sorted most-owned first.
|
||||
- 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.
|
||||
- The bot only replies to mentions of its trigger word if Ollama is installed, running, and has the model in `OLLAMA_MODEL` downloaded - without that, mentioning it gets no reply at all, and nothing is logged (check `bot.error.log` if you expect it to be working and it isn't replying). Each person can only trigger an AI answer once every 5 seconds; asking again sooner (or the Ollama request failing) just gets no reply rather than a second request, which keeps a burst of questions from bogging down your computer generating several replies at once.
|
||||
- `!remember SOMETHING` teaches it a permanent fact about you - a name, a preference, a correction like "don't call me buddy" - that it will always take into account when it replies to you, even after the bot restarts. These facts are saved to `memory.yaml`, per person, and never expire on their own, unlike the short-term memory described below. `!forget SOMETHING` removes one again (it has to match what you typed to `!remember`, word for word - use `!memories` to see the exact wording it has stored), and `!forget all` clears everything it remembers about you at once. Each person can have up to 20 things remembered at a time.
|
||||
|
||||
@@ -313,13 +313,18 @@ async function resolveSteamId(input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchOwnedAppIds(steamId) {
|
||||
async function fetchOwnedGames(steamId) {
|
||||
const url = "https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=" +
|
||||
STEAM_API_KEY + "&steamid=" + steamId + "&format=json&include_played_free_games=true";
|
||||
STEAM_API_KEY + "&steamid=" + steamId + "&format=json&include_played_free_games=true&include_appinfo=true";
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error("owned games failed: " + res.status);
|
||||
const data = await res.json();
|
||||
const games = (data.response && data.response.games) || [];
|
||||
return games.map((g) => ({ appid: g.appid, name: g.name }));
|
||||
}
|
||||
|
||||
async function fetchOwnedAppIds(steamId) {
|
||||
const games = await fetchOwnedGames(steamId);
|
||||
return games.map((g) => g.appid);
|
||||
}
|
||||
|
||||
@@ -717,6 +722,65 @@ async function handleWho(message, query) {
|
||||
await message.reply({ content: lines.join("\n"), allowedMentions: { users: [] } });
|
||||
}
|
||||
|
||||
async function handlePopular(message) {
|
||||
const tally = {};
|
||||
|
||||
function addOwner(key, title, userId) {
|
||||
if (!tally[key]) tally[key] = { title: title, owners: new Set() };
|
||||
tally[key].owners.add(userId);
|
||||
}
|
||||
|
||||
const owned = loadOwned();
|
||||
const appidCache = {};
|
||||
|
||||
for (const userId of Object.keys(owned)) {
|
||||
for (const game of owned[userId]) {
|
||||
if (!(game.id in appidCache)) {
|
||||
try {
|
||||
const info = await fetchGameInfo(game.id);
|
||||
appidCache[game.id] = info.appid || null;
|
||||
} catch (err) {
|
||||
appidCache[game.id] = null;
|
||||
}
|
||||
}
|
||||
const appid = appidCache[game.id];
|
||||
const key = appid ? "steam:" + appid : "itad:" + game.id;
|
||||
addOwner(key, game.title, userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (STEAM_API_KEY) {
|
||||
const links = loadSteamLinks();
|
||||
for (const userId of Object.keys(links)) {
|
||||
let games;
|
||||
try {
|
||||
games = await fetchOwnedGames(links[userId]);
|
||||
} catch (err) {
|
||||
console.error("Owned games check failed for " + userId + ":", err.message);
|
||||
continue;
|
||||
}
|
||||
for (const game of games) {
|
||||
addOwner("steam:" + game.appid, game.name, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const popular = Object.values(tally)
|
||||
.filter((g) => g.owners.size > 1)
|
||||
.map((g) => ({ title: g.title, count: g.owners.size }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
if (popular.length === 0) {
|
||||
await message.reply("Nobody owns any games?! Type !own GAME to add a game you own to the list!");
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = ["Here's what members are playing!"];
|
||||
popular.forEach((g) => lines.push(g.title + ": " + g.count + " member" + (g.count === 1 ? "" : "s")));
|
||||
|
||||
await message.reply(lines.join("\n"));
|
||||
}
|
||||
|
||||
async function handleInfo(message, query) {
|
||||
if (!query) {
|
||||
await message.reply("Usage: !info GAME NAME");
|
||||
@@ -1134,6 +1198,7 @@ async function handleHelp(message) {
|
||||
"!unown GAME - remove a game from your owned list\n" +
|
||||
"!my-steam-profile LINK - link your Steam profile so !info can count games you own automatically\n" +
|
||||
"!who GAME - list who has confirmed they own a game\n" +
|
||||
"!popular - list games owned by more than one member, most owned first\n" +
|
||||
"!remember SOMETHING - permanently teach me a fact about you (a name, a preference), I'll remember it across restarts\n" +
|
||||
"!forget SOMETHING - make me forget something you had me remember (must match exactly), or !forget all\n" +
|
||||
"!memories - show everything I remember about you\n" +
|
||||
@@ -1341,6 +1406,8 @@ client.on("messageCreate", async (message) => {
|
||||
await handleLinkSteam(message, content.slice(18).trim());
|
||||
} else if (lower.startsWith("!who ")) {
|
||||
await handleWho(message, content.slice(5).trim());
|
||||
} else if (lower === "!popular") {
|
||||
await handlePopular(message);
|
||||
} else if (lower.startsWith("!remember ")) {
|
||||
await handleRemember(message, content.slice(10).trim());
|
||||
} else if (lower.startsWith("!forget ")) {
|
||||
|
||||
Reference in New Issue
Block a user