Files
2026-08-29 17:00:21 -07:00

258 lines
8.1 KiB
JavaScript

require("dotenv").config();
const fs = require("fs");
const path = require("path");
const readline = require("node:readline/promises");
const { stdin: input, stdout: output } = require("node:process");
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
const STEAM_API_KEY = process.env.STEAM_API_KEY;
const DISCORD_API = "https://discord.com/api/v10";
const STEAMLINKS_PATH = path.join(__dirname, "steamlinks.json");
const OWNER_CONFIG_PATH = path.join(__dirname, ".link-steam-profiles-owner.json");
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);
}
if (!STEAM_API_KEY) {
console.error("STEAM_API_KEY is not set in .env. Run bash install.sh and provide a Steam Web API key first.");
process.exit(1);
}
function loadSteamLinks() {
if (!fs.existsSync(STEAMLINKS_PATH)) return {};
return JSON.parse(fs.readFileSync(STEAMLINKS_PATH, "utf8"));
}
function saveSteamLinks(links) {
fs.writeFileSync(STEAMLINKS_PATH, JSON.stringify(links, null, 2) + "\n");
}
function loadOwnerSteamId() {
if (!fs.existsSync(OWNER_CONFIG_PATH)) return null;
try {
const data = JSON.parse(fs.readFileSync(OWNER_CONFIG_PATH, "utf8"));
return data.steamId || null;
} catch (err) {
return null;
}
}
function saveOwnerSteamId(steamId) {
fs.writeFileSync(OWNER_CONFIG_PATH, JSON.stringify({ steamId: steamId }, null, 2) + "\n");
}
function parseSteamProfileInput(rawInput) {
const trimmed = rawInput.trim();
let m = trimmed.match(/steamcommunity\.com\/profiles\/(\d{17})/i);
if (m) return { type: "id", value: m[1] };
m = trimmed.match(/steamcommunity\.com\/id\/([^\/\s]+)/i);
if (m) return { type: "vanity", value: m[1] };
if (/^\d{17}$/.test(trimmed)) return { type: "id", value: trimmed };
return { type: "vanity", value: trimmed };
}
async function resolveSteamId(rawInput) {
const parsed = parseSteamProfileInput(rawInput);
if (parsed.type === "id") return parsed.value;
const url = "https://api.steampowered.com/ISteamUser/ResolveVanityURL/v0001/?key=" +
STEAM_API_KEY + "&vanityurl=" + encodeURIComponent(parsed.value);
const res = await fetch(url);
if (!res.ok) throw new Error("resolve vanity url failed: " + res.status);
const data = await res.json();
if (data.response && data.response.success === 1) return data.response.steamid;
return null;
}
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 fetchGuildMembers(guildId) {
const res = await fetch(DISCORD_API + "/guilds/" + guildId + "/members?limit=1000", {
headers: { Authorization: "Bot " + DISCORD_BOT_TOKEN }
});
if (!res.ok) throw new Error("Discord member list failed: " + res.status);
return res.json();
}
async function fetchFriendList(steamId) {
const url = "https://api.steampowered.com/ISteamUser/GetFriendList/v0001/?key=" +
STEAM_API_KEY + "&steamid=" + steamId + "&relationship=friend";
const res = await fetch(url);
if (res.status === 401) {
throw new Error(
"your Steam friends list is set to private. Go to Steam > Settings > Privacy Settings > " +
"\"My friends list\" and set it to Public, then try again."
);
}
if (!res.ok) throw new Error("Steam friend list failed: " + res.status);
const data = await res.json();
return (data.friendslist && data.friendslist.friends) || [];
}
async function fetchPlayerSummaries(steamIds) {
const url = "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=" +
STEAM_API_KEY + "&steamids=" + steamIds.join(",");
const res = await fetch(url);
if (!res.ok) throw new Error("Steam player summaries failed: " + res.status);
const data = await res.json();
return (data.response && data.response.players) || [];
}
function memberDisplayName(member) {
return member.nick || member.user.global_name || member.user.username;
}
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 rl = readline.createInterface({ input, output });
let ownerSteamId = loadOwnerSteamId();
if (!ownerSteamId) {
console.log("First, we need your own Steam profile so we can read your friends list from it.");
const profileInput = await rl.question("Enter your Steam profile URL or SteamID: ");
try {
ownerSteamId = await resolveSteamId(profileInput);
} catch (err) {
console.error("Could not look up that Steam profile:", err.message);
rl.close();
process.exit(1);
}
if (!ownerSteamId) {
console.error("Could not find a Steam profile matching that.");
rl.close();
process.exit(1);
}
saveOwnerSteamId(ownerSteamId);
console.log("Saved, so you won't be asked again next time.");
}
let guilds;
try {
guilds = await fetchBotGuilds();
} catch (err) {
console.error("Could not load your Discord server list:", err.message);
rl.close();
process.exit(1);
}
if (!guilds || guilds.length === 0) {
console.error("The bot is not in any Discord server yet.");
rl.close();
process.exit(1);
}
let guild = guilds[0];
if (guilds.length > 1) {
guild = await pickFromList(rl, "Which Discord server?", guilds, (g) => g.name);
if (!guild) {
rl.close();
return;
}
}
let friends;
try {
friends = await fetchFriendList(ownerSteamId);
} catch (err) {
console.error("Could not load your Steam friends list:", err.message);
rl.close();
process.exit(1);
}
if (friends.length === 0) {
console.error(
"No Steam friends were returned. This usually means your Steam friends list privacy " +
"is not set to public - check that in your Steam privacy settings and try again."
);
rl.close();
process.exit(1);
}
let friendProfiles = [];
const friendIds = friends.map((f) => f.steamid);
for (let i = 0; i < friendIds.length; i += 100) {
const batch = friendIds.slice(i, i + 100);
try {
const summaries = await fetchPlayerSummaries(batch);
friendProfiles.push(...summaries);
} catch (err) {
console.error("Could not load some friend profiles:", err.message);
}
}
friendProfiles.sort((a, b) => (a.personaname || "").localeCompare(b.personaname || ""));
let keepGoing = true;
while (keepGoing) {
let members;
try {
members = await fetchGuildMembers(guild.id);
} catch (err) {
console.error("Could not load server members:", err.message);
break;
}
members = members.filter((m) => !m.user.bot);
members.sort((a, b) => memberDisplayName(a).localeCompare(memberDisplayName(b)));
if (members.length === 0) {
console.error("No members found in that server.");
break;
}
const currentLinks = loadSteamLinks();
const member = await pickFromList(
rl,
"Which Discord server member?",
members,
(m) => memberDisplayName(m) + " (@" + m.user.username + ") - " +
(currentLinks[m.user.id] ? "(linked)" : "(unlinked)")
);
if (!member) break;
const friend = await pickFromList(
rl,
"Which Steam profile?",
friendProfiles,
(f) => (f.personaname || "unknown") + " - " + f.profileurl
);
if (!friend) continue;
const links = loadSteamLinks();
links[member.user.id] = friend.steamid;
saveSteamLinks(links);
console.log("Linked " + memberDisplayName(member) + " to " + (friend.personaname || friend.steamid) + ".");
const again = (await rl.question("Link another? (y/N): ")).trim().toLowerCase();
keepGoing = again === "y" || again === "yes";
}
console.log("Done.");
rl.close();
}
main();