diff --git a/cool-people/index.html b/cool-people/index.html index 8f24822..abcc2fd 100644 --- a/cool-people/index.html +++ b/cool-people/index.html @@ -38,6 +38,17 @@ + @@ -64,6 +75,12 @@
+ +

+ Presence data is served by + Doughmination Restful + by default, falling back to Lanyard and Dustin’s API. +

diff --git a/js/discord.js b/js/discord.js index da6cb6d..107e8a3 100644 --- a/js/discord.js +++ b/js/discord.js @@ -648,8 +648,108 @@ ws.addEventListener("error", () => { try { ws.close(); } catch (e) {} }); } - connect(); - loadDstn(); + // ---- data source: self-hosted API primary, Lanyard + dstn fallback ------ + // The Doughmination Restful API returns presence + full profile in one call, + // so it drives the card on its own. We poll it; only if it can't serve us + // (network error, or no live presence) do we fail over to the Lanyard + // websocket + dstn.to, the same chain the friends grid uses. + const SELF_BASE = "https://restful.doughmination.uk/v1/users/"; + const SELF_POLL_MS = 20000; // own-presence refresh cadence + const SELF_GIVE_UP = 3; // consecutive misses before failing over + let selfTimer = null; + let selfMisses = 0; + let lanyardActive = false; + + // self-host shape -> the Lanyard-shaped object render() already understands + function mapSelfHostToLanyard(j) { + const u = (j.data && j.data.user) || {}; + const p = (j.data && j.data.presence) || {}; + const plat = p.platform || {}; + const dec = u.avatar_decoration; + const clan = u.clan; + return { + discord_user: { + id: u.id || DISCORD_USER_ID, + username: u.username, + global_name: u.global_name, + display_name: u.display_name, + avatar: u.avatar, + avatar_decoration_data: (dec && dec.asset) ? { asset: dec.asset } : null, + primary_guild: (clan && clan.tag) + ? { tag: clan.tag, identity_enabled: true, badge: clan.badge, identity_guild_id: clan.guild_id } + : null, + public_flags: 0 + }, + discord_status: p.status || (p.online ? "online" : "offline"), + activities: p.activities || [], + listening_to_spotify: !!p.listening_to_spotify, + spotify: p.spotify || null, + active_on_discord_desktop: !!plat.desktop, + active_on_discord_mobile: !!plat.mobile, + active_on_discord_web: !!plat.web, + kv: {} + }; + } + + function renderFromSelfHost(j) { + const u = (j.data && j.data.user) || {}; + render(mapSelfHostToLanyard(j)); + // badges arrive pre-resolved (same consumer as dstn badges) + if (Array.isArray(j.data.badges) && j.data.badges.length) { + dstnBadges = j.data.badges; + paintBadges(); + } + // profile extras: banner rebuilt from the raw hash (dodges the animated + // .gif 415), plus bio / connections / pronouns straight from the API + applyBanner( + bannerUrl(u.id || DISCORD_USER_ID, u.banner), + (typeof u.accent_color === "number") ? intToHex(u.accent_color) : null + ); + renderBio(u.bio); + renderConnections(j.data.connected_accounts); + if (pronounsEl) { + if (u.pronouns) { pronounsEl.textContent = u.pronouns; pronounsEl.hidden = false; } + else pronounsEl.hidden = true; + } + } + + function loadSelfHosted() { + return fetch(SELF_BASE + DISCORD_USER_ID, { cache: "no-store" }) + .then(function (r) { return r.ok ? r.json().catch(function () { return null; }) : null; }) + .then(function (j) { + // need a live presence object to drive the card; else fall back + if (!j || !j.success || !j.data || !j.data.user || !j.data.presence) return false; + renderFromSelfHost(j); + return true; + }) + .catch(function () { return false; }); + } + + function startLanyardFallback() { + if (lanyardActive) return; + lanyardActive = true; + if (selfTimer) { clearInterval(selfTimer); selfTimer = null; } + connect(); + loadDstn(); + } + + function pollSelfHost() { + if (document.hidden) return; + loadSelfHosted().then(function (ok) { + if (ok) { selfMisses = 0; return; } + if (++selfMisses >= SELF_GIVE_UP) startLanyardFallback(); + }); + } + + // boot: Doughmination first; Lanyard/dstn only if it can't serve us + loadSelfHosted().then(function (ok) { + if (ok) { + selfMisses = 0; + selfTimer = setInterval(pollSelfHost, SELF_POLL_MS); + } else { + startLanyardFallback(); + } + }); document.addEventListener("visibilitychange", () => { if (!document.hidden && latest) updateTimes(); diff --git a/js/friends.js b/js/friends.js index 3f2ad35..067bd53 100644 --- a/js/friends.js +++ b/js/friends.js @@ -419,8 +419,9 @@ group.members.forEach(function (m) { var refs = buildCard(m); grid.appendChild(refs.el); - // dead alts are banned/retired — never pull live data for them - if (m.discordId && m.tier !== "dead-alt") { + // poll anyone with an ID — even "dead" alts can be semi-alive. Accounts + // that 404 everywhere are dropped automatically by the give-up logic. + if (m.discordId) { var entry = { m: m, refs: refs, misses: 0, stop: false }; liveMembers.push(entry); pollEntry(entry);