add game role commands (roles, role, unroll) with emoji role picker

This commit is contained in:
Justin Oros
2026-09-14 09:58:33 -07:00
parent 8dd94bcca6
commit 5c2d69371a
8 changed files with 980 additions and 8 deletions
+152
View File
@@ -0,0 +1,152 @@
require("dotenv").config();
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const readline = require("node:readline/promises");
const { stdin: input, stdout: output } = require("node:process");
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
const DISCORD_API = "https://discord.com/api/v10";
const ROLES_PATH = path.join(__dirname, "roles.yaml");
if (!DISCORD_BOT_TOKEN) {
console.error("DISCORD_BOT_TOKEN is not set in .env. Run bash install.sh first to set it up.");
process.exit(1);
}
function loadRoles() {
try {
const data = yaml.load(fs.readFileSync(ROLES_PATH, "utf8"));
return (data && typeof data === "object") ? data : {};
} catch (err) {
return {};
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchBotGuilds() {
const res = await fetch(DISCORD_API + "/users/@me/guilds", {
headers: { Authorization: "Bot " + DISCORD_BOT_TOKEN }
});
if (!res.ok) throw new Error("Discord guild list failed: " + res.status);
return res.json();
}
async function fetchGuildRoles(guildId) {
const res = await fetch(DISCORD_API + "/guilds/" + guildId + "/roles", {
headers: { Authorization: "Bot " + DISCORD_BOT_TOKEN }
});
if (!res.ok) throw new Error("Discord role list failed: " + res.status);
return res.json();
}
async function renameRole(guildId, roleId, name) {
const res = await fetch(DISCORD_API + "/guilds/" + guildId + "/roles/" + roleId, {
method: "PATCH",
headers: {
Authorization: "Bot " + DISCORD_BOT_TOKEN,
"Content-Type": "application/json"
},
body: JSON.stringify({ name: name })
});
if (!res.ok) throw new Error("role rename failed: " + res.status + " " + (await res.text()));
}
async function pickFromList(rl, label, items, formatItem) {
console.log("");
console.log(label);
items.forEach((item, i) => {
console.log(" " + (i + 1) + ". " + formatItem(item));
});
while (true) {
const answer = (await rl.question("Enter a number (or 0 to cancel): ")).trim();
const n = parseInt(answer, 10);
if (n === 0) return null;
if (!isNaN(n) && n >= 1 && n <= items.length) return items[n - 1];
console.log("Not a valid choice, try again.");
}
}
async function main() {
const roles = loadRoles();
const keys = Object.keys(roles);
if (keys.length === 0) {
console.log("No roles in roles.yaml, nothing to sync.");
return;
}
let guilds;
try {
guilds = await fetchBotGuilds();
} catch (err) {
console.error("Could not load your Discord server list:", err.message);
process.exit(1);
}
if (!guilds || guilds.length === 0) {
console.error("The bot is not in any Discord server yet.");
process.exit(1);
}
let guild = guilds[0];
if (guilds.length > 1) {
const rl = readline.createInterface({ input, output });
guild = await pickFromList(rl, "Which Discord server?", guilds, (g) => g.name);
rl.close();
if (!guild) return;
}
let discordRoles;
try {
discordRoles = await fetchGuildRoles(guild.id);
} catch (err) {
console.error("Could not load server roles:", err.message);
process.exit(1);
}
const byId = {};
discordRoles.forEach((r) => {
byId[r.id] = r;
});
let updated = 0;
let skipped = 0;
let failed = 0;
for (const key of keys) {
const entry = roles[key];
const desiredName = entry.emoji + " " + entry.title;
const current = byId[entry.roleId];
if (!current) {
console.error("Role for \"" + entry.title + "\" no longer exists in Discord, skipping.");
skipped++;
continue;
}
if (current.name === desiredName) {
skipped++;
continue;
}
try {
await renameRole(guild.id, entry.roleId, desiredName);
console.log("Updated \"" + current.name + "\" to \"" + desiredName + "\".");
updated++;
} catch (err) {
failed++;
console.error("Could not update the role for \"" + entry.title + "\":", err.message);
}
await sleep(300);
}
console.log("");
console.log("Done. Updated: " + updated + ", already in sync or skipped: " + skipped +
(failed > 0 ? ", failed: " + failed : "") + ".");
}
main();