thread API start

This commit is contained in:
MathMan05
2026-01-28 21:46:24 +01:00
committed by Rory&
parent f4114a4896
commit 00b591abf0
14 changed files with 194 additions and 7 deletions
@@ -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;
@@ -137,6 +137,7 @@ router.get(
sticker_items: true,
attachments: true,
},
thread: true,
},
};
+15 -5
View File
@@ -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);