add css kv var, move away from ssr ( multiple queries ), remove colors kv var, add option to disable logging per route
All checks were successful
Code quality checks / biome (push) Successful in 15s

This commit is contained in:
creations 2025-04-25 21:20:08 -04:00
parent bd680ab607
commit 3b6c68c25d
Signed by: creations
GPG key ID: 8F553AA4320FC711
18 changed files with 571 additions and 667 deletions

View file

@ -72,7 +72,7 @@ These can be defined in Lanyard's KV store to customize the page:
| `stars` | Enables starfield background (`true` / `false`) | | `stars` | Enables starfield background (`true` / `false`) |
| `badges` | Enables badge fetching (`true` / `false`) | | `badges` | Enables badge fetching (`true` / `false`) |
| `readme` | URL to a README displayed on the profile (`.md` or `.html`) | | `readme` | URL to a README displayed on the profile (`.md` or `.html`) |
| `colors` | Enables avatar-based color theming (uses `node-vibrant`) | | `css` | URL to a css to change styles on the page, no import or require allowed |
--- ---

View file

@ -26,7 +26,10 @@
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "recommended": true,
"correctness": {
"noUnusedImports": "error"
}
} }
}, },
"javascript": { "javascript": {

View file

@ -22,7 +22,6 @@
"@creations.works/logger": "^1.0.3", "@creations.works/logger": "^1.0.3",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"isomorphic-dompurify": "^2.23.0", "isomorphic-dompurify": "^2.23.0",
"marked": "^15.0.7", "marked": "^15.0.7"
"node-vibrant": "^4.0.3"
} }
} }

View file

