From 30191a5b1fa3b9875df28a2f5df52e1474330974 Mon Sep 17 00:00:00 2001
From: creations <creations@creations.works>
Date: Sat, 5 Apr 2025 09:18:54 -0400
Subject: [PATCH] add readme support

---
 package.json           |   3 +-
 public/css/index.css   | 101 ++++++++++++++++++++++++++++++++++++++++-
 src/helpers/lanyard.ts |  47 +++++++++++++++++++
 src/routes/[id].ts     |  12 +++--
 src/routes/index.ts    |  14 +++---
 src/views/index.ejs    |  13 +++++-
 6 files changed, 176 insertions(+), 14 deletions(-)

diff --git a/package.json b/package.json
index 8240d4f..c272712 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
 		"typescript": "^5.8.2"
 	},
 	"dependencies": {
-		"ejs": "^3.1.10"
+		"ejs": "^3.1.10",
+		"marked": "^15.0.7"
 	}
 }
diff --git a/public/css/index.css b/public/css/index.css
index d6b1db9..9271040 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -230,7 +230,8 @@ ul {
 	}
 
 	body {
-		padding: 1rem;
+		padding: 0;
+		margin: 0;
 		align-items: stretch;
 	}
 
@@ -301,6 +302,7 @@ ul {
 		align-items: center;
 		text-align: center;
 		padding: 1rem;
+		border-radius:0;
 	}
 
 	.activity-art {
@@ -329,7 +331,104 @@ ul {
 	.activity-detail {
 		text-align: center;
 	}
+
+	.progress-time-labels {
+		justify-content: space-between;
+		font-size: 0.7rem;
+		margin-top: 0.25rem;
+		width: 100%;
+	}
 }
 
+/* readme :p */
+.readme {
+	max-width: 600px;
+	width: 100%;
+	background: #1a1a1d;
+	padding: 1.5rem;
+	border-radius: 8px;
+	box-shadow: 0 0 0 1px #2e2e30;
 
+	box-sizing: border-box;
+	overflow: hidden;
+	overflow-y: auto;
+}
 
+.readme h2 {
+	margin-top: 0;
+	color: #00b0f4;
+}
+
+.markdown-body {
+	font-size: 1rem;
+	line-height: 1.6;
+	color: #ddd;
+}
+
+.markdown-body h1,
+.markdown-body h2,
+.markdown-body h3,
+.markdown-body h4,
+.markdown-body h5,
+.markdown-body h6 {
+	color: #ffffff;
+	margin-top: 1.25rem;
+	margin-bottom: 0.5rem;
+}
+
+.markdown-body p {
+	margin: 0.5rem 0;
+}
+
+.markdown-body a {
+	color: #00b0f4;
+	text-decoration: none;
+}
+
+.markdown-body a:hover {
+	text-decoration: underline;
+}
+
+.markdown-body code {
+	background: #2e2e30;
+	padding: 0.2em 0.4em;
+	border-radius: 4px;
+	font-family: monospace;
+	color: #f8f8f2;
+}
+
+.markdown-body pre {
+	background: #2e2e30;
+	padding: 1rem;
+	border-radius: 6px;
+	overflow-x: auto;
+	font-family: monospace;
+	color: #f8f8f2;
+}
+
+.markdown-body ul,
+.markdown-body ol {
+	padding-left: 1.5rem;
+	margin: 0.5rem 0;
+}
+
+.markdown-body blockquote {
+	border-left: 4px solid #00b0f4;
+	padding-left: 1rem;
+	color: #aaa;
+	margin: 1rem 0;
+}
+
+@media (max-width: 600px) {
+	.readme {
+		width: 100%;
+		padding: 1rem;
+
+		margin-top: 1rem;
+		border-radius: 0;
+	}
+
+	.markdown-body {
+		font-size: 0.95rem;
+	}
+}
diff --git a/src/helpers/lanyard.ts b/src/helpers/lanyard.ts
index 6592d7b..cac8c75 100644
--- a/src/helpers/lanyard.ts
+++ b/src/helpers/lanyard.ts
@@ -1,5 +1,6 @@
 import { lanyardConfig } from "@config/environment";
 import { fetch } from "bun";
+import { marked } from "marked";
 
 export async function getLanyardData(id?: string): Promise<LanyardResponse> {
 	let instance: string = lanyardConfig.instance;
@@ -44,3 +45,49 @@ export async function getLanyardData(id?: string): Promise<LanyardResponse> {
 
 	return data;
 }
+
+export async function handleReadMe(data: LanyardData): Promise<string | null> {
+	const userReadMe: string | null = data.kv?.readme;
+	console.log("userReadMe", userReadMe);
+
+	if (
+		!userReadMe ||
+		!userReadMe.toLowerCase().endsWith("readme.md") ||
+		!userReadMe.startsWith("http")
+	) {
+		return null;
+	}
+
+	try {
+		const res: Response = await fetch(userReadMe, {
+			headers: {
+				Accept: "text/markdown",
+			},
+		});
+
+		const contentType: string = res.headers.get("content-type") || "";
+		if (
+			!res.ok ||
+			!(
+				contentType.includes("text/markdown") ||
+				contentType.includes("text/plain")
+			)
+		)
+			return null;
+
+		if (res.headers.has("content-length")) {
+			const size: 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;
+
+		return marked.parse(text);
+	} catch {
+		return null;
+	}
+}
diff --git a/src/routes/[id].ts b/src/routes/[id].ts
index e43c73d..30412e9 100644
--- a/src/routes/[id].ts
+++ b/src/routes/[id].ts
@@ -1,6 +1,6 @@
 import { lanyardConfig } from "@config/environment";
 import { renderEjsTemplate } from "@helpers/ejs";
-import { getLanyardData } from "@helpers/lanyard";
+import { getLanyardData, handleReadMe } from "@helpers/lanyard";
 
 const routeDef: RouteDef = {
 	method: "GET",
@@ -14,7 +14,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
 
 	if (!data.success) {
 		return await renderEjsTemplate("error", {
-			message: "User not found or Lanyard data unavailable.",
+			message: data.error.message,
 		});
 	}
 
@@ -29,20 +29,22 @@ async function handler(request: ExtendedRequest): Promise<Response> {
 	}
 
 	const presence: LanyardData = data.data;
+	const readme: string | Promise<string> | null =
+		await handleReadMe(presence);
+
 	const ejsTemplateData: EjsTemplateData = {
 		title: "User Page",
 		username: presence.discord_user.username,
 		status: presence.discord_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,
+		readme,
 	};
 
 	return await renderEjsTemplate("index", ejsTemplateData);
diff --git a/src/routes/index.ts b/src/routes/index.ts
index d9da07b..6603493 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -1,6 +1,6 @@
 import { lanyardConfig } from "@config/environment";
 import { renderEjsTemplate } from "@helpers/ejs";
-import { getLanyardData } from "@helpers/lanyard";
+import { getLanyardData, handleReadMe } from "@helpers/lanyard";
 
 const routeDef: RouteDef = {
 	method: "GET",
@@ -12,8 +12,8 @@ async function handler(): Promise<Response> {
 	const data: LanyardResponse = await getLanyardData();
 
 	if (!data.success) {
-		return Response.json(data.error, {
-			status: 500,
+		return await renderEjsTemplate("error", {
+			message: data.error.message,
 		});
 	}
 
@@ -28,20 +28,22 @@ async function handler(): Promise<Response> {
 	}
 
 	const presence: LanyardData = data.data;
+	const readme: string | Promise<string> | null =
+		await handleReadMe(presence);
+
 	const ejsTemplateData: EjsTemplateData = {
 		title: "User Page",
 		username: presence.discord_user.username,
 		status: presence.discord_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,
+		readme,
 	};
 
 	return await renderEjsTemplate("index", ejsTemplateData);
diff --git a/src/views/index.ejs b/src/views/index.ejs
index e95c0ff..a77b12a 100644
--- a/src/views/index.ejs
+++ b/src/views/index.ejs
@@ -3,6 +3,11 @@
 <head data-user-id="<%= user.id %>" data-instance-uri="<%= instance %>">
 	<meta charset="UTF-8">
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+	<meta property="og:title" content="<%= username %>'s Presence">
+	<meta property="og:image" content="https://cdn.discordapp.com/avatars/<%= user.id %>/<%= user.avatar %>">
+	<meta property="og:description" content="<%= activities[0]?.state || 'Discord Presence' %>">
+
 	<title><%= title %></title>
 
 	<link rel="stylesheet" href="/public/css/index.css">
@@ -99,7 +104,7 @@
 
 						<% if (progress !== null) { %>
 							<div class="progress-bar" data-start="<%= start %>" data-end="<%= end %>">
-								<div class="progress-fill" style="width: <%= progress %>%"></div>
+								<div class="progress-fill" <%= progress !== null ? `style="width: ${progress}%"` : '' %>></div>
 							</div>
 
 							<% if (start && end) { %>
@@ -114,5 +119,11 @@
 			<% }) %>
 		</ul>
 	<% } %>
+	<% if (readme) { %>
+		<h2>Readme</h2>
+		<section class="readme">
+			<div class="markdown-body"><%- readme %></div>
+		</section>
+	<% } %>
 </body>
 </html>