diff --git a/assets/openapi.json b/assets/openapi.json
index 09506ade9..efd3a4cdc 100644
Binary files a/assets/openapi.json and b/assets/openapi.json differ
diff --git a/assets/schemas.json b/assets/schemas.json
index d3b240410..a7d759fab 100644
Binary files a/assets/schemas.json and b/assets/schemas.json differ
diff --git a/src/api/routes/channels/preload-messages.ts b/src/api/routes/channels/preload-messages.ts
index f50129fad..09743cc73 100644
--- a/src/api/routes/channels/preload-messages.ts
+++ b/src/api/routes/channels/preload-messages.ts
@@ -37,6 +37,7 @@ router.post(
}),
async (req: Request, res: Response) => {
const body = req.body as PreloadMessagesRequestSchema;
+ body.channels ??= body.channel_ids ?? [];
if (body.channels.length > Config.get().limits.message.maxPreloadCount)
return res.status(400).send({
code: 400,
diff --git a/src/api/routes/reporting/index.ts b/src/api/routes/reporting/index.ts
new file mode 100644
index 000000000..f061cdfab
--- /dev/null
+++ b/src/api/routes/reporting/index.ts
@@ -0,0 +1,202 @@
+/*
+ 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 { Request, Response, Router } from "express";
+import { ReportMenuType, ReportMenuTypeNames } from "../../../schemas/api/reports/ReportMenu";
+import path from "path";
+import { HTTPError } from "lambert-server";
+import { CreateReportSchema } from "../../../schemas/api/reports/CreateReport";
+import { FieldErrors } from "@spacebar/util";
+import fs from "fs";
+
+const router = Router({ mergeParams: true });
+
+console.log("[Server] Registering reporting menu routes...");
+router.get(
+ "/",
+ route({
+ description: "[EXT] Get available reporting menu types.",
+ responses: {
+ 200: {
+ body: "Array",
+ },
+ },
+ }),
+ (req: Request, res: Response) => {
+ res.json(Object.values(ReportMenuTypeNames));
+ },
+);
+
+for (const type of Object.values(ReportMenuTypeNames)) {
+ router.get(
+ `/menu/${type}`,
+ route({
+ description: `Get reporting menu options for ${type} reports.`,
+ query: {
+ variant: { type: "string", required: false, description: "Version variant of the menu to retrieve (max 256 characters, default active)" },
+ },
+ responses: {
+ 200: {
+ body: "ReportingMenuResponse",
+ },
+ 204: {},
+ },
+ }),
+ (req: Request, res: Response) => {
+ // TODO: implement
+ // res.send([] as ReportingMenuResponseSchema);
+ res.sendFile(path.join(__dirname, "..", "..", "..", "..", "assets", "temp_report_menu_responses", `${type}.json`));
+ },
+ );
+ console.log(`[Server] Route /reporting/menu/${type} registered (reports).`);
+ router.post(
+ `/${type}`,
+ route({
+ description: `Get reporting menu options for ${type} reports.`,
+ requestBody: "CreateReportSchema",
+ responses: {
+ 200: {
+ body: "ReportingMenuResponse",
+ },
+ 204: {},
+ },
+ }),
+ (req: Request, res: Response) => {
+ // TODO: implement
+ const body = req.body as CreateReportSchema;
+ if (body.name !== type)
+ throw FieldErrors({
+ name: {
+ message: `Expected report type ${type} but got ${body.name}`,
+ code: "INVALID_REPORT_TYPE",
+ },
+ });
+
+ const menuPath = path.join(__dirname, "..", "..", "..", "..", "assets", "temp_report_menu_responses", `${type}.json`);
+ const menuData = JSON.parse(fs.readFileSync(menuPath, "utf-8"));
+ if (body.version !== menuData.version) {
+ throw FieldErrors({
+ version: {
+ message: `Expected report menu version ${menuData.version} but got ${body.version}`,
+ code: "INVALID_REPORT_MENU_VERSION",
+ },
+ });
+ }
+
+ if (body.variant !== menuData.variant) {
+ throw FieldErrors({
+ variant: {
+ message: `Expected report menu variant ${menuData.variant} but got ${body.variant}`,
+ code: "INVALID_REPORT_MENU_VARIANT",
+ },
+ });
+ }
+
+ if (body.breadcrumbs != menuData.breadcrumbs) {
+ throw FieldErrors({
+ breadcrumbs: {
+ message: `Invalid report menu breadcrumbs.`,
+ code: "INVALID_REPORT_MENU_BREADCRUMBS",
+ },
+ });
+ }
+
+ const validateBreadcrumbs = (currentNode: unknown, breadcrumbs: number[]): boolean => {
+ // navigate via node.children ([name, id][]) according to breadcrumbs
+ let node: { children: [string, number][] } = currentNode as { children: [string, number][] };
+ for (const crumb of breadcrumbs) {
+ if (!node || !node.children || !Array.isArray(node.children)) return false;
+ const nextNode = node.children.find((child: [string, number]) => child[1] === crumb);
+ if (!nextNode) return false;
+ // load next node
+ const nextNodeData = menuData.nodes[crumb];
+ if (!nextNodeData) return false;
+ node = nextNodeData;
+ }
+ return true;
+ };
+
+ if (!validateBreadcrumbs(menuData.nodes[menuData.root_node_id], body.breadcrumbs))
+ throw FieldErrors({
+ breadcrumbs: {
+ message: `Invalid report menu breadcrumbs path.`,
+ code: "INVALID_REPORT_MENU_BREADCRUMBS_PATH",
+ },
+ });
+
+ const requireFields = (obj: CreateReportSchema, fields: string[]) => {
+ const missingFields: string[] = [];
+ for (const field of fields) if (!(field in obj)) missingFields.push(field);
+
+ if (missingFields.length > 0)
+ throw FieldErrors(
+ Object.fromEntries(
+ missingFields.map((f) => [
+ f,
+ {
+ message: `Missing required field ${f}.`,
+ code: "MISSING_FIELD",
+ },
+ ]),
+ ),
+ );
+ };
+
+ const t = Number(Object.entries(ReportMenuTypeNames).find((x) => x[1] === type)?.[0]) as ReportMenuType;
+ // TODO: did i miss anything?
+ switch (t) {
+ case ReportMenuType.GUILD:
+ case ReportMenuType.GUILD_DISCOVERY:
+ requireFields(body, ["guild_id"]);
+ break;
+ case ReportMenuType.GUILD_DIRECTORY_ENTRY:
+ requireFields(body, ["guild_id", "channel_id"]);
+ break;
+ case ReportMenuType.GUILD_SCHEDULED_EVENT:
+ requireFields(body, ["guild_id", "scheduled_event_id"]);
+ break;
+ case ReportMenuType.MESSAGE:
+ requireFields(body, ["channel_id", "message_id"]);
+ // NOTE: is body.guild_id set if the channel is in a guild? is body.user_id ever set????
+ break;
+ case ReportMenuType.STAGE_CHANNEL:
+ requireFields(body, ["channel_id", "guild_id", "stage_instance_id"]);
+ break;
+ case ReportMenuType.FIRST_DM:
+ requireFields(body, ["user_id", "channel_id"]);
+ break;
+ case ReportMenuType.USER:
+ requireFields(body, ["reported_user_id"]);
+ break;
+ case ReportMenuType.APPLICATION:
+ requireFields(body, ["application_id"]);
+ break;
+ case ReportMenuType.WIDGET:
+ requireFields(body, ["user_id", "widget_id"]);
+ break;
+ default:
+ throw new HTTPError("Unknown report menu type", 400);
+ }
+
+ throw new HTTPError("Validation success - implementation TODO", 418);
+ },
+ );
+ console.log(`[Server] Route /reporting/${type} registered (reports).`);
+}
+export default router;
diff --git a/src/api/routes/reporting/menu.ts b/src/api/routes/reporting/menu.ts
deleted file mode 100644
index 3cff08468..000000000
--- a/src/api/routes/reporting/menu.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- 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 { Request, Response, Router } from "express";
-import { ReportMenuTypeNames } from "../../../schemas/api/reports/ReportMenu";
-import path from "path";
-
-const router = Router({ mergeParams: true });
-
-console.log("[Server] Registering reporting menu routes...");
-router.get(
- "/",
- route({
- description: "[EXT] Get available reporting menu types.",
- responses: {
- 200: {
- body: "Array",
- },
- },
- }),
- (req: Request, res: Response) => {
- res.json(Object.values(ReportMenuTypeNames));
- },
-);
-
-for (const type of Object.values(ReportMenuTypeNames)) {
- console.log(`[Server] Route /reporting/menu/${type} registered (reports).`);
- router.get(
- `/${type}`,
- route({
- description: `Get reporting menu options for ${type} reports.`,
- query: {
- variant: { type: "string", required: false, description: "Version variant of the menu to retrieve (max 256 characters, default active)" },
- },
- responses: {
- 200: {
- // body: "ReportingMenuResponse",
- },
- 204: {},
- },
- }),
- (req: Request, res: Response) => {
- // TODO: implement
- // res.send([] as ReportingMenuResponseSchema);
- res.sendFile(path.join(__dirname, "..", "..", "..", "..", "assets", "temp_report_menu_responses", `${type}.json`));
- },
- );
-}
-export default router;
diff --git a/src/schemas/api/reports/CreateReport.ts b/src/schemas/api/reports/CreateReport.ts
new file mode 100644
index 000000000..7c5606dc8
--- /dev/null
+++ b/src/schemas/api/reports/CreateReport.ts
@@ -0,0 +1,36 @@
+/*
+ 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 .
+*/
+
+// TODO: check
+export interface CreateReportSchema {
+ version: string;
+ variant: string;
+ name: string;
+ language: string;
+ breadcrumbs: number[];
+ elements?: { [key: string]: string[] };
+ channel_id?: string; // snowflake
+ message_id?: string; // snowflake
+ guild_id?: string; // snowflake
+ stage_instance_id?: string; // snowflake
+ guild_scheduled_event_id?: string; // snowflake
+ reported_user_id?: string; // snowflake
+ application_id?: string; // snowflake
+ user_id?: string; // snowflake
+ widget_id?: string; // snowflake
+}
diff --git a/src/schemas/uncategorised/PreloadMessagesRequestSchema.ts b/src/schemas/uncategorised/PreloadMessagesRequestSchema.ts
index 1dc1eece9..5705e7fe5 100644
--- a/src/schemas/uncategorised/PreloadMessagesRequestSchema.ts
+++ b/src/schemas/uncategorised/PreloadMessagesRequestSchema.ts
@@ -17,5 +17,6 @@
*/
export interface PreloadMessagesRequestSchema {
- channels: string[];
+ channels?: string[];
+ channel_ids?: string[];
}
diff --git a/src/util/entities/BaseClass.ts b/src/util/entities/BaseClass.ts
index 5338837d8..1aaf1479d 100644
--- a/src/util/entities/BaseClass.ts
+++ b/src/util/entities/BaseClass.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, InsertResult, ObjectIdColumn, ObjectLiteral, PrimaryColumn } from "typeorm";
+import { BaseEntity, BeforeInsert, BeforeUpdate, Column, ColumnOptions, FindOptionsWhere, InsertResult, ObjectIdColumn, ObjectLiteral, PrimaryColumn } from "typeorm";
import { Snowflake } from "../util/Snowflake";
import { getDatabase } from "../util/Database";
import { OrmUtils } from "../imports/OrmUtils";
@@ -83,3 +83,5 @@ export class BaseClass extends BaseClassWithoutId {
if (!this.id) this.id = Snowflake.generate();
}
}
+
+export const ArrayColumn = (opts: ColumnOptions) => (process.env.DATABASE?.startsWith("postgres") ? Column({ ...opts, array: true }) : Column({ ...opts, type: "simple-array" }));
diff --git a/src/util/entities/ReportMenu.ts b/src/util/entities/ReportMenu.ts
new file mode 100644
index 000000000..26d66adc3
--- /dev/null
+++ b/src/util/entities/ReportMenu.ts
@@ -0,0 +1,43 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2024 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 { BaseClass, BaseClassWithoutId } from "./BaseClass";
+import { Entity, JoinColumn, ManyToOne, Column } from "typeorm";
+import { User } from "./User";
+import { AutomodAction, AutomodRuleActionType, AutomodRuleEventType, AutomodRuleTriggerMetadata, AutomodRuleTriggerType } from "@spacebar/schemas";
+import { ReportMenuType } from "../../schemas/api/reports/ReportMenu";
+
+@Entity({
+ name: "report_menus",
+})
+export class ReportMenu extends BaseClass {
+ @Column()
+ type: ReportMenuType;
+
+ @Column()
+ variant: string;
+
+ @Column()
+ isCurrent: boolean;
+
+ @Column({ nullable: true })
+ inherits?: string;
+
+ @Column({ type: "jsonb" })
+ content: unknown;
+}