@ -40,6 +40,39 @@
} }
} }
#loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
transition: opacity 0.5s ease;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid var(--border-color);
border-top: 5px solid var(--progress-fill);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* actual styles below */ /* actual styles below */
body { body {
font-family: system-ui, sans-serif; font-family: system-ui, sans-serif;

29
public/css/root.css Normal file
View file

@ -0,0 +1,29 @@
:root {
--background: #0e0e10;
--readme-bg: #1a1a1d;
--card-bg: #1e1f22;
--card-hover-bg: #2a2a2d;
--border-color: #2e2e30;
--text-color: #ffffff;
--text-subtle: #bbb;
--text-secondary: #b5bac1;
--text-muted: #888;
--link-color: #00b0f4;
--button-bg: #5865f2;
--button-hover-bg: #4752c4;
--button-disabled-bg: #2d2e31;
--progress-bg: #f23f43;
--progress-fill: #5865f2;
--status-online: #23a55a;
--status-idle: #f0b232;
--status-dnd: #e03e3e;
--status-offline: #747f8d;
--status-streaming: #b700ff;
--blockquote-color: #aaa;
--code-bg: #2e2e30;
}

View file

@ -353,13 +353,45 @@ if (badgeURL && badgeURL !== "null" && userId) {
}); });
} }
function updatePresence(data) { async function updatePresence(data) {
const avatarWrapper = document.querySelector(".avatar-wrapper"); const cssLink = data.kv?.css;
const statusIndicator = avatarWrapper?.querySelector(".status-indicator");
const mobileIcon = avatarWrapper?.querySelector(".platform-icon.mobile-only");
const userInfo = document.querySelector(".user-info"); if (cssLink) {
const customStatus = userInfo?.querySelector(".custom-status"); try {
const res = await fetch(`/api/css?url=${encodeURIComponent(cssLink)}`);
if (!res.ok) throw new Error("Failed to fetch CSS");
const cssText = await res.text();
const style = document.createElement("style");
style.textContent = cssText;
document.head.appendChild(style);
} catch (err) {
console.error("Failed to load CSS", err);
}
}
const avatarWrapper = document.querySelector(".avatar-wrapper");
const avatarImg = document.querySelector(".avatar-wrapper .avatar");
const usernameEl = document.querySelector(".username");
if (avatarImg && data.discord_user?.avatar) {
const newAvatarUrl = `https://cdn.discordapp.com/avatars/${data.discord_user.id}/${data.discord_user.avatar}`;
avatarImg.src = newAvatarUrl;
avatarImg.classList.remove("hidden");
const siteIcon = document.getElementById("site-icon");
if (siteIcon) {
siteIcon.href = newAvatarUrl;
}
}
if (usernameEl) {
const username =
data.discord_user.global_name || data.discord_user.username;
usernameEl.textContent = username;
document.title = username;
}
const platform = { const platform = {
mobile: data.active_on_discord_mobile, mobile: data.active_on_discord_mobile,
@ -374,37 +406,51 @@ function updatePresence(data) {
status = data.discord_status; status = data.discord_status;
} }
if (statusIndicator) { let updatedStatusIndicator = avatarWrapper.querySelector(".status-indicator");
statusIndicator.className = `status-indicator ${status}`; const updatedMobileIcon = avatarWrapper.querySelector(
} ".platform-icon.mobile-only",
);
if (platform.mobile && !mobileIcon) { if (platform.mobile && !updatedMobileIcon) {
avatarWrapper.innerHTML += ` avatarWrapper.innerHTML += `
<svg class="platform-icon mobile-only ${status}" viewBox="0 0 1000 1500" fill="#43a25a" aria-label="Mobile" width="17" height="17"> <svg class="platform-icon mobile-only ${status}" viewBox="0 0 1000 1500" fill="#43a25a" aria-label="Mobile" width="17" height="17">
<path d="M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z"/> <path d="M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z"/>
</svg> </svg>
`; `;
} else if (!platform.mobile && mobileIcon) { } else if (!platform.mobile && updatedMobileIcon) {
mobileIcon.remove(); updatedMobileIcon.remove();
avatarWrapper.innerHTML += `<div class="status-indicator ${status}"></div>`; avatarWrapper.innerHTML += `<div class="status-indicator ${status}"></div>`;
} }
const custom = data.activities?.find((a) => a.type === 4); updatedStatusIndicator = avatarWrapper.querySelector(".status-indicator");
if (customStatus && custom) {
let emojiHTML = ""; if (updatedStatusIndicator) {
const emoji = custom.emoji; updatedStatusIndicator.className = `status-indicator ${status}`;
if (emoji?.id) {
const emojiUrl = `https://cdn.discordapp.com/emojis/${emoji.id}.${emoji.animated ? "gif" : "png"}`;
emojiHTML = `<img src="${emojiUrl}" alt="${emoji.name}" class="custom-emoji">`;
} else if (emoji?.name) {
emojiHTML = `${emoji.name} `;
}
customStatus.innerHTML = `
${emojiHTML}
${custom.state ? `<span class="custom-status-text">${custom.state}</span>` : ""}
`;
} }
const readmeSection = document.querySelector(".readme");
if (readmeSection && data.kv?.readme) {
const url = data.kv.readme;
try {
const res = await fetch(`/api/readme?url=${encodeURIComponent(url)}`);
if (!res.ok) throw new Error("Failed to fetch readme");
const text = await res.text();
readmeSection.innerHTML = `<div class="markdown-body">${text}</div>`;
readmeSection.classList.remove("hidden");
} catch (err) {
console.error("Failed to load README", err);
readmeSection.classList.add("hidden");
}
} else if (readmeSection) {
readmeSection.classList.add("hidden");
}
const custom = data.activities?.find((a) => a.type === 4);
updateCustomStatus(custom);
const filtered = data.activities const filtered = data.activities
?.filter((a) => a.type !== 4) ?.filter((a) => a.type !== 4)
?.sort((a, b) => { ?.sort((a, b) => {
@ -428,6 +474,47 @@ function updatePresence(data) {
updateElapsedAndProgress(); updateElapsedAndProgress();
getAllNoAsset(); getAllNoAsset();
} }
if (data.kv?.snow === "true") loadEffectScript("snow");
if (data.kv?.rain === "true") loadEffectScript("rain");
if (data.kv?.stars === "true") loadEffectScript("stars");
const loadingOverlay = document.getElementById("loading-overlay");
if (loadingOverlay) {
loadingOverlay.style.opacity = "0";
setTimeout(() => loadingOverlay.remove(), 500);
}
}
function updateCustomStatus(custom) {
const userInfoInner = document.querySelector(".user-info");
const customStatus = userInfoInner?.querySelector(".custom-status");
if (!userInfoInner) return;
if (custom) {
let emojiHTML = "";
if (custom.emoji?.id) {
const emojiUrl = `https://cdn.discordapp.com/emojis/${custom.emoji.id}.${custom.emoji.animated ? "gif" : "png"}`;
emojiHTML = `<img src="${emojiUrl}" alt="${custom.emoji.name}" class="custom-emoji">`;
} else if (custom.emoji?.name) {
emojiHTML = `${custom.emoji.name} `;
}
const html = `
<p class="custom-status">
${emojiHTML}${custom.state ? `<span class="custom-status-text">${custom.state}</span>` : ""}
</p>
`;
if (customStatus) {
customStatus.outerHTML = html;
} else {
userInfoInner.insertAdjacentHTML("beforeend", html);
}
} else if (customStatus) {
customStatus.remove();
}
} }
async function getAllNoAsset() { async function getAllNoAsset() {
@ -454,3 +541,13 @@ async function getAllNoAsset() {
} }
} }
} }
function loadEffectScript(effect) {
const existing = document.querySelector(`script[data-effect="${effect}"]`);
if (existing) return;
const script = document.createElement("script");
script.src = `/public/js/${effect}.js`;
script.dataset.effect = effect;
document.head.appendChild(script);
}

View file

@ -1,84 +1,82 @@
document.addEventListener("DOMContentLoaded", () => { const snowContainer = document.createElement("div");
const snowContainer = document.createElement("div"); snowContainer.style.position = "fixed";
snowContainer.style.position = "fixed"; snowContainer.style.top = "0";
snowContainer.style.top = "0"; snowContainer.style.left = "0";
snowContainer.style.left = "0"; snowContainer.style.width = "100vw";
snowContainer.style.width = "100vw"; snowContainer.style.height = "100vh";
snowContainer.style.height = "100vh"; snowContainer.style.pointerEvents = "none";
snowContainer.style.pointerEvents = "none"; document.body.appendChild(snowContainer);
document.body.appendChild(snowContainer);
const maxSnowflakes = 60; const maxSnowflakes = 60;
const snowflakes = []; const snowflakes = [];
const mouse = { x: -100, y: -100 }; const mouse = { x: -100, y: -100 };
document.addEventListener("mousemove", (e) => { document.addEventListener("mousemove", (e) => {
mouse.x = e.clientX; mouse.x = e.clientX;
mouse.y = e.clientY; mouse.y = e.clientY;
}); });
const createSnowflake = () => { const createSnowflake = () => {
if (snowflakes.length >= maxSnowflakes) { if (snowflakes.length >= maxSnowflakes) {
const oldestSnowflake = snowflakes.shift(); const oldestSnowflake = snowflakes.shift();
snowContainer.removeChild(oldestSnowflake); snowContainer.removeChild(oldestSnowflake);
}
const snowflake = document.createElement("div");
snowflake.classList.add("snowflake");
snowflake.style.position = "absolute";
snowflake.style.width = `${Math.random() * 3 + 2}px`;
snowflake.style.height = snowflake.style.width;
snowflake.style.background = "white";
snowflake.style.borderRadius = "50%";
snowflake.style.opacity = Math.random();
snowflake.style.left = `${Math.random() * window.innerWidth}px`;
snowflake.style.top = `-${snowflake.style.height}`;
snowflake.speed = Math.random() * 3 + 2;
snowflake.directionX = (Math.random() - 0.5) * 0.5;
snowflake.directionY = Math.random() * 0.5 + 0.5;
snowflakes.push(snowflake);
snowContainer.appendChild(snowflake);
};
setInterval(createSnowflake, 80);
function updateSnowflakes() {
snowflakes.forEach((snowflake, index) => {
const rect = snowflake.getBoundingClientRect();
const dx = rect.left + rect.width / 2 - mouse.x;
const dy = rect.top + rect.height / 2 - mouse.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 30) {
snowflake.directionX += (dx / distance) * 0.02;
snowflake.directionY += (dy / distance) * 0.02;
} else {
snowflake.directionX += (Math.random() - 0.5) * 0.01;
snowflake.directionY += (Math.random() - 0.5) * 0.01;
}
snowflake.style.left = `${rect.left + snowflake.directionX * snowflake.speed}px`;
snowflake.style.top = `${rect.top + snowflake.directionY * snowflake.speed}px`;
if (rect.top + rect.height >= window.innerHeight) {
snowContainer.removeChild(snowflake);
snowflakes.splice(index, 1);
}
if (
rect.left > window.innerWidth ||
rect.top > window.innerHeight ||
rect.left < 0
) {
snowflake.style.left = `${Math.random() * window.innerWidth}px`;
snowflake.style.top = `-${snowflake.style.height}`;
}
});
requestAnimationFrame(updateSnowflakes);
} }
updateSnowflakes(); const snowflake = document.createElement("div");
}); snowflake.classList.add("snowflake");
snowflake.style.position = "absolute";
snowflake.style.width = `${Math.random() * 3 + 2}px`;
snowflake.style.height = snowflake.style.width;
snowflake.style.background = "white";
snowflake.style.borderRadius = "50%";
snowflake.style.opacity = Math.random();
snowflake.style.left = `${Math.random() * window.innerWidth}px`;
snowflake.style.top = `-${snowflake.style.height}`;
snowflake.speed = Math.random() * 3 + 2;
snowflake.directionX = (Math.random() - 0.5) * 0.5;
snowflake.directionY = Math.random() * 0.5 + 0.5;
snowflakes.push(snowflake);
snowContainer.appendChild(snowflake);
};
setInterval(createSnowflake, 80);
function updateSnowflakes() {
snowflakes.forEach((snowflake, index) => {
const rect = snowflake.getBoundingClientRect();
const dx = rect.left + rect.width / 2 - mouse.x;
const dy = rect.top + rect.height / 2 - mouse.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 30) {
snowflake.directionX += (dx / distance) * 0.02;
snowflake.directionY += (dy / distance) * 0.02;
} else {
snowflake.directionX += (Math.random() - 0.5) * 0.01;
snowflake.directionY += (Math.random() - 0.5) * 0.01;
}
snowflake.style.left = `${rect.left + snowflake.directionX * snowflake.speed}px`;
snowflake.style.top = `${rect.top + snowflake.directionY * snowflake.speed}px`;
if (rect.top + rect.height >= window.innerHeight) {
snowContainer.removeChild(snowflake);
snowflakes.splice(index, 1);
}
if (
rect.left > window.innerWidth ||
rect.top > window.innerHeight ||
rect.left < 0
) {
snowflake.style.left = `${Math.random() * window.innerWidth}px`;
snowflake.style.top = `-${snowflake.style.height}`;
}
});
requestAnimationFrame(updateSnowflakes);
}
updateSnowflakes();

