const { Client, GatewayIntentBits, ActivityType } = require("discord.js"); const http = require("https"); const TOKEN = process.env.DISCORD_BOT_TOKEN; const TARGET_USER_ID = process.env.DISCORD_USER_ID || "172037532370862080"; const PORT = parseInt(process.env.BOT_PORT || "3001", 10); if (!TOKEN) { console.error("DISCORD_BOT_TOKEN is required"); process.exit(1); } const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildMembers, ], }); const steamCoverCache = new Map(); function fetchJSON(url) { return new Promise((resolve, reject) => { const req = http.get(url, { timeout: 5000 }, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { return fetchJSON(res.headers.location).then(resolve).catch(reject); } if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode}`)); } let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }); req.on("error", reject); req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); }); }); } async function resolveGameCover(name) { if (steamCoverCache.has(name)) return steamCoverCache.get(name); try { const search = await fetchJSON(`https://store.steampowered.com/api/storesearch/?term=${encodeURIComponent(name)}&l=english&cc=us`); const items = search?.response?.items; if (!items || items.length === 0) { steamCoverCache.set(name, null); return null; } const match = items.find((i) => i.name.toLowerCase() === name.toLowerCase()) || items[0]; const appId = match.id; const details = await fetchJSON(`https://store.steampowered.com/api/appdetails/?appids=${appId}&cc=us`); const appData = details?.[String(appId)]?.data; const coverUrl = appData?.header_image || null; steamCoverCache.set(name, coverUrl); console.log(`Resolved cover for "${name}": ${coverUrl}`); return coverUrl; } catch (e) { console.error(`Steam lookup failed for "${name}":`, e.message); steamCoverCache.set(name, null); return null; } } let cachedData = { discord_status: "offline", listening_to_spotify: false, spotify: null, activities: [], }; async function updatePresence(guild) { const member = guild.members.cache.get(TARGET_USER_ID); if (!member || !member.presence) return; const presence = member.presence; cachedData.discord_status = presence.status || "offline"; const activities = presence.activities ? presence.activities.filter((a) => a.type !== ActivityType.Custom) : []; const entries = []; for (const a of activities) { const entry = { name: a.name, type: a.type, details: a.details || null, state: a.state || null, image: null, applicationId: a.applicationId || null, }; if (a.applicationId && a.assets?.largeImage) { const imgKey = a.assets.largeImage; entry.image = imgKey.startsWith("mp:external") ? `https://media.discordapp.net/${imgKey.replace("mp:", "")}` : `https://cdn.discordapp.com/app-assets/${a.applicationId}/${imgKey}.png`; } else if (a.type === ActivityType.Playing && !entry.image) { const steamCover = await resolveGameCover(a.name); if (steamCover) entry.image = steamCover; } if (a.assets) { entry.assets = { large_image: a.assets.largeImage || null, large_text: a.assets.largeText || null, small_image: a.assets.smallImage || null, small_text: a.assets.smallText || null, }; } if (a.timestamps) { entry.timestamps = { start: a.timestamps.start?.toISOString() || null, end: a.timestamps.end?.toISOString() || null, }; } entries.push(entry); } cachedData.activities = entries; const spotifyActivity = activities.find((a) => a.type === ActivityType.Listening && a.name === "Spotify"); if (spotifyActivity && spotifyActivity.syncId) { cachedData.listening_to_spotify = true; cachedData.spotify = { song: spotifyActivity.details || "", artist: spotifyActivity.state ? spotifyActivity.state.replace(/; /g, "; ") : "", album: spotifyActivity.assets?.largeText || "", album_art_url: spotifyActivity.assets?.largeImage ? `https://i.scdn.co/image/${spotifyActivity.assets.largeImage.replace("spotify:", "")}` : null, track_id: spotifyActivity.syncId || null, }; } else { cachedData.listening_to_spotify = false; cachedData.spotify = null; } } async function updateAll() { for (const guild of client.guilds.cache.values()) { await updatePresence(guild); } } client.on("clientReady", () => { console.log(`Bot online as ${client.user.tag}`); client.user.setActivity("Watching Presence", { type: ActivityType.Watching }); for (const guild of client.guilds.cache.values()) { guild.members.fetch(TARGET_USER_ID).then(() => { console.log(`Found user ${TARGET_USER_ID} in guild ${guild.name}`); updateAll(); }).catch((err) => { console.error(`Could not fetch user ${TARGET_USER_ID} in guild ${guild.name}:`, err.message); }); } }); client.on("presenceUpdate", () => { updateAll(); }); const server = require("http").createServer((req, res) => { if (req.method === "GET" && req.url === "/presence") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ data: cachedData })); } else { res.writeHead(404); res.end("Not found"); } }); server.listen(PORT, () => { console.log(`HTTP endpoint listening on port ${PORT}`); }); client.login(TOKEN);