From 06e33083556ae750f25bd07abff0670e0efacda7 Mon Sep 17 00:00:00 2001 From: Rory& Date: Wed, 26 Nov 2025 15:27:19 +0100 Subject: [PATCH] Mention inbox, hopefully --- .idea/data_source_mapping.xml | 6 - .idea/workspace.xml | 95 ++++++++++------ assets/openapi.json | Bin 844877 -> 846010 bytes scripts/openapi.js | 3 +- scripts/util/getRouteDescriptions.js | 3 +- src/api/routes/users/@me/mentions.ts | 161 +++++++++++++++++++++++++++ 6 files changed, 223 insertions(+), 45 deletions(-) delete mode 100644 .idea/data_source_mapping.xml create mode 100644 src/api/routes/users/@me/mentions.ts diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml deleted file mode 100644 index c2a7ad0fd..000000000 --- a/.idea/data_source_mapping.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 11e173ef7..f43b99864 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -43,46 +43,46 @@ + + + + + + + + + @@ -155,6 +165,17 @@ + + + diff --git a/assets/openapi.json b/assets/openapi.json index b62e2b43429f828d13b3295836a1ee59401b8c99..c5e9276405f2cb24065f24f67a7490994394452b 100644 GIT binary patch delta 80 zcmX?m!g$w3F7M2#)7Pc+ywbPk%Q}d>8{Kq7={ev*)cE;%q?o8|~KAFWO f+Yc^apTyK&JDnYfIe?fGh`E56dwcD4o(nzz2S^|Q delta 49 zcmdmW(fI5MF7M2#)7Pc+ywbR>Q&1MH;4j|?PVlE)&-u`Mf&jlX x?.prototype?.OPTS_MARKER == true); if (!opts) return console.error( - `${file} has route without route() description middleware`, + `${bgRedBright("ERROR")} ${file} has route without route() description middleware`, ); console.log(`${method.toUpperCase().padStart("OPTIONS".length)} ${prefix + path}`); diff --git a/src/api/routes/users/@me/mentions.ts b/src/api/routes/users/@me/mentions.ts new file mode 100644 index 000000000..c4491ad08 --- /dev/null +++ b/src/api/routes/users/@me/mentions.ts @@ -0,0 +1,161 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2025 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { route } from "@spacebar/api"; +import { Snowflake, User, Message, Member, Channel, Permissions, timePromise } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { In } from "typeorm"; + +const router: Router = Router({ mergeParams: true }); + +router.get( + "", + route({ + responses: { + 200: { + body: "MessageListResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + // AFAICT this endpoint doesn't list DMs + async (req: Request, res: Response) => { + const limit = req.query.limit && !isNaN(Number(req.query.limit)) ? Number(req.query.limit) : 50; + const everyone = !!req.query.everyone; + const roles = !!req.query.roles; + + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + }); + + const memberships = await Member.find({ + where: { id: req.user_id }, + select: { + guild_id: true, + id: true, + communication_disabled_until: true, + roles: { + // We don't want to include all guild roles, as this could cause a lot more explosive behavior + id: true, + position: true, + permissions: true, + mentionable: true, // cause we can skip querying for unmentionable roles + }, + guild: { + id: true, + owner_id: true, + }, + }, + relations: ["guild", "roles"], + }); + + const channels = await Channel.find({ + where: { + guild_id: In(memberships.map((m) => m.guild_id)), + }, + select: { id: true, permission_overwrites: true }, + }); + + const visibleChannels = channels.filter((c) => { + const member = memberships.find((m) => m.guild_id === c.guild_id)!; + return Permissions.finalPermission({ + user: { id: member.id, roles: member.roles.map((r) => r.id), communication_disabled_until: member.communication_disabled_until, flags: 0 }, + guild: { id: member.guild.id, owner_id: member.guild.owner_id!, roles: member.guild.roles }, + channel: c, + }).has("VIEW_CHANNEL"); + }); + const visibleChannelIds = visibleChannels.map((c) => c.id); + const ownedMentionableRoleIds = memberships.reduce((acc, m) => { + acc.push(...m.roles.filter((r) => r.mentionable).map((r) => r.id)); + return acc; + }, [] as Snowflake[]); + + const [ + { result: userMentions, elapsed: userMentionQueryTime }, + { result: roleMentions, elapsed: roleMentionQueryTime }, + { result: everyoneMentions, elapsed: everyoneMentionQueryTime }, + ] = await Promise.all([ + await timePromise(() => + Message.find({ + where: { + channel_id: In(visibleChannelIds), + mentions: { id: user.id }, + }, + select: { + id: true, + timestamp: true, + }, + order: { + timestamp: "DESC", + }, + take: limit ? Number(limit) : 50, + }), + ), + await timePromise(() => + !roles + ? Promise.resolve([]) + : Message.find({ + where: { + channel_id: In(visibleChannelIds), + mention_roles: { id: In(ownedMentionableRoleIds) }, + }, + select: { + id: true, + timestamp: true, + }, + order: { + timestamp: "DESC", + }, + take: limit ? Number(limit) : 50, + }), + ), + await timePromise(() => + !everyone + ? Promise.resolve([]) + : Message.find({ + where: { + channel_id: In(visibleChannelIds), + mention_everyone: true, + }, + select: { + id: true, + timestamp: true, + }, + order: { + timestamp: "DESC", + }, + take: limit ? Number(limit) : 50, + }), + ), + ]); + + const allMentions = [...userMentions, ...roleMentions, ...everyoneMentions]; + console.log(`[Inbox/mentions] User ${user.id} query results: totalRecs=${allMentions.length} | user=${userMentions.length} (took ${userMentionQueryTime}ms), role=${roleMentions.length} (took ${roleMentionQueryTime}ms), everyone=${everyoneMentions.length} (took ${everyoneMentionQueryTime}ms)`); + + return res.json( + allMentions + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .distinctBy((m) => m.id) + .slice(0, limit), + ); + }, +); + +export default router;