summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
authorMathMan05 <mathmanrm@gmail.com>2025-12-01 15:49:48 -0600
committerRory& <root@rory.gay>2026-01-28 21:46:24 +0100
commit00b591abf047c1665a6ea75c20bd449d605a1a84 (patch)
tree41155f0143f43236533e64d9eafd75f3e4463394 /src/api
parentsechema changes+migrations (diff)
downloadserver-ts-00b591abf047c1665a6ea75c20bd449d605a1a84.tar.xz
thread API start
Diffstat (limited to 'src/api')
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/threads.ts108
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts1
-rw-r--r--src/api/util/handlers/Message.ts20
3 files changed, 124 insertions, 5 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
new file mode 100644

index 00000000..1fc91b08 --- /dev/null +++ b/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts
@@ -0,0 +1,108 @@ +/* + 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 { route, sendMessage } from "@spacebar/api"; +import { Message, Channel, emitEvent, User, MessageUpdateEvent } from "@spacebar/util"; +import { MessageThreadCreationSchema, ChannelType, MessageType } from "@spacebar/schemas"; + +import { Request, Response, Router } from "express"; + +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( + "/", + route({ + requestBody: "MessageThreadCreationSchema", + responses: { + 200: {}, + 403: {}, + }, + }), + async (req: Request, res: Response) => { + const { message_id, channel_id } = req.params; + const body = req.body as MessageThreadCreationSchema; + const message = await Message.findOneOrFail({ + where: { id: message_id, channel_id }, + relations: ["guild"], + }); + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + const user = await User.findOneOrFail({ where: { id: req.user_id } }); + const thread = await Channel.createChannel( + { + id: message.id, + owner: user, + parent: channel, + guild: channel.guild, + member_count: 1, + message_count: 1, + total_message_sent: 1, + name: body.name, + guild_id: channel.guild_id, + rate_limit_per_user: body.rate_limit_per_user, + type: ChannelType.GUILD_PUBLIC_THREAD, + 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 }, + ); + message.thread = thread; + message.flags ||= 1 << 5; + await sendMessage({ + channel_id: thread.id, + type: MessageType.THREAD_STARTER_MESSAGE, + message_reference: { + message_id: message.id, + channel_id: channel.id, + guild_id: channel.guild_id, + }, + author_id: user.id, + }); + await Promise.all([ + emitEvent({ + event: "THREAD_CREATE", + channel_id, + data: { + ...thread.toJSON(), + newly_created: true, + }, + }), + message.save(), + emitEvent({ + event: "MESSAGE_UPDATE", + channel_id: message.channel_id, + data: message.toJSON(), + } as MessageUpdateEvent), + ]); + + return res.json(thread.toJSON()); + }, +); + +export default router; diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
index 6b18e2c9..d8ff0f10 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -137,6 +137,7 @@ router.get( sticker_items: true, attachments: true, }, + thread: true, }, }; diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts
index 766abd13..5d1b9fed 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts
@@ -234,7 +234,8 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { if (opts.message_reference.type != 1) { if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild"); - if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); + if (opts.message_reference.channel_id !== opts.channel_id && opts.type !== MessageType.THREAD_STARTER_MESSAGE) + throw new HTTPError("You can only reference messages from this channel"); } message.message_reference = opts.message_reference; @@ -254,14 +255,22 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { }, }); - if (message.referenced_message.channel_id && message.referenced_message.channel_id !== opts.message_reference.channel_id) + 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) + 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 **/ - message.type = MessageType.REPLY; + if (MessageType.THREAD_STARTER_MESSAGE !== message.type) message.type = MessageType.REPLY; } } @@ -274,7 +283,8 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { !opts.sticker_ids?.length && !opts.poll && !opts.components?.length && - opts.message_reference?.type != 1 + opts.message_reference?.type != 1 && + opts.type !== MessageType.THREAD_STARTER_MESSAGE ) { console.log("[Message] Rejecting empty message:", opts, message); throw new HTTPError("Empty messages are not allowed", 50006);