View file

@ -1,67 +1,65 @@
document.addEventListener("DOMContentLoaded", () => { const container = document.createElement("div");
const container = document.createElement("div"); container.style.position = "fixed";
container.style.position = "fixed"; container.style.top = "0";
container.style.top = "0"; container.style.left = "0";
container.style.left = "0"; container.style.width = "100vw";
container.style.width = "100vw"; container.style.height = "100vh";
container.style.height = "100vh"; container.style.pointerEvents = "none";
container.style.pointerEvents = "none"; container.style.overflow = "hidden";
container.style.overflow = "hidden"; container.style.zIndex = "9999";
container.style.zIndex = "9999"; document.body.appendChild(container);
document.body.appendChild(container);
for (let i = 0; i < 60; i++) { for (let i = 0; i < 60; i++) {
const star = document.createElement("div"); const star = document.createElement("div");
const size = Math.random() * 2 + 1; const size = Math.random() * 2 + 1;
star.style.position = "absolute"; star.style.position = "absolute";
star.style.width = `${size}px`; star.style.width = `${size}px`;
star.style.height = `${size}px`; star.style.height = `${size}px`;
star.style.background = "white"; star.style.background = "white";
star.style.borderRadius = "50%"; star.style.borderRadius = "50%";
star.style.opacity = Math.random(); star.style.opacity = Math.random();
star.style.top = `${Math.random() * 100}vh`; star.style.top = `${Math.random() * 100}vh`;
star.style.left = `${Math.random() * 100}vw`; star.style.left = `${Math.random() * 100}vw`;
star.style.animation = `twinkle ${Math.random() * 3 + 2}s infinite alternate ease-in-out`; star.style.animation = `twinkle ${Math.random() * 3 + 2}s infinite alternate ease-in-out`;
container.appendChild(star); container.appendChild(star);
} }
function createShootingStar() { function createShootingStar() {
const star = document.createElement("div"); const star = document.createElement("div");
star.classList.add("shooting-star"); star.classList.add("shooting-star");
let x = Math.random() * window.innerWidth * 0.8; let x = Math.random() * window.innerWidth * 0.8;
let y = Math.random() * window.innerHeight * 0.3; let y = Math.random() * window.innerHeight * 0.3;
const angle = (Math.random() * Math.PI) / 6 + Math.PI / 8; const angle = (Math.random() * Math.PI) / 6 + Math.PI / 8;
const speed = 10; const speed = 10;
const totalFrames = 60; const totalFrames = 60;
const deg = angle * (180 / Math.PI); const deg = angle * (180 / Math.PI);
star.style.left = `${x}px`;
star.style.top = `${y}px`;
star.style.transform = `rotate(${deg}deg)`;
container.appendChild(star);
let frame = 0;
function animate() {
x += Math.cos(angle) * speed;
y += Math.sin(angle) * speed;
star.style.left = `${x}px`; star.style.left = `${x}px`;
star.style.top = `${y}px`; star.style.top = `${y}px`;
star.style.transform = `rotate(${deg}deg)`; star.style.opacity = `${1 - frame / totalFrames}`;
container.appendChild(star); frame++;
if (frame < totalFrames) {
let frame = 0; requestAnimationFrame(animate);
function animate() { } else {
x += Math.cos(angle) * speed; container.removeChild(star);
y += Math.sin(angle) * speed;
star.style.left = `${x}px`;
star.style.top = `${y}px`;
star.style.opacity = `${1 - frame / totalFrames}`;
frame++;
if (frame < totalFrames) {
requestAnimationFrame(animate);
} else {
container.removeChild(star);
}
} }
animate();
} }
setInterval(() => { animate();
if (Math.random() < 0.3) createShootingStar(); }
}, 1000);
}); setInterval(() => {
if (Math.random() < 0.3) createShootingStar();
}, 1000);

