From f1f253e515b051dbc2f9fe5761f130c40de8408e Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 4 Feb 2026 16:07:42 -0600 Subject: [PATCH] more forum fixes --- .../routes/channels/#channel_id/post-data.ts | 84 +++++++++++++++ .../routes/channels/#channel_id/threads.ts | 101 +++++++++++++++++- src/gateway/opcodes/Identify.ts | 32 +++--- src/schemas/uncategorised/PostDataSchema.ts | 21 ++++ src/schemas/uncategorised/index.ts | 7 +- 5 files changed, 228 insertions(+), 17 deletions(-) create mode 100644 src/api/routes/channels/#channel_id/post-data.ts create mode 100644 src/schemas/uncategorised/PostDataSchema.ts diff --git a/src/api/routes/channels/#channel_id/post-data.ts b/src/api/routes/channels/#channel_id/post-data.ts new file mode 100644 index 000000000..6f1f530e1 --- /dev/null +++ b/src/api/routes/channels/#channel_id/post-data.ts @@ -0,0 +1,84 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 { handleMessage, postHandleMessage, route, sendMessage } from "@spacebar/api"; +import { Channel, emitEvent, User, uploadFile, Attachment, Member, ReadState, MessageCreateEvent, FieldErrors, getPermission, ThreadMember, Message } from "@spacebar/util"; +import { ChannelType, MessageType, ThreadCreationSchema, MessageCreateAttachment, MessageCreateCloudAttachment, PostDataSchema } from "@spacebar/schemas"; + +import { Request, Response, Router } from "express"; +import { messageUpload } from "./messages"; +import { HTTPError } from "#util/util/lambert-server"; +import { FindManyOptions, FindOptionsOrder, In, Like } from "typeorm"; + +const router = Router({ mergeParams: true }); + +// TODO: public read receipts & privacy scoping +// TODO: send read state event to all channel members +// TODO: advance-only notification cursor + +router.post( + "/", + messageUpload.any(), + (req, res, next) => { + if (req.body.payload_json) { + req.body = JSON.parse(req.body.payload_json); + } + + next(); + }, + route({ + requestBody: "PostDataSchema", + permission: "VIEW_CHANNEL", + responses: { + 200: {}, + 403: {}, + }, + }), + async (req: Request, res: Response) => { + const body = (req.body as PostDataSchema).thread_ids; + const threads = await Channel.find({ + where: { + id: In(body), + }, + }); + const [messages, members] = await Promise.all([ + Message.find({ + where: { + id: In(threads.map(({ id }) => id)), + }, + }), + Member.find({ + where: { + id: In(threads.map(({ owner_id }) => owner_id)), + }, + }), + ]); + const objRet: { threads: Record } = { threads: {} }; + for (const thread of threads) { + const owner = members.find(({ id }) => id === thread.owner_id)?.toJSON() || null; + const first_message = messages.find(({ channel_id }) => channel_id === thread.id)?.toJSON() || null; + objRet.threads[thread.id] = { + owner, + first_message, + }; + } + return res.json(objRet); + }, +); + +export default router; diff --git a/src/api/routes/channels/#channel_id/threads.ts b/src/api/routes/channels/#channel_id/threads.ts index 129da9a74..6149d3891 100644 --- a/src/api/routes/channels/#channel_id/threads.ts +++ b/src/api/routes/channels/#channel_id/threads.ts @@ -17,11 +17,13 @@ */ import { handleMessage, postHandleMessage, route, sendMessage } from "@spacebar/api"; -import { Channel, emitEvent, User, uploadFile, Attachment, Member, ReadState, MessageCreateEvent } from "@spacebar/util"; +import { Channel, emitEvent, User, uploadFile, Attachment, Member, ReadState, MessageCreateEvent, FieldErrors, getPermission, ThreadMember, Message } from "@spacebar/util"; import { ChannelType, MessageType, ThreadCreationSchema, MessageCreateAttachment, MessageCreateCloudAttachment } from "@spacebar/schemas"; import { Request, Response, Router } from "express"; import { messageUpload } from "./messages"; +import { HTTPError } from "#util/util/lambert-server"; +import { FindManyOptions, FindOptionsOrder, In, Like } from "typeorm"; const router = Router({ mergeParams: true }); @@ -168,4 +170,101 @@ router.post( }, ); +router.get( + "/search", + route({ + responses: { + 200: { + body: "GuildMessagesSearchResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 422: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { name, slop, tag, tag_setting, archived, sort_by, sort_order, limit, offset, max_id, min_id } = req.query as Record; + const { channel_id } = req.params as Record; + + const parsedLimit = Number(limit) || 25; + if (parsedLimit < 1 || parsedLimit > 25) throw new HTTPError("limit must be between 1 and 25", 422); + + let order: FindOptionsOrder; + switch (sort_by) { + case undefined: + case "creation_time": + order = { + created_at: sort_order === "asc" ? "ASC" : "DESC", + }; + break; + case "last_message_time": + order = { + last_message_id: sort_order === "asc" ? "ASC" : "DESC", + }; + break; + default: + throw FieldErrors({ + sort_by: { + message: "Value must be one of ('last_message_time', 'archive_time', 'relevance', 'creation_time').", + code: "BASE_TYPE_CHOICES", + }, + }); // todo this is wrong + } + const channel = await Channel.findOneOrFail({ + where: { + id: channel_id, + }, + }); + + const permissions = await getPermission(req.user_id, channel.guild_id, channel); + permissions.hasThrow("VIEW_CHANNEL"); + if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json({ threads: [], total_results: 0, members: [], has_more: false, first_messages: [] }); + const member = await Member.findOneOrFail({ where: { guild_id: channel.guild_id, id: req.user_id } }); + + const query: FindManyOptions = { + order, + where: { + parent_id: channel_id, + ...(name ? { name: Like(`%${name}%`) } : {}), + ...(archived + ? { + thread_metadata: { + archived: archived === "true" ? true : false, + }, + } + : {}), + }, + relations: {}, + }; + + const threads: Channel[] = await Channel.find({ ...query, take: parsedLimit || 0, skip: offset ? Number(offset) : 0 }); + const total_results = await Channel.count(query); + + const members = ThreadMember.find({ + where: { + member_idx: member.index, + id: In(threads.map(({ id }) => id)), + }, + }); + + const messages = Message.find({ + where: { + id: In(threads.map(({ id }) => id)), + }, + }); + + const left = total_results - threads.length - +offset; + return res.json({ + threads: threads.map((_) => _.toJSON()), + members: (await members).map((_) => _.toJSON()), + messages: (await messages).map((_) => _.toJSON()), + total_results, + has_more: left > 0, + }); + }, +); + export default router; diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index dacc35ccd..efed94fc6 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -419,8 +419,8 @@ export async function onIdentify(this: WebSocket, data: Payload) { const threadMembers = await ThreadMember.find({ where: { member_idx: In(member_idx) }, - relations: { channel: { thread_members: { member: true } } }, }); + const threadMemberMap = new Map(threadMembers.map((member) => [member.id, member] as const)); const threadMemberTime = taskSw.getElapsedAndReset(); // Populated with guilds 'unavailable' currently @@ -428,6 +428,15 @@ export async function onIdentify(this: WebSocket, data: Payload) { const pending_guilds: Guild[] = []; + const allThreads = ( + await Channel.find({ + where: { + type: In([ChannelType.GUILD_NEWS_THREAD, ChannelType.GUILD_PUBLIC_THREAD]), + guild_id: In(members.map(({ guild }) => guild.id)), + }, + }) + ).filter(({ thread_metadata }) => thread_metadata?.archived === false); + // Generate guilds list ( make them unavailable if user is bot ) const guilds: GuildOrUnavailable[] = members.map((member) => { member.guild.channels = member.guild.channels @@ -456,23 +465,20 @@ export async function onIdentify(this: WebSocket, data: Payload) { pending_guilds.push(member.guild); return { id: member.guild.id, unavailable: true }; } - const threads = threadMembers - .filter(({ channel }) => channel.guild_id === member.guild.id) - .map((_) => { - return { - member: { - ..._.toJSON(), - channel: undefined, - }, - ..._.channel.toJSON(), - }; - }); + + const threads: Channel[] = allThreads.filter((_) => _.guild_id === member.guild_id); return { ...member.guild.toJSON(), joined_at: member.joined_at, - threads, + threads: threads.map((thread) => { + const member = threadMemberMap.get(thread.id)?.toJSON(); + return { + ...thread.toJSON(), + member, + }; + }), }; }); const generateGuildsListTime = taskSw.getElapsedAndReset(); diff --git a/src/schemas/uncategorised/PostDataSchema.ts b/src/schemas/uncategorised/PostDataSchema.ts new file mode 100644 index 000000000..b380610d3 --- /dev/null +++ b/src/schemas/uncategorised/PostDataSchema.ts @@ -0,0 +1,21 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +export interface PostDataSchema { + thread_ids: string[]; +} diff --git a/src/schemas/uncategorised/index.ts b/src/schemas/uncategorised/index.ts index 5a0d9ebda..9083fd2ab 100644 --- a/src/schemas/uncategorised/index.ts +++ b/src/schemas/uncategorised/index.ts @@ -1,17 +1,17 @@ /* 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 . */ @@ -94,3 +94,4 @@ export * from "./WidgetModifySchema"; export * from "./MessageThreadCreationSchema"; export * from "./ThreadCreationSchema"; export * from "./MessageActivity"; +export * from "./PostDataSchema";