fix role button timeout and stale member count on click

This commit is contained in:
justin committed 2026-09-14 10:25:24 -07:00
1 parent f0912e1d03
commit 4601e34e93
2 files changed
+47 -43

No files matched your search

+1 -2
View File
@@ -80,12 +80,11 @@ 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.
- 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-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 "EVENT: 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.
- Any Discord Scheduled Event on your server gets an automatic reminder posted to that same channel 5 minutes before it starts, like "EVENT: Movie Night starting soon!" 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.
- If you added game roles after people had already been using `!own`, run `bash install.sh --backfill-roles` (macOS/Linux) or `powershell -ExecutionPolicy Bypass -File install.ps1 -BackfillRoles` (Windows) once to catch up: it reads everyone's existing `owned.json` entries, creates any missing roles, and assigns each person every role for a game they already marked as owned. It only looks at `!own` data, not Steam-linked libraries. Safe to run more than once, it skips anyone who already has the role. This does not restart the bot.
- To mark every current server member as owning a game at once, without each person running `!own` themselves, run `bash install.sh --bulk-own GAME NAME` (macOS/Linux) or `powershell -ExecutionPolicy Bypass -File install.ps1 -BulkOwn "GAME NAME"` (Windows). It matches the game the same way `!own` does, adds it to `owned.json` for everyone in the server, and creates/assigns the role for it too. Safe to run again later for new members, it only adds what's missing. This does not restart the bot.
- To change a role's emoji (or fix a title) after the fact, edit its `emoji` (or `title`) field directly in `roles.yaml`, then run `bash install.sh --sync-roles` (macOS/Linux) or `powershell -ExecutionPolicy Bypass -File install.ps1 -SyncRoles` (Windows) to push that change to the actual Discord role name - editing `roles.yaml` alone doesn't update Discord on its own. Safe to run any time, it only renames roles that are out of sync and leaves the rest alone. This does not restart the bot.
- 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.
+46 -41
View File
@@ -23,7 +23,7 @@ const WEB_SEARCH_RESULT_COUNT = 5;
const WEB_SEARCH_TIMEOUT_MS = 12000;
const AI_COOLDOWN_MS = 5000;
const GREETING_CHANNEL_NAME = "general";
const EVENT_REMINDER_MINUTES = 15;
const EVENT_REMINDER_MINUTES = 5;
const POPULAR_TOP_N = 10;
const ALL_PRICES_TOP_N = 10;
const SALE_ENDING_SOON_HOURS = 48;
@@ -466,9 +466,23 @@ function sortedRoleEntries() {
const ROLE_PICKER_MAX = 25;
function buildRolePickerComponents(entries) {
function roleMemberCounts(guild, entries) {
const counts = {};
entries.forEach((entry) => {
const role = guild && guild.roles.cache.get(entry.roleId);
counts[entry.roleId] = role ? role.members.size : 0;
});
return counts;
}
function buildRolePickerComponents(entries, counts) {
if (entries.length === 0) return { components: [], overflow: false };
const labelFor = (entry) => {
const count = counts ? counts[entry.roleId] : undefined;
return count === undefined ? entry.title : entry.title + " (" + count + ")";
};
if (entries.length <= ROLE_PICKER_MAX) {
const rows = [];
for (let i = 0; i < entries.length; i += 5) {
@@ -477,7 +491,7 @@ function buildRolePickerComponents(entries) {
row.addComponents(
new ButtonBuilder()
.setCustomId("role:" + entry.roleId)
.setLabel(entry.title.slice(0, 80))
.setLabel(labelFor(entry).slice(0, 80))
.setEmoji(entry.emoji)
.setStyle(ButtonStyle.Secondary)
);
@@ -492,7 +506,7 @@ function buildRolePickerComponents(entries) {
.setCustomId("role-select")
.setPlaceholder("Choose a game role")
.addOptions(shown.map((entry) => ({
label: entry.title.slice(0, 100),
label: labelFor(entry).slice(0, 100),
value: entry.roleId,
emoji: entry.emoji
})));
@@ -1507,41 +1521,13 @@ async function handleRoles(message) {
}
}
const lines = entries.map((entry) => {
const role = guild && guild.roles.cache.get(entry.roleId);
const count = role ? role.members.size : 0;
return entry.emoji + " **" + entry.title + "** - " + count + (count === 1 ? " member" : " members");
});
const chunks = [];
let chunk = "";
for (const line of lines) {
const candidate = chunk ? chunk + "\n" + line : line;
if (candidate.length > 4000) {
chunks.push(chunk);
chunk = line;
} else {
chunk = candidate;
}
}
if (chunk) chunks.push(chunk);
const picker = buildRolePickerComponents(entries);
for (let i = 0; i < chunks.length; i++) {
const embed = new EmbedBuilder()
.setTitle(i === 0 ? "Game roles" : "Game roles (continued)")
.setDescription(chunks[i]);
await message.reply({ embeds: [embed] });
}
if (picker.components.length > 0) {
const counts = roleMemberCounts(guild, entries);
const picker = buildRolePickerComponents(entries, counts);
const content = picker.overflow
? "Use the dropdown below to add yourself to a role (showing the first " + ROLE_PICKER_MAX + " alphabetically - use !role GAME for any others):"
: "Click on a button below to add yourself to a role:";
: "Click a button below to add or remove yourself from a role:";
await message.reply({ content: content, components: picker.components });
}
}
async function handleRole(message, input) {
if (!input) {
@@ -2215,7 +2201,7 @@ async function checkUpcomingEvents(client) {
}
try {
await channel.send("EVENT: " + event.name + " starting in " + EVENT_REMINDER_MINUTES + " minutes!");
await channel.send("EVENT: " + event.name + " starting soon!");
} catch (err) {
console.error("Could not send event reminder:", err.message);
}
@@ -2290,7 +2276,8 @@ client.on("guildMemberAdd", async (member) => {
const entries = sortedRoleEntries();
if (entries.length > 0) {
const picker = buildRolePickerComponents(entries);
const counts = roleMemberCounts(member.guild, entries);
const picker = buildRolePickerComponents(entries, counts);
const content = "Add yourself to a role for games you own or play, " + member.toString() + ":" +
(picker.overflow ? " (showing the first " + ROLE_PICKER_MAX + " alphabetically in the dropdown, !role GAME works for the rest)" : "");
try {
@@ -2330,18 +2317,36 @@ async function handleRoleInteraction(interaction, roleId) {
return;
}
await interaction.deferUpdate();
const hadRole = interaction.member.roles.cache.has(roleId);
let confirmation;
try {
if (interaction.member.roles.cache.has(roleId)) {
if (hadRole) {
await interaction.member.roles.remove(roleId);
await interaction.reply({ content: "Removed you from the " + entry.emoji + " " + entry.title + " role.", flags: MessageFlags.Ephemeral });
confirmation = "Removed you from the " + entry.emoji + " " + entry.title + " role.";
} else {
await interaction.member.roles.add(roleId);
await interaction.reply({ content: "Added you to the " + entry.emoji + " " + entry.title + " role.", flags: MessageFlags.Ephemeral });
confirmation = "Added you to the " + entry.emoji + " " + entry.title + " role.";
}
} catch (err) {
console.error("Role button interaction failed:", err.message);
await interaction.reply({ content: "Could not update that role - make sure I have Manage Roles permission and my role is above the roles I manage.", flags: MessageFlags.Ephemeral });
await interaction.followUp({ content: "Could not update that role - make sure I have Manage Roles permission and my role is above the roles I manage.", flags: MessageFlags.Ephemeral });
return;
}
const entries = sortedRoleEntries();
const counts = roleMemberCounts(interaction.guild, entries);
if (counts[roleId] !== undefined) {
counts[roleId] = Math.max(0, counts[roleId] + (hadRole ? -1 : 1));
}
const picker = buildRolePickerComponents(entries, counts);
try {
await interaction.editReply({ components: picker.components });
} catch (err) {
console.error("Could not refresh role picker message:", err.message);
}
await interaction.followUp({ content: confirmation, flags: MessageFlags.Ephemeral });
}
client.on("interactionCreate", async (interaction) => {