View file

@ -1,49 +0,0 @@
import { fetch } from "bun";
import { Vibrant } from "node-vibrant/node";
export async function getImageColors(
url: string,
hex?: boolean,
): Promise<ImageColorResult | null> {
if (!url) return null;
if (typeof url !== "string" || !url.startsWith("http")) return null;
let res: Response;
try {
res = await fetch(url);
} catch {
return null;
}
if (!res.ok) return null;
const type: string | null = res.headers.get("content-type");
if (!type?.startsWith("image/")) return null;
const buffer: Buffer = Buffer.from(await res.arrayBuffer());
const base64: string = buffer.toString("base64");
const colors: Palette = await Vibrant.from(buffer).getPalette();
return {
img: `data:${type};base64,${base64}`,
colors: hex
? {
Muted: rgbToHex(safeRgb(colors.Muted)),
LightVibrant: rgbToHex(safeRgb(colors.LightVibrant)),
Vibrant: rgbToHex(safeRgb(colors.Vibrant)),
LightMuted: rgbToHex(safeRgb(colors.LightMuted)),
DarkVibrant: rgbToHex(safeRgb(colors.DarkVibrant)),
DarkMuted: rgbToHex(safeRgb(colors.DarkMuted)),
}
: colors,
};
}
function safeRgb(swatch: Swatch | null | undefined): number[] {
return Array.isArray(swatch?.rgb) ? (swatch.rgb ?? [0, 0, 0]) : [0, 0, 0];
}
export function rgbToHex(rgb: number[]): string {
return `#${rgb.map((c) => Math.round(c).toString(16).padStart(2, "0")).join("")}`;
}

View file

