(function friends() { "use strict"; var FRIENDS = [ { title: "Fiancée", members: [ { name: "Aria", img: "/assets/friends/ari.png", tier: "wife", discordId: "1305215902685597797", link: null } ] }, { title: "Close Friends", members: [ { name: "Lilly", img: "/assets/friends/lilly.png", tier: "close", discordId: "908055723659898902", link: null }, { name: "Ria", img: "/assets/friends/ria.png", tier: "close", discordId: "1513506390088618145", link: null }, { name: "Camilla", img: "/assets/friends/camilla.png", tier: "close", discordId: "1110542429838397471", link: "https://cammy-the-cat.com" }, { name: "Saphie", img: "/assets/friends/saphie.png", tier: "close", discordId: "527709099186716673", link: null }, { name: "Ari", img: "/assets/friends/meowhem.png", tier: "close", discordId: "1474568910736199825", link: "https://a.stupid.cat" } ] }, { title: "Friends", members: [ { name: "Aureal", img: "/assets/friends/aureal.gif", tier: "friend", discordId: "1498977251134279900", link: "https://aureal.dev/" }, { name: "Fin", img: "/assets/friends/fin.png", tier: "friend", discordId: "867818211574808607", link: null }, { name: "Meme", img: "/assets/friends/meme.png", tier: "friend", discordId: "812998699667161098", link: null }, { name: "N", img: "/assets/friends/n.png", tier: "friend", discordId: "639399972407869450", link: null }, { name: "Lylla", img: "/assets/friends/lylla.png", tier: "friend", discordId: "1009889543878611016", link: null }, { name: "Simon", img: "/assets/friends/simon.png", tier: "friend", discordId: "758466783354814514", link: null } ] }, { title: "Other Peeps", subtitle: "You can request to be added here!", members: [ { name: "furi", img: "/assets/known-people/furi.png", tier: "known", discordId: "781445370177126401", link: "https://furina.is-a.dev"}, { name: "pokemon", img: "/assets/known-people/pokemon.png", tier: "known", discordId: "784443338627612673", link: "https://devmatei.com/" }, { name: "Sy", img: "/assets/known-people/sy.png", tier: "known", discordId: "1354212422839308469", link: "https://synapsexen.xyz/"} ] }, { title: "Alts", subtitle: "My other accounts, dead or alive", members: [ { name: "J", img: "/assets/alts/j.png", tier: "active-alt", discordId: "1500197577336033301", link: null}, { name: "Uzi", img: "/assets/alts/uzi.png", tier: "active-alt", discordId: "526626867973849123", link: null }, { name: "clovetwilight3", img: "/assets/alts/clovetwilight3.png", tier: "dead-alt", discordId: null, link: null }, { name: "estrogenhrt", img: "/assets/alts/estrogenhrt.png", tier: "dead-alt", discordId: null, link: null }, { name: "Clove <3", img: "/assets/alts/clove.png", tier: "dead-alt", discordId: "1125844710511104030", link: null}, { name: "Clove ⛤", img: "/assets/alts/butterfly.png", tier: "dead-alt", discordId: "514994021970739201", link: null }, { name: "Mrow", img: "/assets/alts/mrow.png", tier: "dead-alt", discordId: "219480349053288450", link: null } ] } ]; var REFRESH_MS = 60000; // re-poll live friends once a minute var root = document.getElementById("friends-root"); if (!root) return; // title → URL-safe anchor id, e.g. "Close Friends" -> "close-friends" function slugify(str) { return String(str == null ? "" : str) .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } // ---- helpers (mirrors discord.js) ----------------------------------- function esc(str) { return String(str == null ? "" : str) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function intToHex(n) { return "#" + (Number(n) >>> 0).toString(16).padStart(6, "0").slice(-6); } // Re-serve Discord CDN images cookieless via wsrv.nl (same trick as the // presence card) so no third-party cookies are set. function proxyImg(url, opts) { if (!url) return url; if (!/^https:\/\/(cdn|media)\.discordapp\.(com|net)\//.test(url)) return url; var src = url.replace(/^https:\/\//, ""); var q = "https://wsrv.nl/?url=" + encodeURIComponent(src) + "&output=webp"; if (opts && opts.w) q += "&w=" + opts.w + "&dpr=2&fit=cover"; return q; } function avatarUrl(u) { if (!u || !u.avatar) { var def = (Number(u && u.discriminator) || 0) % 5; return proxyImg("https://cdn.discordapp.com/embed/avatars/" + def + ".png"); } var ext = String(u.avatar).startsWith("a_") ? "gif" : "png"; return proxyImg("https://cdn.discordapp.com/avatars/" + u.id + "/" + u.avatar + "." + ext + "?size=128", { w: 96 }); } function bannerUrl(id, hash) { if (!id || !hash) return null; // Animated banners (a_) must be requested WITHOUT the .gif extension: // Discord's CDN throws HTTP 415 for some a_*.gif banners, but the // extension-less URL works (wsrv then re-serves it as cookieless webp). var animated = String(hash).startsWith("a_"); var url = "https://cdn.discordapp.com/banners/" + id + "/" + hash + (animated ? "" : ".png") + "?size=480"; return proxyImg(url, { w: 480 }); } // custom-status emoji → CDN url (custom emoji) or null (unicode emoji) function customEmojiUrl(e) { if (!e || !e.id) return null; return proxyImg("https://cdn.discordapp.com/emojis/" + e.id + (e.animated ? ".gif" : ".png") + "?size=32"); } // Render the Discord custom status (activity type 4): emoji + text. function renderCustomStatus(refs, activities) { if (!refs.custom) return; var c = (activities || []).find(function (a) { return a && a.type === 4; }); var text = c && c.state ? String(c.state) : ""; var emoji = c && c.emoji; if (!text && !(emoji && (emoji.id || emoji.name))) { refs.custom.hidden = true; refs.custom.innerHTML = ""; return; } var eu = emoji ? customEmojiUrl(emoji) : null; var emojiHtml = eu ? '' : (emoji && emoji.name ? '' + esc(emoji.name) + "" : ""); refs.custom.innerHTML = emojiHtml + (text ? '' + esc(text) + "" : ""); refs.custom.hidden = false; } // dstn.to badge list → small icon row (same source the presence card uses) function renderDstnBadges(badges) { if (!Array.isArray(badges)) return ""; return badges.map(function (b) { var img = '' + esc(b.description || b.id) + ''; return b.link ? '' + img + "" : img; }).join(""); } // Self-hosted badge list → small icon row. The self-hosted API ships a // ready-made `icon_url`; fall back to building it from `icon` if absent. function renderSelfBadges(badges) { if (!Array.isArray(badges)) return ""; return badges.map(function (b) { var url = b.icon_url || ("https://cdn.discordapp.com/badge-icons/" + esc(b.icon) + ".png"); var img = '' + esc(b.description || b.id) + ''; return b.link ? '' + img + "" : img; }).join(""); } // Self-hosted clan tag → guild badge + tag. Shape: {tag, badge_url}. function renderSelfClan(refs, clan) { if (!refs.tag) return; if (clan && clan.tag) { var b = clan.badge_url ? proxyImg(clan.badge_url) : null; refs.tag.innerHTML = (b ? '' : "") + '' + esc(clan.tag) + ""; refs.tag.hidden = false; } else { refs.tag.hidden = true; } } // Discord server (clan) tag — the little guild badge + tag next to a name function guildTagBadgeUrl(pg) { if (!pg || !pg.badge || !pg.identity_guild_id) return null; return proxyImg("https://cdn.discordapp.com/guild-tag-badges/" + pg.identity_guild_id + "/" + pg.badge + ".png?size=24"); } function renderClanTag(refs, pg) { if (!refs.tag) return; if (pg && pg.tag && pg.identity_enabled) { var b = guildTagBadgeUrl(pg); refs.tag.innerHTML = (b ? '' : "") + '' + esc(pg.tag) + ""; refs.tag.hidden = false; } else { refs.tag.hidden = true; } } // ---- build one card ------------------------------------------------- function buildCard(m) { var card = document.createElement("article"); card.className = "friend-card" + (m.tier ? " tier-" + m.tier : ""); card.dataset.status = "unconnected"; // until proven otherwise var nameTag = m.link ? "a" : "span"; var nameAttrs = m.link ? ' href="' + esc(m.link) + '" target="_blank" rel="noopener"' : ""; card.innerHTML = '
' + '
' + '' + '' + esc(m.name) + '' + '' + '' + '' + '' + '' + '<' + nameTag + ' class="fc-name' + (m.tier ? " " + m.tier : "") + '"' + nameAttrs + '>' + esc(m.name) + '' + '' + '' + '' + '' + '' + '' + '
'; return { el: card, pfp: card.querySelector(".fc-pfp"), deco: card.querySelector(".fc-deco"), statusDot: card.querySelector(".fc-status"), user: card.querySelector(".fc-user"), custom: card.querySelector(".fc-custom"), tag: card.querySelector(".fc-tag"), badges: card.querySelector(".fc-badges"), banner: card.querySelector(".fc-banner") }; } var STATUS_TITLE = { online: "Online", idle: "Idle", dnd: "Do Not Disturb", offline: "Offline" }; // ---- live data: self-hosted API (primary source) ------------------- // Returns a promise resolving to: // "full" → user + live presence rendered (no fallback needed) // "profile" → user rendered but presence was null (fall back to Lanyard // for the live status dot + current activity/custom status) // false → nothing usable (full Lanyard + dstn fallback) var SELF_BASE = "https://restful.doughmination.uk/v1/users/"; function loadSelfHosted(m, refs) { return fetch(SELF_BASE + m.discordId, { cache: "no-store" }) .then(function (r) { return r.ok ? r.json().catch(function () { return null; }) : null; }) .then(function (j) { if (!j || !j.success || !j.data || !j.data.user) return false; var u = j.data.user || {}; var p = j.data.presence; // may be null when the user isn't live-tracked var havePresence = !!p; // --- profile (rendered whenever we have a user object) --- if (u.avatar) { refs.pfp.src = avatarUrl({ id: u.id || m.discordId, avatar: u.avatar }); } // avatar decoration — load straight from Discord's CDN (the wsrv webp // proxy drops the APNG animation). API ships a ready `url`. if (refs.deco) { var deco = u.avatar_decoration; var dUrl = deco && (deco.url || (deco.asset ? "https://cdn.discordapp.com/avatar-decoration-presets/" + deco.asset + ".png" : null)); if (dUrl) { refs.deco.src = dUrl; refs.deco.hidden = false; } else { refs.deco.hidden = true; } } if (u.username) refs.user.textContent = "@" + u.username; renderSelfClan(refs, u.clan); if (Array.isArray(j.data.badges) && j.data.badges.length) { refs.badges.innerHTML = renderSelfBadges(j.data.badges); } // banner — rebuild from the raw hash via bannerUrl() rather than the // API's banner_url, which uses a `.gif` extension that 415s for some // animated banners. Falls back to accent colour. var bUrl = bannerUrl(u.id || m.discordId, u.banner); if (bUrl) { refs.banner.style.backgroundImage = "url('" + bUrl + "')"; refs.el.classList.add("has-banner"); } else if (typeof u.accent_color === "number") { refs.banner.style.background = intToHex(u.accent_color); refs.el.classList.add("has-banner"); } // --- live presence --- if (havePresence) { var status = p.status || (p.online ? "online" : "offline"); refs.el.dataset.status = status; refs.statusDot.title = STATUS_TITLE[status] || "Offline"; // activities follow the standard Discord shape (type 4 = custom status) renderCustomStatus(refs, p.activities); return "full"; } return "profile"; }) .catch(function () { return false; }); } // ---- live data: Lanyard (status + avatar) --------------------------- function loadLanyard(m, refs) { return fetch("https://api.lanyard.rest/v1/users/" + m.discordId) .then(function (r) { return r.json().catch(function () { return null; }); }) .then(function (j) { if (!j || !j.success || !j.data) { // in config but not in the Lanyard server → stays "unconnected" return false; } var d = j.data; var u = d.discord_user || {}; var status = d.discord_status || "offline"; refs.el.dataset.status = status; refs.statusDot.title = STATUS_TITLE[status] || "Offline"; if (u.avatar) refs.pfp.src = avatarUrl(u); // avatar decoration (animated APNG) — loaded straight from Discord's // CDN; the wsrv webp proxy fails on these and would drop the animation. var deco = u.avatar_decoration_data; if (refs.deco) { if (deco && deco.asset) { refs.deco.src = "https://cdn.discordapp.com/avatar-decoration-presets/" + deco.asset + ".png"; refs.deco.hidden = false; } else { refs.deco.hidden = true; } } if (u.username) refs.user.textContent = "@" + u.username; renderCustomStatus(refs, d.activities); renderClanTag(refs, u.primary_guild); return true; }) .catch(function () { // API down / network error → static fallback, blue dot return false; }); } // ---- live data: dstn.to (badges + Nitro banner) --------------------- function loadDstn(m, refs) { return fetch("https://dcdn.dstn.to/profile/" + m.discordId) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (j) { if (!j) return; var u = j.user || {}; var prof = j.user_profile || {}; if (Array.isArray(j.badges) && j.badges.length) { refs.badges.innerHTML = renderDstnBadges(j.badges); } var bUrl = bannerUrl(u.id || m.discordId, prof.banner || u.banner); if (bUrl) { refs.banner.style.backgroundImage = "url('" + bUrl + "')"; refs.el.classList.add("has-banner"); } else if (u.banner_color) { refs.banner.style.background = u.banner_color; refs.el.classList.add("has-banner"); } else if (typeof u.accent_color === "number") { refs.banner.style.background = intToHex(u.accent_color); refs.el.classList.add("has-banner"); } }) .catch(function () {}); } // Self-hosted API is the primary source; Lanyard + dstn.to are fallbacks. // Resolves to true if ANY source had data for this member, false if they // were not found anywhere — callers use that to stop re-polling 404s. function refreshMember(m, refs) { return loadSelfHosted(m, refs).then(function (result) { if (result === "full") return true; // self-host covered everything if (result === "profile") { // profile came from self-host but it isn't tracking live presence — // pull the live status dot + activity/custom status from Lanyard only. loadLanyard(m, refs); return true; } // result === false → self-host had nothing: original Lanyard + dstn path return loadLanyard(m, refs).then(function (live) { if (live) { loadDstn(m, refs); return true; } return false; // not found in self-host OR Lanyard }); }); } // Stop polling a member after this many consecutive "not found anywhere" // results. Keeps 404s (e.g. someone not in the self-host DB or Lanyard) // from spamming the console on every refresh. Any successful resolve resets // the counter, so a transient blip won't permanently drop a friend. Tunable. var GIVE_UP_AFTER = 3; function pollEntry(entry) { if (entry.stop) return; refreshMember(entry.m, entry.refs).then(function (resolved) { if (resolved) { entry.misses = 0; return; } entry.misses++; if (entry.misses >= GIVE_UP_AFTER) entry.stop = true; // give up for this session }); } // ---- render --------------------------------------------------------- var liveMembers = []; // {m, refs} for those with an id, for polling FRIENDS.forEach(function (group) { var section = document.createElement("section"); section.className = "section"; section.id = slugify(group.title); // anchor target, e.g. #alts var h2 = document.createElement("h2"); h2.className = "section-title"; h2.textContent = group.title; section.appendChild(h2); if (group.subtitle) { var sub = document.createElement("p"); sub.className = "section-subtitle"; sub.textContent = group.subtitle; section.appendChild(sub); } var grid = document.createElement("div"); grid.className = "friend-grid"; group.members.forEach(function (m) { var refs = buildCard(m); grid.appendChild(refs.el); // 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); } }); section.appendChild(grid); root.appendChild(section); }); // ---- jump to anchor (sections are built after page load) ------------ function scrollToHash() { var id = (location.hash || "").slice(1); if (!id) return; var target = document.getElementById(id); if (target) target.scrollIntoView(); } scrollToHash(); window.addEventListener("hashchange", scrollToHash); // ---- poll live members ---------------------------------------------- if (liveMembers.length) { setInterval(function () { if (document.hidden) return; liveMembers.forEach(function (x) { if (!x.stop) pollEntry(x); }); }, REFRESH_MS); } })();