Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
8b7bedbf0b | |||
bf66b301ae | |||
7816210a2c | |||
b109f67125 | |||
23f37beef3 |
10 changed files with 229 additions and 35 deletions
|
@ -5,3 +5,6 @@ PORT=8080
|
||||||
# this is only the default value if non is give in /id
|
# this is only the default value if non is give in /id
|
||||||
LANYARD_USER_ID=id-here
|
LANYARD_USER_ID=id-here
|
||||||
LANYARD_INSTANCE=https://lanyard.rest
|
LANYARD_INSTANCE=https://lanyard.rest
|
||||||
|
|
||||||
|
# Required if you want to enable badges
|
||||||
|
BADGE_API_URL=http://localhost:8081
|
||||||
|
|
|
@ -36,6 +36,7 @@ cp .env.example .env
|
||||||
| `PORT` | Port to run the server on (default: `8080`) |
|
| `PORT` | Port to run the server on (default: `8080`) |
|
||||||
| `LANYARD_USER_ID` | Your Discord user ID |
|
| `LANYARD_USER_ID` | Your Discord user ID |
|
||||||
| `LANYARD_INSTANCE` | Lanyard WebSocket endpoint URL |
|
| `LANYARD_INSTANCE` | Lanyard WebSocket endpoint URL |
|
||||||
|
| `BADGE_API_URL` | Uses the [badge api](https://git.creations.works/creations/badgeAPI) only required if you want to use badges
|
||||||
|
|
||||||
#### Optional Lanyard KV Vars (per-user customization)
|
#### Optional Lanyard KV Vars (per-user customization)
|
||||||
|
|
||||||
|
|
|
@ -9,3 +9,5 @@ export const lanyardConfig: LanyardConfig = {
|
||||||
userId: process.env.LANYARD_USER_ID || "",
|
userId: process.env.LANYARD_USER_ID || "",
|
||||||
instance: process.env.LANYARD_INSTANCE || "https://api.lanyard.rest",
|
instance: process.env.LANYARD_INSTANCE || "https://api.lanyard.rest",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const badgeApi: string | null = process.env.BADGE_API_URL || null;
|
||||||
|
|
|
@ -26,7 +26,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-header.hidden {
|
.activity-header.hidden {
|
||||||
|
@ -45,7 +45,10 @@ body {
|
||||||
.avatar-status-wrapper {
|
.avatar-status-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 2rem;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 700px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
|
@ -60,6 +63,28 @@ body {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badges {
|
||||||
|
max-width: 700px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.decoration {
|
.decoration {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -18px;
|
top: -18px;
|
||||||
|
@ -104,11 +129,14 @@ body {
|
||||||
|
|
||||||
.platform-icon.mobile-only {
|
.platform-icon.mobile-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 4px;
|
bottom: 0;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
background-color: var(--background);
|
||||||
|
padding: 0.3rem 0.1rem;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
|
@ -116,6 +144,52 @@ body {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-info-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-inner h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clan-badge {
|
||||||
|
width: 50px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: .4rem 0.5rem;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clan-badge img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clan-badge span {
|
||||||
|
font-size: .9rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -364,6 +438,14 @@ ul {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badges {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-status-wrapper {
|
.avatar-status-wrapper {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -494,14 +576,16 @@ ul {
|
||||||
|
|
||||||
/* readme :p */
|
/* readme :p */
|
||||||
.readme {
|
.readme {
|
||||||
max-width: 700px;
|
max-width: fit-content;
|
||||||
|
min-width: 700px;
|
||||||
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--readme-bg);
|
background: var(--readme-bg);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|
||||||
margin-top: 2rem;
|
margin-top: 1rem;
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -579,7 +663,8 @@ ul {
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.readme {
|
.readme {
|
||||||
width: 100%;
|
max-width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
|
|
@ -84,6 +84,7 @@ setInterval(updateElapsedAndProgress, 1000);
|
||||||
const head = document.querySelector("head");
|
const head = document.querySelector("head");
|
||||||
const userId = head?.dataset.userId;
|
const userId = head?.dataset.userId;
|
||||||
let instanceUri = head?.dataset.instanceUri;
|
let instanceUri = head?.dataset.instanceUri;
|
||||||
|
let badgeURL = head?.dataset.badgeUrl;
|
||||||
|
|
||||||
if (userId && instanceUri) {
|
if (userId && instanceUri) {
|
||||||
if (!instanceUri.startsWith("http")) {
|
if (!instanceUri.startsWith("http")) {
|
||||||
|
@ -276,6 +277,79 @@ function buildActivityHTML(activity) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (badgeURL && badgeURL !== "null" && userId) {
|
||||||
|
if (!badgeURL.startsWith("http")) {
|
||||||
|
badgeURL = `https://${badgeURL}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!badgeURL.endsWith("/")) {
|
||||||
|
badgeURL += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBadges(userId, options = {}) {
|
||||||
|
const {
|
||||||
|
services = [],
|
||||||
|
seperated = false,
|
||||||
|
cache = true,
|
||||||
|
targetId = "badges",
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (services.length) params.set("services", services.join(","));
|
||||||
|
if (seperated) params.set("seperated", "true");
|
||||||
|
if (!cache) params.set("cache", "false");
|
||||||
|
|
||||||
|
const url = `${badgeURL}${userId}?${params.toString()}`;
|
||||||
|
const target = document.getElementById(targetId);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
target.classList.add("hidden");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok || !json.badges) {
|
||||||
|
target.textContent = "Failed to load badges.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const badges = Array.isArray(json.badges)
|
||||||
|
? json.badges
|
||||||
|
: Object.values(json.badges).flat();
|
||||||
|
|
||||||
|
if (badges.length === 0) {
|
||||||
|
target.innerHTML = "";
|
||||||
|
target.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.innerHTML = "";
|
||||||
|
for (const badge of badges) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = badge.badge;
|
||||||
|
img.alt = badge.tooltip;
|
||||||
|
img.title = badge.tooltip;
|
||||||
|
img.className = "badge";
|
||||||
|
target.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
target.classList.remove("hidden");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
target.innerHTML = "";
|
||||||
|
target.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBadges(userId, {
|
||||||
|
services: [],
|
||||||
|
seperated: false,
|
||||||
|
cache: true,
|
||||||
|
targetId: "badges",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updatePresence(data) {
|
function updatePresence(data) {
|
||||||
const avatarWrapper = document.querySelector(".avatar-wrapper");
|
const avatarWrapper = document.querySelector(".avatar-wrapper");
|
||||||
const statusIndicator = avatarWrapper?.querySelector(".status-indicator");
|
const statusIndicator = avatarWrapper?.querySelector(".status-indicator");
|
||||||
|
|
|
@ -49,11 +49,12 @@ export async function getLanyardData(id?: string): Promise<LanyardResponse> {
|
||||||
|
|
||||||
export async function handleReadMe(data: LanyardData): Promise<string | null> {
|
export async function handleReadMe(data: LanyardData): Promise<string | null> {
|
||||||
const userReadMe: string | null = data.kv?.readme;
|
const userReadMe: string | null = data.kv?.readme;
|
||||||
|
const validExtension = /\.(md|markdown|txt|html?)$/i;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!userReadMe ||
|
!userReadMe ||
|
||||||
!userReadMe.toLowerCase().endsWith("readme.md") ||
|
!userReadMe.startsWith("http") ||
|
||||||
!userReadMe.startsWith("http")
|
!validExtension.test(userReadMe)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -65,15 +66,7 @@ export async function handleReadMe(data: LanyardData): Promise<string | null> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const contentType: string = res.headers.get("content-type") || "";
|
if (!res.ok) return null;
|
||||||
if (
|
|
||||||
!res.ok ||
|
|
||||||
!(
|
|
||||||
contentType.includes("text/markdown") ||
|
|
||||||
contentType.includes("text/plain")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (res.headers.has("content-length")) {
|
if (res.headers.has("content-length")) {
|
||||||
const size: number = Number.parseInt(
|
const size: number = Number.parseInt(
|
||||||
|
@ -86,9 +79,17 @@ export async function handleReadMe(data: LanyardData): Promise<string | null> {
|
||||||
const text: string = await res.text();
|
const text: string = await res.text();
|
||||||
if (!text || text.length < 10) return null;
|
if (!text || text.length < 10) return null;
|
||||||
|
|
||||||
const html: string | null = await marked.parse(text);
|
let html: string;
|
||||||
const safe: string | null = DOMPurify.sanitize(html);
|
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;
|
return safe;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { lanyardConfig } from "@config/environment";
|
import { getImageColors } from "@/helpers/colors";
|
||||||
|
import { badgeApi, lanyardConfig } from "@config/environment";
|
||||||
import { renderEjsTemplate } from "@helpers/ejs";
|
import { renderEjsTemplate } from "@helpers/ejs";
|
||||||
import { getLanyardData, handleReadMe } from "@helpers/lanyard";
|
import { getLanyardData, handleReadMe } from "@helpers/lanyard";
|
||||||
|
|
||||||
|
@ -38,6 +39,14 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
status = presence.discord_status;
|
status = presence.discord_status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let colors: ImageColorResult | null = null;
|
||||||
|
if (presence.kv.colors === "true") {
|
||||||
|
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/${presence.discord_user.discriminator || 1 % 5}`;
|
||||||
|
colors = await getImageColors(avatar, true);
|
||||||
|
}
|
||||||
|
|
||||||
const ejsTemplateData: EjsTemplateData = {
|
const ejsTemplateData: EjsTemplateData = {
|
||||||
title: presence.discord_user.global_name || presence.discord_user.username,
|
title: presence.discord_user.global_name || presence.discord_user.username,
|
||||||
username:
|
username:
|
||||||
|
@ -52,8 +61,10 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
},
|
},
|
||||||
instance,
|
instance,
|
||||||
readme,
|
readme,
|
||||||
allowSnow: presence.kv.snow || false,
|
allowSnow: presence.kv.snow === "true",
|
||||||
allowRain: presence.kv.rain || false,
|
allowRain: presence.kv.rain === "true",
|
||||||
|
colors: colors?.colors ?? {},
|
||||||
|
badgeApi: badgeApi,
|
||||||
};
|
};
|
||||||
|
|
||||||
return await renderEjsTemplate("index", ejsTemplateData);
|
return await renderEjsTemplate("index", ejsTemplateData);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getImageColors } from "@/helpers/colors";
|
import { getImageColors } from "@/helpers/colors";
|
||||||
import { 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";
|
import { getLanyardData, handleReadMe } from "@helpers/lanyard";
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ async function handler(): Promise<Response> {
|
||||||
allowSnow: presence.kv.snow === "true",
|
allowSnow: presence.kv.snow === "true",
|
||||||
allowRain: presence.kv.rain === "true",
|
allowRain: presence.kv.rain === "true",
|
||||||
colors: colors?.colors ?? {},
|
colors: colors?.colors ?? {},
|
||||||
|
badgeApi: badgeApi,
|
||||||
};
|
};
|
||||||
|
|
||||||
return await renderEjsTemplate("index", ejsTemplateData);
|
return await renderEjsTemplate("index", ejsTemplateData);
|
||||||
|
|
|
@ -225,15 +225,15 @@ class ServerHandler {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers: Headers = response.headers;
|
const headers = request.headers;
|
||||||
let ip: string | null = server.requestIP(request)?.address || null;
|
let ip = server.requestIP(request)?.address;
|
||||||
|
|
||||||
if (!ip) {
|
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
|
||||||
ip =
|
ip =
|
||||||
headers.get("CF-Connecting-IP") ||
|
headers.get("CF-Connecting-IP")?.trim() ||
|
||||||
headers.get("X-Real-IP") ||
|
headers.get("X-Real-IP")?.trim() ||
|
||||||
headers.get("X-Forwarded-For") ||
|
headers.get("X-Forwarded-For")?.split(",")[0].trim() ||
|
||||||
null;
|
"unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.custom(
|
logger.custom(
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head data-user-id="<%= user.id %>" data-instance-uri="<%= instance %>">
|
<head data-user-id="<%= user.id %>" data-instance-uri="<%= instance %>" data-badge-url="<%= badgeApi %>">
|
||||||
<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">
|
||||||
|
|
||||||
<meta property="og:title" content="<%= username %>'s Presence">
|
<%
|
||||||
|
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 property="og:image" content="https://cdn.discordapp.com/avatars/<%= user.id %>/<%= user.avatar %>">
|
||||||
<meta property="og:description" content="<%= activities[0]?.state || 'Discord Presence' %>">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
|
||||||
<title><%= title %></title>
|
<title><%= title %></title>
|
||||||
|
|
||||||
|
@ -24,7 +29,7 @@
|
||||||
<meta name="color-scheme" content="dark">
|
<meta name="color-scheme" content="dark">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<%- include('partial/style.ejs') %>
|
<%- include("partial/style.ejs") %>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="user-card">
|
<div class="user-card">
|
||||||
|
@ -43,7 +48,15 @@
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<h1><%= username %></h1>
|
<div class="user-info-inner">
|
||||||
|
<h1><%= 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>
|
||||||
<% if (activities.length && activities[0].type === 4) {
|
<% if (activities.length && activities[0].type === 4) {
|
||||||
const emoji = activities[0].emoji;
|
const emoji = activities[0].emoji;
|
||||||
const isCustom = emoji?.id;
|
const isCustom = emoji?.id;
|
||||||
|
@ -66,6 +79,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% if(badgeApi) { %>
|
||||||
|
<div id="badges" class="badges"></div>
|
||||||
|
<% } %>
|
||||||
<%
|
<%
|
||||||
let filtered = activities
|
let filtered = activities
|
||||||
.filter(a => a.type !== 4)
|
.filter(a => a.type !== 4)
|
||||||
|
|
Loading…
Add table
Reference in a new issue