summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-12-19 23:14:21 +0100
committerRory& <root@rory.gay>2025-12-19 23:14:21 +0100
commitffbe8b5987ccd88f6c2d04beab2944959490e15d (patch)
treec067e43817a1b37fa573a7671c5f6d67e928e2cb
parentDeprecation: Use object-based relations for typeorm (diff)
downloadserver-ts-ffbe8b5987ccd88f6c2d04beab2944959490e15d.tar.xz
Implement widget
-rw-r--r--assets/public/widget.html152
-rw-r--r--src/api/Server.ts2
-rw-r--r--src/api/routes/guilds/#guild_id/widget.json.ts18
3 files changed, 167 insertions, 5 deletions
diff --git a/assets/public/widget.html b/assets/public/widget.html
new file mode 100644

index 00000000..a03f4b36 --- /dev/null +++ b/assets/public/widget.html
@@ -0,0 +1,152 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Spacebar Widget</title> + + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet"> + + <style> + body { + font-family: 'Montserrat', sans-serif; + background-color: rgb(10, 10, 10); + color: white; + font-size: 1.1rem; + height: 100vh; + } + + * { + padding: 0; + margin: 0; + } + + p { + margin-top: 10px; + } + + .container { + display: flex; + flex-direction: column; + height: 100%; + } + + #wordmark { + width: min(250px, 50%); + } + + .header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px; + border-bottom: 1px solid #222; + } + + .content { + padding: 8px; + } + + #online-count { + font-size: 1.2rem; + font-weight: 999; + } + + a, a:visited { + color: #0185ff; + } + </style> + + <script> + // id param in query + const guildId = new URLSearchParams(window.location.search).get("id"); + + document.addEventListener("DOMContentLoaded", function() { + const vcList = document.getElementById("vc-list"); + const memberList = document.getElementById("member-list"); + const onlineCount = document.getElementById("online-count"); + + function fetchData() { + fetch(`/api/guilds/${guildId}/widget.json`) + .then(response => response.json()) + .then(data => { + // Update online count + onlineCount.textContent = data.presence_count; + + // Update voice channels + vcList.innerHTML = ""; + data.channels.forEach(channel => { + const channelDiv = document.createElement("div"); + const channelName = document.createElement("h3"); + channelName.textContent = "🔊 " + channel.name; + channelDiv.appendChild(channelName); + + const userList = document.createElement("ul"); + channel.users?.forEach(user => { + const userItem = document.createElement("li"); + userItem.textContent = user.username; + userList.appendChild(userItem); + }); + channelDiv.appendChild(userList); + vcList.appendChild(channelDiv); + }); + + // Update online members + memberList.innerHTML = ""; + data.members.forEach(member => { + const memberItem = document.createElement("div"); + const img = document.createElement("img"); + img.src = member.avatar_url; + img.alt = member.username; + img.width = 32; + img.height = 32; + img.style.borderRadius = "50%"; + img.style.marginRight = "8px"; + memberItem.appendChild(img); + const nameSpan = document.createElement("span"); + nameSpan.textContent = member.username; + memberItem.appendChild(nameSpan); + memberItem.style.display = "flex"; + memberItem.style.alignItems = "center"; + memberItem.style.marginBottom = "6px"; + memberList.appendChild(memberItem); + }); + }) + .catch(error => { + console.error("Error fetching widget data:", error); + }); + } + + // Initial fetch + fetchData(); + + // Refresh data every 30 seconds + setInterval(fetchData, 30000); + }); + </script> +</head> + +<body> +<div class="container"> + <div class="header"> + <img alt="Spacebar Logo" + id="wordmark" + src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg" /> + <span> + <b id="online-count">Unknown</b> + users online + </span> + </div> + <div class="content"> + <div id="vc-list"></div> + <p>Online users</p> + <div id="member-list"></div> + </div> +</div> +</body> + +</html> \ No newline at end of file diff --git a/src/api/Server.ts b/src/api/Server.ts
index f81898e8..9cc1820f 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts
@@ -115,8 +115,8 @@ export class SpacebarServer extends Server { app.use("/imageproxy/:hash/:size/:url", ImageProxy); app.get("/", (req, res) => res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "index.html"))); - app.get("/verify-email", (req, res) => res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "verify.html"))); + app.get("/widget", (req, res) => res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "widget.html"))); app.get("/_spacebar/api/schemas.json", (req, res) => { res.sendFile(path.join(ASSETS_FOLDER, "schemas.json")); diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts
index 8f41417a..dea8f29f 100644 --- a/src/api/routes/guilds/#guild_id/widget.json.ts +++ b/src/api/routes/guilds/#guild_id/widget.json.ts
@@ -17,7 +17,7 @@ */ import { randomString, route } from "@spacebar/api"; -import { Channel, DiscordApiErrors, Guild, Invite, Member, Permissions } from "@spacebar/util"; +import { Channel, Config, DiscordApiErrors, Guild, Invite, Member, Permissions } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router: Router = Router({ mergeParams: true }); @@ -99,7 +99,17 @@ router.get( // Fetch members // TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file) - const members = await Member.find({ where: { guild_id: guild_id } }); + const members = await Member.find({ where: { guild_id: guild_id }, relations: { user: { sessions: true } } }); + const memberData = members.map((x) => { + return { + id: x.id, + username: x.user.username, + discriminator: x.user.discriminator, + avatar: x.user.avatar, + status: "online", // TODO + avatar_url: x.user.avatar ? `${Config.get().cdn.endpointPublic}/avatars/${x.id}/${x.user.avatar}.png` : undefined, + }; + }); // Construct object to respond with const data = { @@ -107,8 +117,8 @@ router.get( name: guild.name, instant_invite: invite?.code, channels: channels, - members: members, - presence_count: guild.presence_count, + members: memberData, + presence_count: guild.presence_count || members.filter((m) => m.user.sessions.filter((s) => (s.last_seen?.getTime() ?? 0) > Date.now() - 1000 * 60)).length, }; res.set("Cache-Control", "public, max-age=300");