From 825e6ec5c7c921971c0c175d321a9f57bb392320 Mon Sep 17 00:00:00 2001 From: Clove Twilight Date: Fri, 26 Jun 2026 02:50:30 +0100 Subject: [PATCH] mrow --- src/discord/constants.ts | 37 ++++++++++++++++++++++++++++++++++++- src/presence.ts | 27 ++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/discord/constants.ts b/src/discord/constants.ts index 6d77c1b..871b345 100644 --- a/src/discord/constants.ts +++ b/src/discord/constants.ts @@ -118,6 +118,41 @@ export function emojiUrl(id: string, animated: boolean): string { return `${CDN}/emojis/${id}.${animated ? "gif" : "png"}?size=32`; } +/** + * Resolve a Rich Presence activity asset (`activity.assets.large_image` / + * `small_image`) to an actual image URL. Discord prefixes these with a + * scheme for "external" assets (streaming previews, proxied media); a bare + * hash with no prefix is a normal app-asset on the CDN. + */ + +// asset URL scheme handling adapted from pxseu/lanyard-ui, MPL-2.0 +export function activityAssetUrl(raw: string | undefined | null, applicationId?: string | null): string | null { + if (!raw) return null; + + const split = raw.split(":"); + if (split.length < 2) { + // Plain hash — standard Rich Presence app-asset, needs the app id. + return applicationId ? `${CDN}/app-assets/${applicationId}/${raw}.png` : null; + } + + switch (split[0]) { + case "mp": + // External Discord-proxied asset (e.g. attachment, embed image). + return `https://media.discordapp.net/${split.slice(1).join(":")}`; + case "twitch": + // Twitch stream preview. + return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${split[1]}.png`; + case "youtube": + // YouTube live-stream thumbnail. + return `https://i.ytimg.com/vi/${split[1]}/hqdefault_live.jpg`; + case "spotify": + // Spotify album/cover art. + return `https://i.scdn.co/image/${split[1]}`; + default: + return applicationId ? `${CDN}/app-assets/${applicationId}/${raw}.png` : null; + } +} + // ---- collectibles (Shop wishlist) --------------------------------------- // Discord product type ids -> our human-readable kind. See Userdoccers // "Collectible Product Type". 1000/2000/3000 are bundle/variants/external. @@ -156,4 +191,4 @@ export function nameplateStaticUrl(asset: string): string { } export function nameplateVideoUrl(asset: string): string { return `${CDN}/assets/collectibles/${asset}asset.webm`; -} +} \ No newline at end of file diff --git a/src/presence.ts b/src/presence.ts index 4bd47d8..736f92c 100644 --- a/src/presence.ts +++ b/src/presence.ts @@ -3,7 +3,7 @@ * ===================================================================== */ import type { DiscordStatus, UnifiedCustomStatus, UnifiedPresence, UnifiedSpotify } from "./types"; -import { emojiUrl } from "./discord/constants"; +import { activityAssetUrl, emojiUrl } from "./discord/constants"; export interface RawPresence { user: { id: string }; @@ -54,8 +54,29 @@ function extractCustomStatus(activities: any[]): UnifiedCustomStatus | null { }; } +/** + * Resolve an activity's `assets.large_image`/`small_image` (which can be a + * bare app-asset hash, or a scheme-prefixed external reference like + * "twitch:username" for streams) into actual, directly-loadable URLs. + * Leaves the original asset fields untouched and just adds the resolved + * `*_url` companions, so this is purely additive. + */ +function enrichActivityAssets(a: any): any { + if (!a || !a.assets) return a; + const appId = a.application_id ?? null; + return { + ...a, + assets: { + ...a.assets, + large_image_url: activityAssetUrl(a.assets.large_image, appId), + small_image_url: activityAssetUrl(a.assets.small_image, appId), + }, + }; +} + export function buildPresence(raw: RawPresence): UnifiedPresence { - const activities = Array.isArray(raw.activities) ? raw.activities : []; + const rawActivities = Array.isArray(raw.activities) ? raw.activities : []; + const activities = rawActivities.map(enrichActivityAssets); const status: DiscordStatus = raw.status ?? "offline"; const cs = raw.client_status || {}; const spotify = extractSpotify(activities); @@ -90,4 +111,4 @@ export function offlinePresence(userId: string): UnifiedPresence { spotify: null, updated_at: Date.now(), }; -} +} \ No newline at end of file