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 @@
- {
- "keyToString": {
- "NIXITCH_NIXPKGS_CONFIG": "/etc/nix/nixpkgs-config.nix",
- "NIXITCH_NIX_CONF_DIR": "",
- "NIXITCH_NIX_OTHER_STORES": "",
- "NIXITCH_NIX_PATH": "/home/Rory/.nix-defexpr/channels:nixpkgs=/nix/store/wb6agba4kfsxpbnb5hzlq58vkjzvbsk6-source",
- "NIXITCH_NIX_PROFILES": "/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/Rory /home/Rory/.local/state/nix/profile /nix/profile /home/Rory/.nix-profile",
- "NIXITCH_NIX_REMOTE": "",
- "NIXITCH_NIX_USER_PROFILE_DIR": "/nix/var/nix/profiles/per-user/Rory",
- "Node.js.Server.ts.executor": "Debug",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.git.unshallow": "true",
- "javascript.nodejs.core.library.configured.version": "24.8.0",
- "javascript.nodejs.core.library.typings.version": "24.7.0",
- "last_opened_file_path": "/home/Rory/git/spacebar/server-master/src/util/migration/postgres",
- "node.js.detected.package.eslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_interpreter_path": "node",
- "nodejs_package_manager_path": "npm",
- "npm.Start API.executor": "Run",
- "npm.Start CDN.executor": "Run",
- "npm.Start Gateway.executor": "Run",
- "npm.build.executor": "Run",
- "npm.start.executor": "Debug",
- "prettierjs.PrettierConfiguration.Package": "/home/Rory/git/spacebar/server-master/node_modules/prettier",
- "settings.editor.selected.configurable": "preferences.pluginManager",
- "ts.external.directory.path": "/home/Rory/git/spacebar/server-master/node_modules/typescript/lib"
+
+}]]>
@@ -111,14 +111,24 @@
+
+
+
+
+
+
+
+
+
@@ -155,6 +165,17 @@
+
+
+
diff --git a/assets/openapi.json b/assets/openapi.json
index b62e2b434..c5e927640 100644
Binary files a/assets/openapi.json and b/assets/openapi.json differ
diff --git a/scripts/openapi.js b/scripts/openapi.js
index 6d4d63667..589614cc2 100644
--- a/scripts/openapi.js
+++ b/scripts/openapi.js
@@ -24,6 +24,7 @@ const {
NO_AUTHORIZATION_ROUTES,
} = require("../dist/api/middlewares/Authentication");
require("../dist/util/util/extensions");
+const { bgRedBright } = require("picocolors");
const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json");
@@ -84,7 +85,7 @@ function combineSchemas(schemas) {
for (const key in definitions) {
if (!schemaRegEx.test(key)) {
- console.error(`Invalid schema name: ${key}, context:`, definitions[key]);
+ console.error(`${bgRedBright("ERROR")} Invalid schema name: ${key}, context:`, definitions[key]);
continue;
}
specification.components = specification.components || {};
diff --git a/scripts/util/getRouteDescriptions.js b/scripts/util/getRouteDescriptions.js
index 54b5e1bc0..54c8fe194 100644
--- a/scripts/util/getRouteDescriptions.js
+++ b/scripts/util/getRouteDescriptions.js
@@ -2,6 +2,7 @@ const express = require("express");
const path = require("path");
const { traverseDirectory } = require("lambert-server");
const RouteUtility = require("../../dist/api/util/handlers/route.js");
+const { bgRedBright } = require("picocolors");
const methods = ["get", "post", "put", "delete", "patch"];
const routes = new Map();
@@ -24,7 +25,7 @@ function proxy(file, method, prefix, path, ...args) {
const opts = args.find((x) => 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;