mirror of
https://github.com/spacebarchat/server.git
synced 2026-04-04 10:55:44 +00:00
Merge branch 'spacebarchat:master' into removeStupidPolies
This commit is contained in:
6
.idea/data_source_mapping.xml
generated
6
.idea/data_source_mapping.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/61e66448-285a-4205-a1d7-c2704d5afb2c/console.sql" value="61e66448-285a-4205-a1d7-c2704d5afb2c" />
|
||||
</component>
|
||||
</project>
|
||||
95
.idea/workspace.xml
generated
95
.idea/workspace.xml
generated
@@ -43,46 +43,46 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"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"
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"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": "settings.javascript.linters.tslint",
|
||||
"ts.external.directory.path": "/home/Rory/git/spacebar/server-master/node_modules/typescript/lib"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
],
|
||||
"GitStage.ChangesTree.GroupingKeys": [
|
||||
"directory",
|
||||
"module",
|
||||
"repository"
|
||||
"GitStage.ChangesTree.GroupingKeys": [
|
||||
"directory",
|
||||
"module",
|
||||
"repository"
|
||||
]
|
||||
}
|
||||
}</component>
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/src/util/migration/postgres" />
|
||||
@@ -111,14 +111,24 @@
|
||||
<value>
|
||||
<map>
|
||||
<entry key="Start API" value="STOPPED" />
|
||||
<entry key="Start CDN" value="STOPPED" />
|
||||
<entry key="Start Gateway" value="STOPPED" />
|
||||
<entry key="build" value="STOPPED" />
|
||||
<entry key="Start bundle" value="STOPPED" />
|
||||
</map>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="RunManager" selected="Compound.Start separated">
|
||||
<list>
|
||||
<item itemvalue="Compound.Start separated" />
|
||||
<item itemvalue="npm.Start API" />
|
||||
<item itemvalue="npm.Start CDN" />
|
||||
<item itemvalue="npm.Start Gateway" />
|
||||
<item itemvalue="npm.Start bundle" />
|
||||
</list>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
@@ -155,6 +165,17 @@
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
<breakpoints>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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 || {};
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
206
src/api/routes/users/@me/mentions.ts
Normal file
206
src/api/routes/users/@me/mentions.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { route } from "@spacebar/api";
|
||||
import { Snowflake, User, Message, Member, Channel, Permissions, timePromise, NewUrlUserSignatureData, Stopwatch, Attachment } 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 before = req.query.before && BigInt(req.query.before as string);
|
||||
|
||||
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, guild_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.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,
|
||||
}),
|
||||
),
|
||||
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,
|
||||
}),
|
||||
),
|
||||
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,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
const allMentions = [...userMentions, ...roleMentions, ...everyoneMentions];
|
||||
console.log(
|
||||
`[Inbox/mentions] User ${user.id} query results: totalRecs=${allMentions.length} | user=${userMentions.length} (took ${userMentionQueryTime.totalMilliseconds}ms), role=${roleMentions.length} (took ${roleMentionQueryTime.totalMilliseconds}ms), everyone=${everyoneMentions.length} (took ${everyoneMentionQueryTime.totalMilliseconds}ms)`,
|
||||
);
|
||||
const messageIdsToReturn = allMentions
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
||||
.distinctBy((m) => m.id)
|
||||
.slice(0, limit);
|
||||
|
||||
const sw = Stopwatch.startNew();
|
||||
const finalMessages = (
|
||||
await Message.find({
|
||||
where: { id: In(messageIdsToReturn.map((m) => m.id)) },
|
||||
order: { timestamp: "DESC" },
|
||||
relations: [
|
||||
"author",
|
||||
"webhook",
|
||||
"application",
|
||||
"mentions",
|
||||
"mention_roles",
|
||||
"mention_channels",
|
||||
"sticker_items",
|
||||
"attachments",
|
||||
"referenced_message",
|
||||
"referenced_message.author",
|
||||
"referenced_message.webhook",
|
||||
"referenced_message.application",
|
||||
"referenced_message.mentions",
|
||||
"referenced_message.mention_roles",
|
||||
"referenced_message.mention_channels",
|
||||
"referenced_message.sticker_items",
|
||||
"referenced_message.attachments",
|
||||
],
|
||||
})
|
||||
).map((m) => {
|
||||
return {
|
||||
...m.toJSON(),
|
||||
attachments: m.attachments?.map((attachment: Attachment) =>
|
||||
Attachment.prototype.signUrls.call(
|
||||
attachment,
|
||||
new NewUrlUserSignatureData({
|
||||
ip: req.ip,
|
||||
userAgent: req.headers["user-agent"] as string,
|
||||
}),
|
||||
),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`[Inbox/mentions] User ${user.id} fetched full message data for ${finalMessages.length} messages in ${sw.elapsed().totalMilliseconds}ms`);
|
||||
|
||||
return res.json(finalMessages);
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -64,7 +64,12 @@ export type PublicMemberKeys =
|
||||
| "deaf"
|
||||
| "mute"
|
||||
| "premium_since"
|
||||
| "avatar";
|
||||
| "avatar"
|
||||
| "banner"
|
||||
| "bio"
|
||||
| "theme_colors"
|
||||
| "pronouns"
|
||||
| "communication_disabled_until";
|
||||
|
||||
export const PublicMemberProjection: PublicMemberKeys[] = [
|
||||
"id",
|
||||
@@ -77,6 +82,11 @@ export const PublicMemberProjection: PublicMemberKeys[] = [
|
||||
"mute",
|
||||
"premium_since",
|
||||
"avatar",
|
||||
"banner",
|
||||
"bio",
|
||||
"theme_colors",
|
||||
"pronouns",
|
||||
"communication_disabled_until",
|
||||
];
|
||||
|
||||
export type PublicMember = Omit<Pick<Member, PublicMemberKeys>, "roles"> & {
|
||||
|
||||
@@ -44,6 +44,12 @@ export const MemberPrivateProjection: (keyof Member)[] = [
|
||||
"roles",
|
||||
"settings",
|
||||
"user",
|
||||
"avatar",
|
||||
"banner",
|
||||
"bio",
|
||||
"theme_colors",
|
||||
"pronouns",
|
||||
"communication_disabled_until",
|
||||
];
|
||||
|
||||
@Entity({
|
||||
|
||||
@@ -147,8 +147,31 @@ export async function generateToken(id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
let lastFsCheck: number;
|
||||
let cachedKeypair: {
|
||||
privateKey: crypto.KeyObject;
|
||||
publicKey: crypto.KeyObject;
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
// Get ECDSA keypair from file or generate it
|
||||
export async function loadOrGenerateKeypair() {
|
||||
if (cachedKeypair) {
|
||||
// check for file deletion every minute
|
||||
if (Date.now() - lastFsCheck > 60000) {
|
||||
if (!existsSync("jwt.key") || !existsSync("jwt.key.pub")) {
|
||||
console.log("[JWT] Keypair files disappeared... Saving them again.");
|
||||
await Promise.all([
|
||||
fs.writeFile("jwt.key", cachedKeypair.privateKey.export({ format: "pem", type: "sec1" })),
|
||||
fs.writeFile("jwt.key.pub", cachedKeypair.publicKey.export({ format: "pem", type: "spki" })),
|
||||
]);
|
||||
}
|
||||
lastFsCheck = Date.now();
|
||||
}
|
||||
|
||||
return cachedKeypair;
|
||||
}
|
||||
|
||||
let privateKey: crypto.KeyObject;
|
||||
let publicKey: crypto.KeyObject;
|
||||
|
||||
@@ -185,5 +208,6 @@ export async function loadOrGenerateKeypair() {
|
||||
.update(publicKey.export({ format: "pem", type: "spki" }))
|
||||
.digest("hex");
|
||||
|
||||
return { privateKey, publicKey, fingerprint };
|
||||
lastFsCheck = Date.now();
|
||||
return cachedKeypair = { privateKey, publicKey, fingerprint };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user