@ -1,97 +0,0 @@
import { lanyardConfig } from "@config/environment";
import { fetch } from "bun";
import DOMPurify from "isomorphic-dompurify";
import { marked } from "marked";
export async function getLanyardData(id?: string): Promise<LanyardResponse> {
let instance: string = lanyardConfig.instance;
if (instance.endsWith("/")) {
instance = instance.slice(0, -1);
}
if (!instance.startsWith("http://") && !instance.startsWith("https://")) {
instance = `https://${instance}`;
}
const url: string = `${instance}/v1/users/${id || lanyardConfig.userId}`;
const res: Response = await fetch(url, {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
if (!res.ok) {
return {
success: false,
error: {
code: "API_ERROR",
message: `Lanyard API responded with status ${res.status}`,
},
};
}
const data: LanyardResponse = (await res.json()) as LanyardResponse;
if (!data.success) {
return {
success: false,
error: {
code: "API_ERROR",
message: "Failed to fetch valid Lanyard data",
},
};
}
return data;
}
export async function handleReadMe(data: LanyardData): Promise<string | null> {
const userReadMe: string | null = data.kv?.readme;
const validExtension = /\.(md|markdown|txt|html?)$/i;
if (
!userReadMe ||
!userReadMe.startsWith("http") ||
!validExtension.test(userReadMe)
) {
return null;
}
try {
const res: Response = await fetch(userReadMe, {
headers: {
Accept: "text/markdown",
},
});
if (!res.ok) return null;
if (res.headers.has("content-length")) {
const size: number = Number.parseInt(
res.headers.get("content-length") || "0",
10,
);
if (size > 1024 * 100) return null;
}
const text: string = await res.text();
if (!text || text.length < 10) return null;
let html: string;
if (
userReadMe.toLowerCase().endsWith(".html") ||
userReadMe.toLowerCase().endsWith(".htm")
) {
html = text;
} else {
html = await marked.parse(text);
}
const safe: string | null = DOMPurify.sanitize(html);
return safe;
} catch {
return null;
}
}

View file

@ -1,7 +1,5 @@
import { getImageColors } from "@/helpers/colors";
import { badgeApi, lanyardConfig } from "@config/environment"; import { badgeApi, lanyardConfig } from "@config/environment";
import { renderEjsTemplate } from "@helpers/ejs"; import { renderEjsTemplate } from "@helpers/ejs";
import { getLanyardData, handleReadMe } from "@helpers/lanyard";
const routeDef: RouteDef = { const routeDef: RouteDef = {
method: "GET", method: "GET",
@ -11,67 +9,18 @@ const routeDef: RouteDef = {
async function handler(request: ExtendedRequest): Promise<Response> { async function handler(request: ExtendedRequest): Promise<Response> {
const { id } = request.params; const { id } = request.params;
const data: LanyardResponse = await getLanyardData( const instance = lanyardConfig.instance
id || lanyardConfig.userId, .replace(/^https?:\/\//, "")
); .replace(/\/$/, "");
if (!data.success) {
return await renderEjsTemplate("error", {
message: data.error.message,
});
}
let instance: string = lanyardConfig.instance;
if (instance.endsWith("/")) {
instance = instance.slice(0, -1);
}
if (instance.startsWith("http://") || instance.startsWith("https://")) {
instance = instance.slice(instance.indexOf("://") + 3);
}
const presence: LanyardData = data.data;
const readme: string | Promise<string> | null = await handleReadMe(presence);
let status: string;
if (presence.activities.some((activity) => activity.type === 1)) {
status = "streaming";
} else {
status = presence.discord_status;
}
const avatar: string = presence.discord_user.avatar
? `https://cdn.discordapp.com/avatars/${presence.discord_user.id}/${presence.discord_user.avatar}`
: `https://cdn.discordapp.com/embed/avatars/${Math.floor(Math.random() * 5)}.png`;
let colors: ImageColorResult | null = null;
if (presence.kv.colors === "true") {
colors = await getImageColors(avatar, true);
}
const ejsTemplateData: EjsTemplateData = { const ejsTemplateData: EjsTemplateData = {
title: presence.discord_user.global_name || presence.discord_user.username, title: "Discord Profile",
username: username: "",
presence.discord_user.global_name || presence.discord_user.username, user: { id: id || lanyardConfig.userId },
status: status,
activities: presence.activities,
user: presence.discord_user,
platform: {
desktop: presence.active_on_discord_desktop,
mobile: presence.active_on_discord_mobile,
web: presence.active_on_discord_web,
},
instance: instance, instance: instance,
readme: readme, badgeApi: badgeApi,
badgeApi: presence.kv.badges !== "false" ? badgeApi : null, avatar: `https://cdn.discordapp.com/embed/avatars/${Math.floor(Math.random() * 5)}.png`,
avatar: avatar, extraOptions: {},
colors: colors?.colors ?? {},
extraOptions: {
snow: presence.kv.snow === "true",
rain: presence.kv.rain === "true",
stars: presence.kv.stars === "true",
},
}; };
return await renderEjsTemplate("index", ejsTemplateData); return await renderEjsTemplate("index", ejsTemplateData);

View file

@ -1,38 +0,0 @@
import { getImageColors } from "@helpers/colors";
const routeDef: RouteDef = {
method: "GET",
accepts: "*/*",
returns: "application/json",
};
async function handler(request: ExtendedRequest): Promise<Response> {
const { url } = request.query;
const result: ImageColorResult | null = await getImageColors(url, true);
await getImageColors(url);
if (!result) {
return new Response("Invalid URL", {
status: 400,
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"Access-Control-Allow-Origin": "*",
},
});
}
const compressed: Uint8Array = Bun.gzipSync(JSON.stringify(result));
return new Response(compressed, {
headers: {
"Content-Type": "application/json",
"Content-Encoding": "gzip",
"Cache-Control": "public, max-age=31536000, immutable",
"Access-Control-Allow-Origin": "*",
},
});
}
export { handler, routeDef };

90
src/routes/api/css.ts Normal file
View file

@ -0,0 +1,90 @@
import { fetch } from "bun";
const routeDef: RouteDef = {
method: "GET",
accepts: "*/*",
returns: "*/*",
log: false,
};
async function handler(request: ExtendedRequest): Promise<Response> {
const { url } = request.query;
if (!url || !url.startsWith("http") || !/\.css$/i.test(url)) {
return Response.json(
{
success: false,
error: {
code: "INVALID_URL",
message: "Invalid URL provided",
},
},
{ status: 400 },
);
}
const res = await fetch(url, {
headers: {
Accept: "text/css",
},
});
if (!res.ok) {
return Response.json(
{
success: false,
error: {
code: "FETCH_FAILED",
message: "Failed to fetch CSS file",
},
},
{ status: 400 },
);
}
if (res.headers.has("content-length")) {
const size = Number.parseInt(res.headers.get("content-length") || "0", 10);
if (size > 1024 * 50) {
return Response.json(
{
success: false,
error: {
code: "FILE_TOO_LARGE",
message: "CSS file exceeds 50KB limit",
},
},
{ status: 400 },
);
}
}
const text = await res.text();
if (!text || text.length < 5) {
return Response.json(
{
success: false,
error: {
code: "INVALID_CONTENT",
message: "CSS content is too small or invalid",
},
},
{ status: 400 },
);
}
const sanitized = text
.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
.replace(/@import\s+url\(['"]?(.*?)['"]?\);?/gi, "");
return new Response(sanitized, {
headers: {
"Content-Type": "text/css",
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
Pragma: "no-cache",
Expires: "0",
},
status: 200,
});
}
export { handler, routeDef };

105
src/routes/api/readme.ts Normal file
View file

@ -0,0 +1,105 @@
import { fetch } from "bun";
import DOMPurify from "isomorphic-dompurify";
import { marked } from "marked";
const routeDef: RouteDef = {
method: "GET",
accepts: "*/*",
returns: "*/*",
log: false,
};
async function handler(request: ExtendedRequest): Promise<Response> {
const { url } = request.query;
if (
!url ||
!url.startsWith("http") ||
!/\.(md|markdown|txt|html?)$/i.test(url)
) {
return Response.json(
{
success: false,
error: {
code: "INVALID_URL",
message: "Invalid URL provided",
},
},
{ status: 400 },
);
}
const res = await fetch(url, {
headers: {
Accept: "text/markdown",
},
});
if (!res.ok) {
return Response.json(
{
success: false,
error: {
code: "FETCH_FAILED",
message: "Failed to fetch the file",
},
},
{ status: 400 },
);
}
if (res.headers.has("content-length")) {
const size = Number.parseInt(res.headers.get("content-length") || "0", 10);
if (size > 1024 * 100) {
return Response.json(
{
success: false,
error: {
code: "FILE_TOO_LARGE",
message: "File size exceeds 100KB limit",
},
},
{ status: 400 },
);
}
}
const text = await res.text();
if (!text || text.length < 10) {
return Response.json(
{
success: false,
error: {
code: "INVALID_CONTENT",
message: "File is too small or invalid",
},
},
{ status: 400 },
);
}
let html: string;
if (
url.toLowerCase().endsWith(".html") ||
url.toLowerCase().endsWith(".htm")
) {
html = text;
} else {
html = await marked.parse(text);
}
const safe = DOMPurify.sanitize(html) || "";
return new Response(safe, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
Pragma: "no-cache",
Expires: "0",
},
status: 200,
});
}
export { handler, routeDef };

View file

@ -109,6 +109,8 @@ class ServerHandler {
let requestBody: unknown = {}; let requestBody: unknown = {};
let response: Response; let response: Response;
let logRequest = true;
if (match) { if (match) {
const { filePath, params, query } = match; const { filePath, params, query } = match;
@ -119,6 +121,8 @@ class ServerHandler {
? contentType.split(";")[0].trim() ? contentType.split(";")[0].trim()
: null; : null;
logRequest = routeModule.routeDef.log !== false;
if ( if (
routeModule.routeDef.needsBody === "json" && routeModule.routeDef.needsBody === "json" &&
actualContentType === "application/json" actualContentType === "application/json"
@ -227,28 +231,30 @@ class ServerHandler {
); );
} }
const headers = request.headers; if (logRequest) {
let ip = server.requestIP(request)?.address; const headers = request.headers;
let ip = server.requestIP(request)?.address;
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") { if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
ip = ip =
headers.get("CF-Connecting-IP")?.trim() || headers.get("CF-Connecting-IP")?.trim() ||
headers.get("X-Real-IP")?.trim() || headers.get("X-Real-IP")?.trim() ||
headers.get("X-Forwarded-For")?.split(",")[0].trim() || headers.get("X-Forwarded-For")?.split(",")[0].trim() ||
"unknown"; "unknown";
}
logger.custom(
`[${request.method}]`,
`(${response.status})`,
[
request.url,
`${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`,
ip || "unknown",
],
"90",
);
} }
logger.custom(
`[${request.method}]`,
`(${response.status})`,
[
request.url,
`${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`,
ip || "unknown",
],
"90",
);
return response; return response;
} }
} }

View file

@ -5,230 +5,48 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<% <title>Discord Presence</title>
const displayName = username.endsWith('s') ? `${username}'` : `${username}'s`;
const profileUrl = `https://discord.com/users/${user.id}`;
%>
<meta property="og:title" content="<%= displayName %> Discord Presence">
<meta property="og:description" content="<%= activities?.[0]?.state || 'See what ' + displayName + ' is doing on Discord.' %>">
<meta property="og:image" content="https://cdn.discordapp.com/avatars/<%= user.id %>/<%= user.avatar %>">
<meta name="twitter:card" content="summary_large_image">
<title><%= title %></title>
<link rel="stylesheet" href="/public/css/index.css"> <link rel="stylesheet" href="/public/css/index.css">
<script src="/public/js/index.js" defer></script> <link rel="stylesheet" href="/public/css/root.css">
<% if (extraOptions.snow) { %>
<script src="/public/js/snow.js" defer></script>
<% } %>
<% if(extraOptions.rain) { %>
<script src="/public/js/rain.js" defer></script>
<% } %>
<% if (extraOptions.stars) { %>
<script src="/public/js/stars.js" defer></script>
<% } %>
<meta name="color-scheme" content="dark"> <meta name="color-scheme" content="dark">
<link rel="icon" id="site-icon" type="image/png">
<link rel="icon" href="<%= avatar %>" type="image/png">
</head> </head>
<body> <body>
<%- include("partial/style.ejs") %> <div id="loading-overlay">
<a href="https://git.creations.works/creations/profilePage" tareget="_blank" rel="noopener noreferrer"> <div class="loading-spinner"></div>
</div>
<a href="https://git.creations.works/creations/profilePage" target="_blank" rel="noopener noreferrer">
<img class="open-source-logo" src="/public/assets/forgejo_logo.svg" alt="Forgejo Logo" style="opacity: 0.5;"> <img class="open-source-logo" src="/public/assets/forgejo_logo.svg" alt="Forgejo Logo" style="opacity: 0.5;">
</a> </a>
<div class="user-card"> <div class="user-card">
<div class="avatar-status-wrapper"> <div class="avatar-status-wrapper">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<img class="avatar" src="<%= avatar %>" alt="Avatar"> <img class="avatar hidden" src="">
<% if (user.avatar_decoration_data) { %> <div class="status-indicator offline"></div>
<img class="decoration" src="https://cdn.discordapp.com/avatar-decoration-presets/<%= user.avatar_decoration_data.asset %>" alt="Decoration">
<% } %>
<% if (platform.mobile) { %>
<svg class="platform-icon mobile-only <%= status %>" viewBox="0 0 1000 1500" aria-label="Mobile" width="17" height="17">
<path d="M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z" />
</svg>
<% } else { %>
<div class="status-indicator <%= status %>"></div>
<% } %>
</div> </div>
<div class="user-info"> <div class="user-info">
<div class="user-info-inner"> <div class="user-info-inner">
<h1><%= username %></h1> <h1 class="username"></h1>
<% if (user.clan && user.clan.tag) { %>
<div class="clan-badge">
<img src="https://cdn.discordapp.com/clan-badges/<%= user.clan.identity_guild_id %>/<%= user.clan.badge %>" alt="Clan Badge" class="clan-badge">
<span class="clan-name"><%= user.clan.tag %></span>
</div>
<% } %>
</div> </div>
<% if (activities.length && activities[0].type === 4) {
const emoji = activities[0].emoji;
const isCustom = emoji?.id;
const emojiUrl = isCustom
? `https://cdn.discordapp.com/emojis/${emoji.id}.${emoji.animated ? "gif" : "png"}`
: null;
%>
<p class="custom-status">
<% if (isCustom && emojiUrl) { %>
<img src="<%= emojiUrl %>" alt="<%= emoji.name %>" class="custom-emoji">
<% } else if (emoji?.name) { %>
<%= emoji.name %>
<% } %>
<% if (activities[0].state) { %>
<span class="custom-status-text"><%= activities[0].state %></span>
<% } %>
</p>
<% } %>
</div> </div>
</div> </div>
</div> </div>
<% if(badgeApi) { %> <div id="badges" class="badges hidden"></div>
<div id="badges" class="badges"></div>
<% } %>
<%
let filtered = activities
.filter(a => a.type !== 4)
.sort((a, b) => {
const priority = { 2: 0, 1: 1, 3: 2 };
const aPriority = priority[a.type] ?? 99;
const bPriority = priority[b.type] ?? 99;
return aPriority - bPriority;
});
%>
<h2 class="activity-header <%= filtered.length === 0 ? 'hidden' : '' %>">Activities</h2> <h2 class="activity-header hidden">Activities</h2>
<ul class="activities"> <ul class="activities"></ul>
<% filtered.forEach(activity => {
const start = activity.timestamps?.start;
const end = activity.timestamps?.end;
const now = Date.now();
const elapsed = start ? now - start : 0;
const total = (start && end) ? end - start : null;
const progress = (total && elapsed > 0) ? Math.min(100, Math.floor((elapsed / total) * 100)) : null;
let art = null; <section class="readme hidden">
let smallArt = null; <div class="markdown-body"></div>
function resolveActivityImage(img, applicationId) {
if (!img) return null;
if (img.startsWith("mp:external/")) {
return `https://media.discordapp.net/external/${img.slice("mp:external/".length)}`;
}
if (img.includes("/https/")) {
const clean = img.split("/https/")[1];
return clean ? `https://${clean}` : null;
}
if (img.startsWith("spotify:")) {
return `https://i.scdn.co/image/${img.split(":")[1]}`;
}
return `https://cdn.discordapp.com/app-assets/${applicationId}/${img}.png`;
}
if (activity.assets) {
art = resolveActivityImage(activity.assets.large_image, activity.application_id);
smallArt = resolveActivityImage(activity.assets.small_image, activity.application_id);
}
const activityTypeMap = {
0: "Playing",
1: "Streaming",
2: "Listening",
3: "Watching",
4: "Custom Status",
5: "Competing",
};
const activityType = activity.name === "Spotify"
? "Listening to Spotify"
: activity.name === "TIDAL"
? "Listening to TIDAL"
: activityTypeMap[activity.type] || "Playing";
%>
<li class="activity">
<div class="activity-wrapper">
<div class="activity-type-wrapper">
<span class="activity-type-label" data-type="<%= activity.type %>"><%= activityType %></span>
<% if (start && progress === null) { %>
<div class="activity-timestamp" data-start="<%= start %>">
<% const started = new Date(start); %>
<span>
Since: <%= started.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) %> <span class="elapsed"></span>
</span>
</div>
<% } %>
</div>
<div class="activity-wrapper-inner">
<div class="activity-image-wrapper <%= art ?? "no-asset" %>">
<img class="activity-image <%= art ? '' : 'no-asset' %>" src="<%= art || '' %>" data-name="<%= activity.name.replace(/"/g, '&quot;') %>" <%= activity.assets?.large_text ? `title="${activity.assets.large_text}"` : '' %>>
<img class="activity-image-small <%= smallArt ?? "no-asset" %>" src="<%= smallArt %>" <%= activity.assets?.small_text ? `title="${activity.assets.small_text}"` : '' %>>
</div>
<div class="activity-content">
<div class="inner-content">
<%
const isMusic = activity.type === 2 || activity.type === 3;
const primaryLine = isMusic ? activity.details : activity.name;
const secondaryLine = isMusic ? activity.state : activity.details;
const tertiaryLine = isMusic ? activity.assets?.large_text : activity.state;
%>
<div class="activity-top">
<div class="activity-header <%= progress !== null ? 'no-timestamp' : '' %>">
<span class="activity-name"><%= primaryLine %></span>
</div>
<% if (secondaryLine) { %>
<div class="activity-detail"><%= secondaryLine %></div>
<% } %>
<% if (tertiaryLine) { %>
<div class="activity-detail"><%= tertiaryLine %></div>
<% } %>
</div>
<div class="activity-bottom">
<% if (activity.buttons && activity.buttons.length > 0) { %>
<div class="activity-buttons">
<% activity.buttons.forEach((button, index) => {
const buttonLabel = typeof button === 'string' ? button : button.label;
let buttonUrl = null;
if (typeof button === 'object' && button.url) {
buttonUrl = button.url;
} else if (index === 0 && activity.url) {
buttonUrl = activity.url;
}
%>
<% if (buttonUrl) { %>
<a href="<%= buttonUrl %>" class="activity-button" target="_blank" rel="noopener noreferrer"><%= buttonLabel %></a>
<% } %>
<% }) %>
</div>
<% } %>
</div>
</div>
</div>
</div>
<% if (progress !== null) { %>
<div class="progress-bar" data-start="<%= start %>" data-end="<%= end %>">
<div class="progress-fill" <%= progress !== null ? `style="width: ${progress}%"` : '' %>></div>
</div>
<% if (start && end) { %>
<div class="progress-time-labels" data-start="<%= start %>" data-end="<%= end %>">
<span class="progress-current"></span>
<span class="progress-total"><%= Math.floor((end - start) / 60000) %>:<%= String(Math.floor(((end - start) % 60000) / 1000)).padStart(2, "0") %></span>
</div>
<% } %>
<% } %>
</div>
</li>
<% }); %>
</ul>
<% if (readme) { %>
<section class="readme">
<div class="markdown-body"><%- readme %></div>
</section> </section>
<% } %>
<script src="/public/js/index.js"></script>
</body> </body>
</html> </html>

View file

@ -1,31 +0,0 @@
<style>
:root {
--background: <%= colors.DarkVibrant || '#0e0e10' %>;
--readme-bg: <%= colors.DarkMuted || '#1a1a1d' %>;
--card-bg: <%= colors.DarkMuted || '#1e1f22' %>;
--card-hover-bg: <%= colors.Muted || '#2a2a2d' %>;
--border-color: <%= colors.Muted || '#2e2e30' %>;
--text-color: #ffffff;
--text-subtle: #bbb;
--text-secondary: #b5bac1;
--text-muted: #888;
--link-color: <%= colors.Vibrant || '#00b0f4' %>;
--button-bg: <%= colors.Vibrant || '#5865f2' %>;
--button-hover-bg: <%= colors.LightVibrant || '#4752c4' %>;
--button-disabled-bg: #2d2e31;
--progress-bg: <%= colors.DarkVibrant || '#f23f43' %>;
--progress-fill: <%= colors.Vibrant || '#5865f2' %>;
--status-online: #23a55a;
--status-idle: #f0b232;
--status-dnd: #e03e3e;
--status-offline: #747f8d;
--status-streaming: #b700ff;
--blockquote-color: #aaa;
--code-bg: <%= colors.Muted || '#2e2e30' %>;
}
</style>

8
types/routes.d.ts vendored
View file

@ -3,6 +3,7 @@ type RouteDef = {
accepts: string | null | string[]; accepts: string | null | string[];
returns: string; returns: string;
needsBody?: "multipart" | "json"; needsBody?: "multipart" | "json";
log?: boolean;
}; };
type RouteModule = { type RouteModule = {
@ -13,10 +14,3 @@ type RouteModule = {
) => Promise<Response> | Response; ) => Promise<Response> | Response;
routeDef: RouteDef; routeDef: RouteDef;
}; };
type Palette = Awaited<ReturnType<typeof Vibrant.prototype.getPalette>>;
type Swatch = Awaited<ReturnType<typeof Vibrant.prototype.getSwatches>>;
type ImageColorResult = {
img: string;
colors: Palette | Record<string, string>;
};