diff --git a/assets/openapi.json b/assets/openapi.json
index d641fe628..9cf9ac7b7 100644
Binary files a/assets/openapi.json and b/assets/openapi.json differ
diff --git a/assets/schemas.json b/assets/schemas.json
index 46a36939b..71631da3f 100644
Binary files a/assets/schemas.json and b/assets/schemas.json differ
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
index 934efc316..7058bd3ce 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -35,7 +35,7 @@ import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import multer from "multer";
import { handleMessage, postHandleMessage, route } from "../../../../../util";
-import { MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageEditSchema } from "@spacebar/schemas";
+import { MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageEditSchema, ChannelType } from "@spacebar/schemas";
const router = Router({ mergeParams: true });
// TODO: message content/embed string length limit
@@ -293,6 +293,10 @@ router.delete(
const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
+ if (channel.type === ChannelType.GUILD_PUBLIC_THREAD) {
+ if (channel.message_count !== undefined) channel.message_count--;
+ channel.save(); //Save async, it's fine
+ }
const message = await Message.findOneOrFail({
where: { id: message_id },
});
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 000000000..44e9fc357
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts
@@ -0,0 +1,114 @@
+/*
+ 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 { route, sendMessage } from "@spacebar/api";
+import { Message, Channel, emitEvent, User, MessageUpdateEvent, Recipient } 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 recipient = Recipient.create({ channel_id: message.id, user });
+
+ 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,
+ 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 },
+ );
+
+ recipient.save();
+
+ 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 6b18e2c98..00241149e 100644
--- a/src/api/routes/channels/#channel_id/messages/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -41,6 +41,7 @@ import {
stringGlobToRegexp,
uploadFile,
User,
+ Recipient,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
@@ -59,6 +60,7 @@ import {
Reaction,
ReadStateType,
RelationshipType,
+ ChannelType,
} from "@spacebar/schemas";
const router: Router = Router({ mergeParams: true });
@@ -137,6 +139,11 @@ router.get(
sticker_items: true,
attachments: true,
},
+ thread: {
+ recipients: {
+ user: true,
+ },
+ },
},
};
@@ -246,6 +253,7 @@ router.get(
return x;
});
+ //console.log(ret);
await Promise.all(
ret
@@ -409,8 +417,6 @@ router.post(
//@ts-ignore dont care2
message.edited_timestamp = null;
- channel.last_message_id = message.id;
-
if (channel.isDm()) {
const channel_dto = await DmChannelDTO.from(channel);
@@ -464,11 +470,20 @@ router.post(
data: message,
} as MessageCreateEvent),
message.guild_id ? Member.update({ id: req.user_id, guild_id: message.guild_id }, { last_message_id: message.id }) : null,
- channel.save(),
]);
// no await as it shouldnt block the message send function and silently catch error
postHandleMessage(message).catch((e) => console.error("[Message] post-message handler failed", e));
+ if (channel.type === ChannelType.GUILD_PUBLIC_THREAD && channel.recipients && !channel.recipients.find((_) => _.user.id === req.user_id)) {
+ const rec = Recipient.create({
+ channel_id: channel.id,
+ user_id: req.user_id,
+ });
+ channel.recipients.push(rec);
+ rec.save().then(() => {
+ channel.save();
+ });
+ }
return res.json(
message.withSignedAttachments(
diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts
index 766abd13f..c81de300d 100644
--- a/src/api/util/handlers/Message.ts
+++ b/src/api/util/handlers/Message.ts
@@ -106,6 +106,15 @@ export async function handleMessage(opts: MessageOptions): Promise {
components: opts.components ?? undefined, // Fix Discord-Go?
});
const ephermal = (message.flags & (1 << 6)) !== 0;
+ if (!ephermal && channel.type === ChannelType.GUILD_PUBLIC_THREAD) {
+ const rep = Channel.getRepository();
+ console.log(channel.id);
+ await rep.increment({ id: channel.id }, "message_count", 1);
+ await rep.increment({ id: channel.id }, "total_message_sent", 1);
+ }
+ if (!ephermal) {
+ channel.last_message_id = message.id;
+ }
if (cloudAttachments && cloudAttachments.length > 0) {
console.log("[Message] Processing attachments for message", message.id, ":", message.attachments);
@@ -234,7 +243,8 @@ export async function handleMessage(opts: MessageOptions): Promise {
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 +264,22 @@ export async function handleMessage(opts: MessageOptions): Promise {
},
});
- 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 +292,8 @@ export async function handleMessage(opts: MessageOptions): Promise {
!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);
diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts
index 34732c381..be52450a8 100644
--- a/src/gateway/opcodes/Identify.ts
+++ b/src/gateway/opcodes/Identify.ts
@@ -296,7 +296,10 @@ export async function onIdentify(this: WebSocket, data: Payload) {
] = await Promise.all([
timePromise(() =>
Channel.find({
- where: { guild_id: In(guildIds) },
+ where: {
+ guild_id: In(guildIds),
+ type: Not(ChannelType.GUILD_PUBLIC_THREAD),
+ },
order: { guild_id: "ASC" },
}),
),
diff --git a/src/schemas/api/channels/Channel.ts b/src/schemas/api/channels/Channel.ts
index 2d11bf803..4bfe3b2f4 100644
--- a/src/schemas/api/channels/Channel.ts
+++ b/src/schemas/api/channels/Channel.ts
@@ -36,6 +36,14 @@ export enum ChannelPermissionOverwriteType {
member = 1,
group = 2,
}
+export interface threadMetadata {
+ archived: boolean;
+ auto_archive_duration: number;
+ archive_timestamp: string;
+ locked: boolean;
+ invitable?: boolean;
+ create_timestamp: string; //Discord docs say this is optional, but it's only for after a certain date so it's not
+}
export interface DMChannel extends Omit {
type: ChannelType.DM | ChannelType.GROUP_DM;
diff --git a/src/schemas/api/messages/Message.ts b/src/schemas/api/messages/Message.ts
index a46c8f10f..b6144f7f8 100644
--- a/src/schemas/api/messages/Message.ts
+++ b/src/schemas/api/messages/Message.ts
@@ -39,6 +39,7 @@ export enum MessageType {
ENCRYPTED = 16,
REPLY = 19,
APPLICATION_COMMAND = 20, // application command or self command invocation
+ THREAD_STARTER_MESSAGE = 21,
ROUTE_ADDED = 41, // custom message routing: new route affecting that channel
ROUTE_DISABLED = 42, // custom message routing: given route no longer affecting that channel
SELF_COMMAND_SCRIPT = 43, // self command scripts
diff --git a/src/schemas/uncategorised/MessageThreadCreationSchema.ts b/src/schemas/uncategorised/MessageThreadCreationSchema.ts
new file mode 100644
index 000000000..729922240
--- /dev/null
+++ b/src/schemas/uncategorised/MessageThreadCreationSchema.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 MessageThreadCreationSchema {
+ auto_archive_duration?: number;
+ rate_limit_per_user?: number;
+ name: string;
+ type?: number;
+ location?: string; //0 clue what this means lol
+}
diff --git a/src/schemas/uncategorised/index.ts b/src/schemas/uncategorised/index.ts
index 603151e5e..545e3e554 100644
--- a/src/schemas/uncategorised/index.ts
+++ b/src/schemas/uncategorised/index.ts
@@ -91,3 +91,4 @@ export * from "./WebhookCreateSchema";
export * from "./WebhookExecuteSchema";
export * from "./WebhookUpdateSchema";
export * from "./WidgetModifySchema";
+export * from "./MessageThreadCreationSchema";
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index e978a9c6d..5ec84acfa 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -31,7 +31,7 @@ import { User } from "./User";
import { VoiceState } from "./VoiceState";
import { Webhook } from "./Webhook";
import { Member } from "./Member";
-import { ChannelPermissionOverwrite, ChannelPermissionOverwriteType, ChannelType, PublicUserProjection } from "@spacebar/schemas";
+import { ChannelPermissionOverwrite, ChannelPermissionOverwriteType, ChannelType, PublicUserProjection, threadMetadata } from "@spacebar/schemas";
@Entity({
name: "channels",
@@ -152,6 +152,18 @@ export class Channel extends BaseClass {
@Column({ nullable: true })
default_thread_rate_limit_per_user?: number = 0;
+ @Column({ type: "simple-json", nullable: true })
+ thread_metadata?: threadMetadata;
+
+ @Column({ nullable: true })
+ member_count?: number;
+
+ @Column({ nullable: true })
+ message_count?: number;
+
+ @Column({ nullable: true })
+ total_message_sent?: number;
+
/** Must be calculated Channel.calculatePosition */
position: number;
@@ -203,6 +215,7 @@ export class Channel extends BaseClass {
}
switch (channel.type) {
+ case ChannelType.GUILD_PUBLIC_THREAD:
case ChannelType.GUILD_TEXT:
case ChannelType.GUILD_NEWS:
case ChannelType.GUILD_VOICE:
@@ -532,6 +545,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) } : {}),
};
}
}
diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index 508ffd8ca..cd18fb6b7 100644
--- a/src/util/entities/Message.ts
+++ b/src/util/entities/Message.ts
@@ -48,6 +48,16 @@ export class Message extends BaseClass {
})
channel: Channel;
+ @Column({ nullable: true })
+ @RelationId((message: Message) => message.thread)
+ thread_id?: string;
+
+ @JoinColumn({ name: "thread_id" })
+ @ManyToOne(() => Channel, {
+ onDelete: "CASCADE",
+ })
+ thread?: Channel;
+
@Column({ nullable: true })
@RelationId((message: Message) => message.guild)
guild_id?: string;
@@ -234,6 +244,7 @@ export class Message extends BaseClass {
poll: this.poll ?? undefined,
content: this.content ?? "",
pinned: this.pinned,
+ thread: this.thread ? this.thread.toJSON() : this.thread,
};
}
diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index d72a8e213..4e25e4cbd 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -775,6 +775,7 @@ export type EVENT =
| "RELATIONSHIP_UPDATE"
| "SESSIONS_REPLACE"
| "USER_SETTINGS_PROTO_UPDATE"
+ | "THREAD_CREATE"
| CUSTOMEVENTS;
export type CUSTOMEVENTS = "INVALIDATED" | "RATELIMIT";
diff --git a/src/util/migration/postgres/1764612754204-threads.ts b/src/util/migration/postgres/1764612754204-threads.ts
new file mode 100644
index 000000000..e3690dacc
--- /dev/null
+++ b/src/util/migration/postgres/1764612754204-threads.ts
@@ -0,0 +1,21 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Threads1764612754204 implements MigrationInterface {
+ name = "Threads1764612754204";
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "messages" ADD "thread_id" character varying`);
+ await queryRunner.query(`ALTER TABLE "channels" ADD "thread_metadata" text`);
+ await queryRunner.query(`ALTER TABLE "channels" ADD "member_count" integer`);
+ await queryRunner.query(`ALTER TABLE "channels" ADD "message_count" integer`);
+ await queryRunner.query(`ALTER TABLE "channels" ADD "total_message_sent" integer`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "channels" DROP COLUMN "total_message_sent"`);
+ await queryRunner.query(`ALTER TABLE "channels" DROP COLUMN "message_count"`);
+ await queryRunner.query(`ALTER TABLE "channels" DROP COLUMN "member_count"`);
+ await queryRunner.query(`ALTER TABLE "channels" DROP COLUMN "thread_metadata"`);
+ await queryRunner.query(`ALTER TABLE "messages" DROP COLUMN "thread_id"`);
+ }
+}
diff --git a/src/util/migration/postgres/1764622231800-threadGoof.ts b/src/util/migration/postgres/1764622231800-threadGoof.ts
new file mode 100644
index 000000000..3a2737eac
--- /dev/null
+++ b/src/util/migration/postgres/1764622231800-threadGoof.ts
@@ -0,0 +1,15 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class ThreadGoof1764622231800 implements MigrationInterface {
+ name = "ThreadGoof1764622231800";
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "messages" ADD CONSTRAINT "FK_bb3af7f695d50083e6523290d41" FOREIGN KEY ("thread_id") REFERENCES "channels"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "messages" DROP CONSTRAINT "FK_bb3af7f695d50083e6523290d41"`);
+ }
+}
diff --git a/src/util/migrations/postgres/1764609924756-threads.ts b/src/util/migrations/postgres/1764609924756-threads.ts
new file mode 100644
index 000000000..efa2ce7f4
--- /dev/null
+++ b/src/util/migrations/postgres/1764609924756-threads.ts
@@ -0,0 +1,25 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Threads1764609924756 implements MigrationInterface {
+ name = "Threads1764609924756";
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "messages" DROP CONSTRAINT "FK_bb3af7f695d50083e6523290d41"`);
+ await queryRunner.query(`ALTER TABLE "channels" DROP COLUMN "total_message_sent"`);
+ await queryRunner.query(`ALTER TABLE "channels" ADD "total_messsage_sent" integer`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "thread_metadata" DROP NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "member_count" DROP NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "message_count" DROP NOT NULL`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "message_count" SET NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "member_count" SET NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "thread_metadata" SET NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" DROP COLUMN "total_messsage_sent"`);
+ await queryRunner.query(`ALTER TABLE "channels" ADD "total_message_sent" integer NOT NULL`);
+ await queryRunner.query(
+ `ALTER TABLE "messages" ADD CONSTRAINT "FK_bb3af7f695d50083e6523290d41" FOREIGN KEY ("thread_id") REFERENCES "channels"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+ );
+ }
+}
diff --git a/src/util/migrations/postgres/1764610059742-threadtypo.ts b/src/util/migrations/postgres/1764610059742-threadtypo.ts
new file mode 100644
index 000000000..2fe2a985a
--- /dev/null
+++ b/src/util/migrations/postgres/1764610059742-threadtypo.ts
@@ -0,0 +1,25 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Threadtypo1764610059742 implements MigrationInterface {
+ name = "Threadtypo1764610059742";
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "messages" DROP CONSTRAINT "FK_bb3af7f695d50083e6523290d41"`);
+ await queryRunner.query(`ALTER TABLE "channels" DROP COLUMN "total_message_sent"`);
+ await queryRunner.query(`ALTER TABLE "channels" ADD "total_messsage_sent" integer`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "thread_metadata" DROP NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "member_count" DROP NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "message_count" DROP NOT NULL`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "message_count" SET NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "member_count" SET NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" ALTER COLUMN "thread_metadata" SET NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "channels" DROP COLUMN "total_messsage_sent"`);
+ await queryRunner.query(`ALTER TABLE "channels" ADD "total_message_sent" integer NOT NULL`);
+ await queryRunner.query(
+ `ALTER TABLE "messages" ADD CONSTRAINT "FK_bb3af7f695d50083e6523290d41" FOREIGN KEY ("thread_id") REFERENCES "channels"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+ );
+ }
+}