summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-02-04 00:19:51 +0100
committerRory& <root@rory.gay>2026-02-04 00:19:51 +0100
commitb953291b99371c1576ea81c499703163aca44588 (patch)
tree2e229ba95929504c576c7489860b13197bb40412 /src/api
parentMerge remote-tracking branch 'mathium05/errorFix' (diff)
parentmore changes (diff)
downloadserver-ts-b953291b99371c1576ea81c499703163aca44588.tar.xz
Merge remote-tracking branch 'mathium05/threadFixes'
Diffstat (limited to 'src/api')
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/threads.ts13
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts54
-rw-r--r--src/api/routes/channels/#channel_id/thread-members.ts25
-rw-r--r--src/api/routes/channels/#channel_id/threads.ts173
-rw-r--r--src/api/util/handlers/Message.ts58
5 files changed, 278 insertions, 45 deletions
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts b/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts

index 2f834851..a2cb86c2 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts
@@ -32,6 +32,7 @@ router.post( "/", route({ requestBody: "MessageThreadCreationSchema", + permission: "CREATE_PUBLIC_THREADS", responses: { 200: {}, 403: {}, @@ -63,7 +64,7 @@ router.post( name: body.name, guild_id: channel.guild_id, rate_limit_per_user: body.rate_limit_per_user, - type: ChannelType.GUILD_PUBLIC_THREAD, + type: channel.type === ChannelType.GUILD_NEWS ? ChannelType.GUILD_NEWS_THREAD : ChannelType.GUILD_PUBLIC_THREAD, recipients: [], thread_metadata: { archived: false, @@ -91,6 +92,16 @@ router.post( }, author_id: user.id, }); + sendMessage({ + channel_id: channel.id, + type: MessageType.THREAD_CREATED, + content: thread.name, + message_reference: { + channel_id: channel.id, + guild_id: channel.guild_id, + }, + author_id: user.id, + }); await Promise.all([ emitEvent({ event: "THREAD_CREATE", diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
index 74e30aad..54ef7f3e 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -42,6 +42,10 @@ import { uploadFile, User, Recipient, + ThreadMember, + ThreadMemberFlags, + ThreadMembersUpdateEvent, + ThreadCreateEvent, } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -266,10 +270,10 @@ router.get( // polyfill message references for old messages await Promise.all( ret - .filter((msg) => msg.message_reference && !msg.referenced_message?.id) + .filter((msg) => msg.message_reference && !msg.referenced_message?.id && msg.message_reference.message_id) .map(async (msg) => { const whereOptions: { id: string; guild_id?: string; channel_id?: string } = { - id: msg.message_reference!.message_id, + id: msg.message_reference!.message_id as string, }; if (msg.message_reference!.guild_id) whereOptions.guild_id = msg.message_reference!.guild_id; if (msg.message_reference!.channel_id) whereOptions.channel_id = msg.message_reference!.channel_id; @@ -286,7 +290,7 @@ router.get( ); // TODO: config max upload size -const messageUpload = multer({ +export const messageUpload = multer({ limits: { fileSize: Config.get().limits.message.maxAttachmentSize, fields: 10, @@ -315,7 +319,7 @@ router.post( }, route({ requestBody: "MessageCreateSchema", - permission: "SEND_MESSAGES", + permission: "VIEW_CHANNEL", right: "SEND_MESSAGES", responses: { 200: { @@ -337,6 +341,48 @@ router.post( where: { id: channel_id }, relations: { recipients: { user: true } }, }); + if (channel.isThread()) { + req.permission!.hasThrow("SEND_MESSAGES_IN_THREADS"); + if (channel.recipients && !channel.recipients.find(({ id }) => id === req.user_id)) { + const member = await Member.findOneOrFail({ where: { id: req.user_id, guild_id: channel.guild_id! } }); + + if (!(await ThreadMember.existsBy({ member_idx: member.index, id: channel_id }))) { + const threadMember = ThreadMember.create({ + member_idx: member.index, + id: channel_id, + join_timestamp: new Date(), + muted: false, + flags: ThreadMemberFlags.ALL_MESSAGES, + }); + await threadMember.save(); + + // increment member count + if (channel.member_count !== null && channel.member_count !== undefined) { + channel.member_count++; + await channel.save(); + } + + await emitEvent({ + event: "THREAD_MEMBERS_UPDATE", + data: { + guild_id: channel.guild_id!, + id: channel.id, + member_count: channel.member_count, + added_members: [{ user_id: req.user_id, ...threadMember.toJSON() }], + }, + channel_id: channel.id, + } as ThreadMembersUpdateEvent); + + await emitEvent({ + event: "THREAD_CREATE", + data: { ...channel.toJSON(), newly_created: false }, + user_id: req.user_id, + } as ThreadCreateEvent); + } + } + } else { + req.permission!.hasThrow("SEND_MESSAGES"); + } if (!channel.isWritable()) { throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400); } diff --git a/src/api/routes/channels/#channel_id/thread-members.ts b/src/api/routes/channels/#channel_id/thread-members.ts
index 685252a8..281ed2f9 100644 --- a/src/api/routes/channels/#channel_id/thread-members.ts +++ b/src/api/routes/channels/#channel_id/thread-members.ts
@@ -81,6 +81,7 @@ router.post( } const member = await Member.findOneOrFail({ where: { id: user_id, guild_id: thread.guild_id! } }); + if (await ThreadMember.existsBy({ member_idx: member.index, id: channel_id })) { return res.status(204).send(); } @@ -100,7 +101,7 @@ router.post( guild_id: thread.guild_id!, id: thread.id, member_count: thread.member_count, - added_member_ids: [user_id], + added_members: [{ user_id: user_id, ...threadMember.toJSON() }], }, channel_id: thread.id, } as ThreadMembersUpdateEvent); @@ -153,17 +154,17 @@ router.delete( }, channel_id: thread.id, } as ThreadMembersUpdateEvent); - - await emitEvent({ - event: "THREAD_DELETE", - data: { - id: thread.id, - guild_id: thread.guild_id!, - parent_id: thread.parent_id!, - type: thread.type, - }, - user_id: user_id, - } as ThreadDeleteEvent); + if (thread.type === ChannelType.GUILD_PRIVATE_THREAD) + await emitEvent({ + event: "THREAD_DELETE", + data: { + id: thread.id, + guild_id: thread.guild_id!, + parent_id: thread.parent_id!, + type: thread.type, + }, + user_id: user_id, + } as ThreadDeleteEvent); return res.status(204).send(); }, diff --git a/src/api/routes/channels/#channel_id/threads.ts b/src/api/routes/channels/#channel_id/threads.ts new file mode 100644
index 00000000..430716b5 --- /dev/null +++ b/src/api/routes/channels/#channel_id/threads.ts
@@ -0,0 +1,173 @@ +/* + 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 <https://www.gnu.org/licenses/>. +*/ + +import { handleMessage, postHandleMessage, route, sendMessage } from "@spacebar/api"; +import { Message, Channel, emitEvent, User, MessageUpdateEvent, Recipient, uploadFile, Attachment, Member, ReadState, MessageCreateEvent } from "@spacebar/util"; +import { MessageThreadCreationSchema, ChannelType, MessageType, ThreadCreationSchema, MessageCreateAttachment, MessageCreateCloudAttachment } from "@spacebar/schemas"; + +import { Request, Response, Router } from "express"; +import { messageUpload } from "./messages"; + +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: "ThreadCreationSchema", + permission: "CREATE_PUBLIC_THREADS", + responses: { + 200: {}, + 403: {}, + }, + }), + async (req: Request, res: Response) => { + // TODO: check for differences with https://github.com/spacebarchat/server/pull/876/files#diff-95be9c4cdfd8ba6f67361cd40b9abc8226b35d83e2bb44bf5b4682f1d66155e9 + const { channel_id } = req.params; + const body = req.body as ThreadCreationSchema; + + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + const user = await User.findOneOrFail({ where: { id: req.user_id } }); + + const thread = await Channel.createChannel( + { + owner: user, + parent: channel, + guild: channel.guild, + member_count: 1, + message_count: body.message ? 1 : 0, + total_message_sent: body.message ? 1 : 0, + name: body.name, + guild_id: channel.guild_id, + rate_limit_per_user: body.rate_limit_per_user, + type: body.type, + recipients: [], + thread_metadata: { + archived: false, + auto_archive_duration: body.auto_archive_duration || channel.default_auto_archive_duration || 4320, + archive_timestamp: new Date().toISOString(), + locked: false, + create_timestamp: new Date().toISOString(), + }, + }, + void 0, + { skipPermissionCheck: true, keepId: true, skipEventEmit: true }, + ); + const recipient = Recipient.create({ channel_id: channel.id, user }); + + await recipient.save(); + + await Promise.all([ + emitEvent({ + event: "THREAD_CREATE", + channel_id, + data: { + ...thread.toJSON(), + newly_created: true, + }, + }), + ]); + if (body.type !== ChannelType.GUILD_PRIVATE_THREAD) + sendMessage({ + channel_id: channel.id, + type: MessageType.THREAD_CREATED, + content: thread.name, + message_reference: { + channel_id: channel.id, + guild_id: channel.guild_id, + }, + author_id: user.id, + }); + if (body.message) { + const files = (req.files as Express.Multer.File[]) ?? []; + const attachments: (Attachment | MessageCreateAttachment | MessageCreateCloudAttachment)[] = body.message.attachments ?? []; + for (const currFile of files) { + try { + const file = await uploadFile(`/attachments/${channel.id}`, currFile); + attachments.push(Attachment.create({ ...file, proxy_url: file.url })); + } catch (error) { + return res.status(400).json({ message: error?.toString() }); + } + } + const embeds = body.message.embeds || []; + const message = await handleMessage({ + ...body, + type: 0, + pinned: false, + author_id: req.user_id, + embeds, + channel_id, + attachments, + timestamp: new Date(), + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore wrong type but idk why it's mad + message.edited_timestamp = null; + if (message.guild_id) { + // handleMessage will fetch the Member, but only if they are not guild owner. + // have to fetch ourselves otherwise. + if (!message.member) { + message.member = await Member.findOneOrFail({ + where: { id: req.user_id, guild_id: message.guild_id }, + relations: { roles: true }, + }); + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + message.member.roles = message.member.roles.filter((x) => x.id != x.guild_id).map((x) => x.id); + } + let read_state = await ReadState.findOne({ + where: { user_id: req.user_id, channel_id }, + }); + if (!read_state) read_state = ReadState.create({ user_id: req.user_id, channel_id }); + read_state.last_message_id = message.id; + //It's a little more complicated than this but this'll do + read_state.mention_count = 0; + + await Promise.all([ + read_state.save(), + message.save(), + emitEvent({ + event: "MESSAGE_CREATE", + channel_id: channel_id, + data: message, + } as MessageCreateEvent), + message.guild_id ? Member.update({ id: req.user_id, guild_id: message.guild_id }, { last_message_id: message.id }) : null, + ]); + postHandleMessage(message).catch((e) => console.error("[Message] post-message handler failed", e)); + } + + return res.json(thread.toJSON()); + }, +); + +export default router; diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts
index 759f60b8..bf1f5e96 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts
@@ -248,38 +248,40 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { } message.message_reference = opts.message_reference; - message.referenced_message = await Message.findOneOrFail({ - where: { - id: opts.message_reference.message_id, - }, - relations: { - author: true, - webhook: true, - application: true, - mentions: true, - mention_roles: true, - mention_channels: true, - sticker_items: true, - attachments: true, - }, - }); + if (message.message_reference.message_id) { + message.referenced_message = await Message.findOneOrFail({ + where: { + id: opts.message_reference.message_id, + }, + relations: { + author: true, + webhook: true, + application: true, + mentions: true, + mention_roles: true, + mention_channels: true, + sticker_items: true, + attachments: true, + }, + }); - if ( - message.referenced_message.channel_id && - message.referenced_message.channel_id !== opts.message_reference.channel_id && - opts.type !== MessageType.THREAD_STARTER_MESSAGE - ) - throw new HTTPError("Referenced message not found in the specified channel", 404); - if ( - message.referenced_message.guild_id && - message.referenced_message.guild_id !== opts.message_reference.guild_id && - opts.type !== MessageType.THREAD_STARTER_MESSAGE - ) - throw new HTTPError("Referenced message not found in the specified channel", 404); + if ( + message.referenced_message.channel_id && + message.referenced_message.channel_id !== opts.message_reference.channel_id && + opts.type !== MessageType.THREAD_STARTER_MESSAGE + ) + throw new HTTPError("Referenced message not found in the specified channel", 404); + if ( + message.referenced_message.guild_id && + message.referenced_message.guild_id !== opts.message_reference.guild_id && + opts.type !== MessageType.THREAD_STARTER_MESSAGE + ) + throw new HTTPError("Referenced message not found in the specified channel", 404); + } } /** Q: should be checked if the referenced message exists? ANSWER: NO otherwise backfilling won't work **/ - if (MessageType.THREAD_STARTER_MESSAGE !== message.type) message.type = MessageType.REPLY; + if (MessageType.THREAD_STARTER_MESSAGE !== message.type && MessageType.THREAD_CREATED !== message.type) message.type = MessageType.REPLY; } }