From c31ae1528e34d05d11aa24287d71232595808120 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 21 Dec 2025 03:57:06 +0100 Subject: [PATCH] Shields --- package-lock.json | Bin 437924 -> 440980 bytes package.json | 1 + src/api/middlewares/Authentication.ts | 1 + src/api/routes/guilds/#guild_id/shield.svg.ts | 100 ++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 src/api/routes/guilds/#guild_id/shield.svg.ts diff --git a/package-lock.json b/package-lock.json index 2c0a698630b75651589551aa8063f887db9c0ff7..41c03b0eb1ccc848fb92f31d74d5505db082b1ec 100644 GIT binary patch delta 1399 zcmZ{k+i%-c9LJU8BrAx^x~^j#VzdnciIUj4IB|DDI2SvP9lLezd6;G=cH<_o6FZKb zAfX{7_FOu4v==la-VoyQ5~N8;+aG|2v=@YswkKXdbPv!cE=fgEH*F6;>74U>IN#6j z`#tBgpU+(T5)d28J0ss`Q`6X4-B^8D>&sn$(yk2%E}OtyZEfy25As1K!%KX=p*V2wqF{ z$&8xd*sZiMXplxKz5C$Bv))t+3&J0wz6H4atS>NkI5gNV`A+X*h;N}`SyPCPVa`y7 ziaD6FJp9m4E*!{QJ-58t(d7R$!?$<5EIjoEGXK&1>SXUrkbPlJGDks6kxcmMqJIJI zt$B~bXWsV`ugIEy*lM(9)JOO5>vw&jk}hY$6;~)0%~FL?Ewvt#vUJVH(}K;nWtTSE z+vRvY(stDx8?_};CdND)?%HEP(Rv1#t#31WF_tt5BAM-tm0=@Jgx%c-=-Fd4>Tn2y zD}Q+c6Z|{CMi!sw8{GNc^Xm0Kk!hwr0R;Y3cc=QF+JiUmcta&>94n&X8&9mEWYaQ|n>i}l z-l|4arFM|HQ;$q;X2_**C>nw*`ye*CvIlOx{`@2U!H0Li3cMKy=iYdNw=>g{s!=W{0=hbxdb2h z{3mBoJTVhy>)PN*5`Tlp5$y}v;M{`k4~0_*|A5A*+k)3Y0PZ~ydW zRdkYyfF{vlU5qwWmJ5#*bm$WKY(0kATjSnHRmwC@TbfPlW+z`!A_n2ONtsLaob{CD zme-uDJ+5_9T16kpCPQK!P2akB@gP(1?h@jioPPltj#)iU+0V%qVVAxu9pQ{Bk->29P#;M;jdZd1b{u2V%PSEjO3 xH=. +*/ + +import { route } from "@spacebar/api"; +import { DiscordApiErrors, Guild, Member } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { makeBadge } from "badge-maker"; +import path from "path"; +import fs from "fs"; + +const router: Router = Router({ mergeParams: true }); + +// Undocumented API notes: +// An invite is created for the widget_channel_id on request (only if an existing one created by the widget doesn't already exist) +// This invite created doesn't include an inviter object like user created ones and has a default expiry of 24 hours +// Missing user object information is intentional (https://github.com/discord/discord-api-docs/issues/1287) +// channels returns voice channel objects where @everyone has the CONNECT permission +// members (max 100 returned) is a sample of all members, and bots par invisible status, there exists some alphabetical distribution pattern between the members returned + +// https://discord.com/developers/docs/resources/guild#get-guild-widget +const expiryTime = 1000 * 60 * 5; // 5 minutes +const jsonDataCache = new Map; expiry: Date }>(); + +const assetsPath = path.join(__dirname, "..", "..", "..", "..", "..", "assets"); +const whiteLogo = "data:image/png;base64," + Buffer.from(fs.readFileSync(path.join(assetsPath, "icon_white.png"))).toString("base64"); +const blueLogo = "data:image/png;base64," + Buffer.from(fs.readFileSync(path.join(assetsPath, "icon.png"))).toString("base64"); + +router.get( + "/", + route({ + responses: { + 200: {}, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + + let cacheEntry = jsonDataCache.get(guild_id); + if (!cacheEntry || cacheEntry.expiry.getTime() < Date.now()) { + // Create new cache entry + const dataPromise = getWidgetJsonData(guild_id); + cacheEntry = { + data: dataPromise, + expiry: new Date(Date.now() + expiryTime), + }; + console.log("[Shield] Caching shield data for guild", guild_id); + jsonDataCache.set(guild_id, cacheEntry); + } + + const cacheRemainingSeconds = Math.floor((cacheEntry.expiry.getTime() - Date.now()) / 1000); + res.set("Cache-Control", `public, max-age=${cacheRemainingSeconds}, s-maxage=${cacheRemainingSeconds}, immutable`); + res.set("Content-Type", "image/svg+xml;charset=utf-8"); + return res.status(200).send(await cacheEntry.data); + }, +); + +async function getWidgetJsonData(guild_id: string, useWhiteLogo: boolean = true) { + const guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: { + channel_ordering: true, + widget_channel_id: true, + widget_enabled: true, + presence_count: true, + name: true, + }, + }); + if (!guild.widget_enabled) throw DiscordApiErrors.EMBED_DISABLED; + + const members = await Member.find({ where: { guild_id: guild_id }, relations: { user: { sessions: true } } }); + const minLastSeen = Date.now() - 1000 * 60 * 5; + const onlineMembers = members.filter((m) => m.user.sessions.filter((s) => (s.last_seen?.getTime() ?? 0) > minLastSeen).length > 0); + + return makeBadge({ + label: "Spacebar", + message: `${onlineMembers.length} online`, + color: "#0185ff", + logoBase64: useWhiteLogo ? whiteLogo : blueLogo, + }); +} + +export default router;