mirror of
https://github.com/spacebarchat/server.git
synced 2026-03-30 18:15:41 +00:00
Implement widget
This commit is contained in:
152
assets/public/widget.html
Normal file
152
assets/public/widget.html
Normal file
@@ -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>
|
||||
@@ -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"));
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user