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 2f8348519..a2cb86c2b 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 74e30aadf..54ef7f3e9 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 685252a8b..281ed2f90 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 000000000..430716b51
--- /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 .
+*/
+
+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 759f60b8e..bf1f5e969 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_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;
}
}
diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts
index 77421765b..e1c57d552 100644
--- a/src/gateway/opcodes/Identify.ts
+++ b/src/gateway/opcodes/Identify.ts
@@ -47,6 +47,7 @@ import {
SessionsReplace,
Sticker,
Stopwatch,
+ ThreadMember,
timeFunction,
timePromise,
TraceNode,
@@ -208,7 +209,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
where: { id: this.user_id },
select: {
// We only want some member props
- ...Object.fromEntries(MemberPrivateProjection.map((x) => [x, true])),
+ ...Object.fromEntries(["index", ...MemberPrivateProjection].map((x) => [x, true])),
settings: true, // guild settings
roles: { id: true }, // the full role is fetched from the `guild` relation
guild: { id: true },
@@ -300,7 +301,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
Channel.find({
where: {
guild_id: In(guildIds),
- type: Not(ChannelType.GUILD_PUBLIC_THREAD),
+ type: Not(In([ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD, ChannelType.GUILD_NEWS_THREAD])),
},
order: { guild_id: "ASC" },
}),
@@ -414,9 +415,17 @@ export async function onIdentify(this: WebSocket, data: Payload) {
];
});
const mergedMembersTime = taskSw.getElapsedAndReset();
+ const member_idx = members.map(({ index }) => index);
+
+ const threadMembers = await ThreadMember.find({
+ where: { member_idx: In(member_idx) },
+ relations: { channel: { recipients: { user: true } } },
+ });
+ const threadMemberTime = taskSw.getElapsedAndReset();
// Populated with guilds 'unavailable' currently
// Just for bots
+
const pending_guilds: Guild[] = [];
// Generate guilds list ( make them unavailable if user is bot )
@@ -447,12 +456,23 @@ 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(),
+ };
+ });
return {
...member.guild.toJSON(),
joined_at: member.joined_at,
- threads: member.guild.channels.filter((x) => x.isThread()),
+ threads,
};
});
const generateGuildsListTime = taskSw.getElapsedAndReset();
@@ -667,6 +687,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
emitPresenceUpdateTime,
remapReadStateIdsTime,
buildReadyEventDataTime,
+ threadMemberTime,
};
for (const [key, value] of Object.entries(times)) {
if (value) {
diff --git a/src/schemas/uncategorised/MessageActivity.ts b/src/schemas/uncategorised/MessageActivity.ts
new file mode 100644
index 000000000..bebd29702
--- /dev/null
+++ b/src/schemas/uncategorised/MessageActivity.ts
@@ -0,0 +1,25 @@
+/*
+ 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 MessageActivity {
+ type: 1 | 3 | 5;
+ session_id?: string;
+ party_id?: string;
+ name_override?: string;
+ icon_override?: string;
+}
diff --git a/src/schemas/uncategorised/MessageCreateSchema.ts b/src/schemas/uncategorised/MessageCreateSchema.ts
index 345be32cf..7c0d0728b 100644
--- a/src/schemas/uncategorised/MessageCreateSchema.ts
+++ b/src/schemas/uncategorised/MessageCreateSchema.ts
@@ -49,7 +49,7 @@ export interface MessageCreateSchema {
replied_user?: boolean;
};
message_reference?: {
- message_id: string;
+ message_id?: string;
channel_id?: string;
guild_id?: string;
fail_if_not_exists?: boolean;
diff --git a/src/schemas/uncategorised/MessageThreadCreationSchema.ts b/src/schemas/uncategorised/MessageThreadCreationSchema.ts
index ed78d7dd8..2d0044dbe 100644
--- a/src/schemas/uncategorised/MessageThreadCreationSchema.ts
+++ b/src/schemas/uncategorised/MessageThreadCreationSchema.ts
@@ -16,12 +16,10 @@
along with this program. If not, see .
*/
-import { ChannelType } from "../api";
-
export interface MessageThreadCreationSchema {
auto_archive_duration?: number;
rate_limit_per_user?: number;
name: string;
- type?: ChannelType.GUILD_NEWS_THREAD | ChannelType.GUILD_PRIVATE_THREAD | ChannelType.GUILD_PUBLIC_THREAD;
- location?: string; //0 clue what this means lol
+ location?: string; //ignore it
+ type?: number;
}
diff --git a/src/schemas/uncategorised/ThreadCreationSchema.ts b/src/schemas/uncategorised/ThreadCreationSchema.ts
new file mode 100644
index 000000000..ca1f25e28
--- /dev/null
+++ b/src/schemas/uncategorised/ThreadCreationSchema.ts
@@ -0,0 +1,47 @@
+/*
+ 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 { ActionRowComponent, ChannelType, Embed } from "#schemas/api";
+import { MessageActivity } from "./MessageActivity";
+import { MessageCreateAttachment, MessageCreateCloudAttachment } from "./MessageCreateSchema";
+
+export interface ThreadCreationSchema {
+ auto_archive_duration?: number;
+ rate_limit_per_user?: number;
+ name: string;
+ type: ChannelType.GUILD_PUBLIC_THREAD | ChannelType.GUILD_PRIVATE_THREAD;
+ invitable?: boolean;
+ applied_tags?: string[];
+ location?: string; //Ignore it
+ message?: {
+ content?: string;
+ embeds?: Embed[];
+ allowed_mentions?: {
+ parse?: string[];
+ roles?: string[];
+ users?: string[];
+ replied_user?: boolean;
+ };
+ components?: ActionRowComponent[] | null;
+ sticker_ids?: string[];
+ activity?: MessageActivity;
+ application_id?: string;
+ flags?: number;
+ attachments?: (MessageCreateAttachment | MessageCreateCloudAttachment)[];
+ };
+}
diff --git a/src/schemas/uncategorised/index.ts b/src/schemas/uncategorised/index.ts
index 545e3e554..5a0d9ebda 100644
--- a/src/schemas/uncategorised/index.ts
+++ b/src/schemas/uncategorised/index.ts
@@ -92,3 +92,5 @@ export * from "./WebhookExecuteSchema";
export * from "./WebhookUpdateSchema";
export * from "./WidgetModifySchema";
export * from "./MessageThreadCreationSchema";
+export * from "./ThreadCreationSchema";
+export * from "./MessageActivity";
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index a387f313e..e3c9c8e87 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -676,7 +676,7 @@ export class Channel extends BaseClass {
user_limit: this.user_limit || undefined,
rate_limit_per_user: this.rate_limit_per_user || undefined,
owner_id: this.owner_id || undefined,
- ...(this.type === ChannelType.GUILD_PUBLIC_THREAD && this.recipients ? { member_ids_preview: this.recipients.map((_) => _.user.id) } : {}),
+ ...(this.isThread() && this.recipients ? { member_ids_preview: this.recipients.map((_) => _.user.id) } : {}),
};
}
}
diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index cd18fb6b7..620f20fc5 100644
--- a/src/util/entities/Message.ts
+++ b/src/util/entities/Message.ts
@@ -173,7 +173,7 @@ export class Message extends BaseClass {
@Column({ type: "simple-json", nullable: true })
message_reference?: {
- message_id: string;
+ message_id?: string;
channel_id?: string;
guild_id?: string;
type?: number; // 0 = DEFAULT, 1 = FORWARD
diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index b1ed1fdfe..6ddf6b298 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -645,7 +645,7 @@ export interface ThreadMembersUpdateEvent extends Event {
id: string;
guild_id: string;
member_count: number;
- added_members?: ThreadMember[];
+ added_members?: (ThreadMember & { user_id: string })[];
removed_member_ids?: string[];
};
}