From 38ae39440eb81c0f97619a786adbf69e30c0cd7a Mon Sep 17 00:00:00 2001 From: denshooter Date: Thu, 23 Apr 2026 23:10:59 +0200 Subject: [PATCH] feat: resolve game covers from Steam API when Discord has no image --- discord-presence-bot/index.js | 139 +++++++++++++++++++++++----------- next.config.ts | 8 ++ 2 files changed, 104 insertions(+), 43 deletions(-) diff --git a/discord-presence-bot/index.js b/discord-presence-bot/index.js index b2f15ef..e95783c 100644 --- a/discord-presence-bot/index.js +++ b/discord-presence-bot/index.js @@ -1,5 +1,5 @@ const { Client, GatewayIntentBits, ActivityType } = require("discord.js"); -const http = require("http"); +const http = require("https"); const TOKEN = process.env.DISCORD_BOT_TOKEN; const TARGET_USER_ID = process.env.DISCORD_USER_ID || "172037532370862080"; @@ -18,6 +18,53 @@ const client = new Client({ ], }); +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, @@ -25,52 +72,58 @@ let cachedData = { activities: [], }; -function updatePresence(guild) { +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"; - cachedData.activities = presence.activities - ? presence.activities - .filter((a) => a.type !== ActivityType.Custom) - .map((a) => { - 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`; - } - 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, - }; - } - return entry; - }) + const activities = presence.activities + ? presence.activities.filter((a) => a.type !== ActivityType.Custom) : []; - const spotifyActivity = presence.activities - ? presence.activities.find((a) => a.type === ActivityType.Listening && a.name === "Spotify") - : null; + 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; @@ -89,9 +142,9 @@ function updatePresence(guild) { } } -function updateAll() { +async function updateAll() { for (const guild of client.guilds.cache.values()) { - updatePresence(guild); + await updatePresence(guild); } } @@ -112,7 +165,7 @@ client.on("presenceUpdate", () => { updateAll(); }); -const server = http.createServer((req, res) => { +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 })); @@ -126,4 +179,4 @@ server.listen(PORT, () => { console.log(`HTTP endpoint listening on port ${PORT}`); }); -client.login(TOKEN); +client.login(TOKEN); \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index 064417a..97fd142 100644 --- a/next.config.ts +++ b/next.config.ts @@ -62,6 +62,14 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "media.discordapp.net", }, + { + protocol: "https", + hostname: "store.steampowered.com", + }, + { + protocol: "https", + hostname: "cdn.akamai.steamstatic.com", + }, { protocol: "https", hostname: "cms.dk0.dev",