From f4114a48962a6395d49345e70f86ba84cc7d944d Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 1 Dec 2025 11:28:26 -0600 Subject: [PATCH 1/4] sechema changes+migrations --- assets/openapi.json | Bin 891224 -> 892878 bytes assets/schemas.json | Bin 383174 -> 384531 bytes src/schemas/api/channels/Channel.ts | 8 ++++++ src/util/entities/Channel.ts | 14 +++++++++- src/util/entities/Message.ts | 10 +++++++ .../postgres/1764609924756-threads.ts | 25 ++++++++++++++++++ .../postgres/1764610059742-threadtypo.ts | 25 ++++++++++++++++++ 7 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/util/migrations/postgres/1764609924756-threads.ts create mode 100644 src/util/migrations/postgres/1764610059742-threadtypo.ts diff --git a/assets/openapi.json b/assets/openapi.json index d641fe628f65d6bb08f00ec88f1c785fff7af45a..230af20d7613cb196e39bc11b47621e28f33784a 100644 GIT binary patch delta 489 zcmYL^KS&!<9LIU@-g}qCSaTpq{Rb|kg_0qd=%6)=G$I6xg_aIpuI6Z@(X+|vq*Sb! zRSfmJDVd}dYOumBrCvLin&M)uNF213BF!L<;!`6XKJews_xFL{?ndPMR%B+Ayt5Gf z$JM+)k-$P9*+<||$W-t5!d+%RvA)LYu=bwKL$_%+qPyfNdiF>Nu2%_SV239$Ga|mB z7~Gd-nz9TavdV;V^m zR@e}`jQFxayHN7+Fv^@fN98x^Mc|AD5#13CuV;mDT7M{q?VjsPeH3FSycvnlzB~M? z!lxtN@&7OPk$f>^a(eGkr_^nOybpFHiTObsij^Q=2AcJSSpbqv0(I)Hk1eH15`dTo-bH2j32X> z%LT?G^e)^Iji^^1Q%Ct!X}hs%2dq&uCTvTuTJ_{a#(=h{G~vS$6STT#X>0f;v<4E& Y#p7HeB&x(C@k)G>8j0VD$7_B47fpS-C;$Ke delta 124 zcmX@N-|WUFvxY5hWSwN^bg#u zSEmPDVrHBEwv{b*JCN03GQGf;Np<=HYfgdo^BgSO&vUR!d9)u1VFh9~AZ7<*4j|?P RVlE)&24bG=M?!eHya8NaF=_w+ diff --git a/assets/schemas.json b/assets/schemas.json index 46a36939bb19c24f6c05ed2d85a36daf53d6595b..d1017ec4ad559ed9d555a39dd1f9f55f066030fd 100644 GIT binary patch delta 429 zcmX@MQGD_q@rEso+UC<6o-zARpTNx&Jn*Rwnl8&KFr-KrFWDN+OIp(-(*V#rPap~e;&g*lX0_?IuNY;gJDg delta 121 zcmV-<0EYjQycfo?7l5<@D>s*r3j$k~H3JuyZTq&3D4Fn#S b5Mu=ohudxgx7%(6EeeNNd<3^xd { type: ChannelType.DM | ChannelType.GROUP_DM; diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index a250758fa..1d52fa016 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; diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index 508ffd8ca..c38f60abd 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: "channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + thread?: Channel; + @Column({ nullable: true }) @RelationId((message: Message) => message.guild) guild_id?: string; 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`, + ); + } +} From 00b591abf047c1665a6ea75c20bd449d605a1a84 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 1 Dec 2025 15:49:48 -0600 Subject: [PATCH 2/4] thread API start --- assets/openapi.json | Bin 892878 -> 895324 bytes assets/schemas.json | Bin 384531 -> 385192 bytes .../messages/#message_id/threads.ts | 108 ++++++++++++++++++ .../channels/#channel_id/messages/index.ts | 1 + src/api/util/handlers/Message.ts | 20 +++- src/gateway/opcodes/Identify.ts | 5 +- src/schemas/api/messages/Message.ts | 1 + .../MessageThreadCreationSchema.ts | 25 ++++ src/schemas/uncategorised/index.ts | 1 + src/util/entities/Channel.ts | 1 + src/util/entities/Message.ts | 2 +- src/util/interfaces/Event.ts | 1 + .../postgres/1764612754204-threads.ts | 21 ++++ .../postgres/1764622231800-threadGoof.ts | 15 +++ 14 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 src/api/routes/channels/#channel_id/messages/#message_id/threads.ts create mode 100644 src/schemas/uncategorised/MessageThreadCreationSchema.ts create mode 100644 src/util/migration/postgres/1764612754204-threads.ts create mode 100644 src/util/migration/postgres/1764622231800-threadGoof.ts diff --git a/assets/openapi.json b/assets/openapi.json index 230af20d7613cb196e39bc11b47621e28f33784a..9cf9ac7b710a375ad101ca6ca8b4077aadec76ba 100644 GIT binary patch delta 262 zcmX@N-|WsMvxXMN7N#xCE;ZBbH?y*Hg=7?^CZ;$8iOCZb)23U7u!>DT6UZ7h-O_<6 zZ@R;A=D2AunH8r`=wely?%=~JIX!+Ov&!^_4NPj2qTQ!2sADpjUJ%aA+U{Ayyxp^g zW&XG6D=)H2w7)1~1!6WJW(Q&pAm#*OuI(?1xS39{m4GeX&M3z_n{m2)DJwf0#2M2M zeq~ab{@@Ic-1H4C?5xudeB5UF. +*/ + +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 6b18e2c98..d8ff0f102 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 766abd13f..5d1b9fed9 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 { 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 { }, }); - 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 { !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/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 1d52fa016..13ce278e9 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -215,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: diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index c38f60abd..5dea9bcfd 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -52,7 +52,7 @@ export class Message extends BaseClass { @RelationId((message: Message) => message.thread) thread_id?: string; - @JoinColumn({ name: "channel_id" }) + @JoinColumn({ name: "thread_id" }) @ManyToOne(() => Channel, { onDelete: "CASCADE", }) 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"`); + } +} From cc15a5d219ee78ce6de918252a80423591c74058 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 1 Dec 2025 16:51:02 -0600 Subject: [PATCH 3/4] initial recipients work --- .../channels/#channel_id/messages/#message_id/index.ts | 6 +++++- .../#channel_id/messages/#message_id/threads.ts | 8 +++++++- src/api/routes/channels/#channel_id/messages/index.ts | 10 ++++++---- src/api/util/handlers/Message.ts | 9 +++++++++ src/util/entities/Channel.ts | 1 + src/util/entities/Message.ts | 1 + 6 files changed, 29 insertions(+), 6 deletions(-) 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 index 1fc91b081..44e9fc357 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 @@ -17,7 +17,7 @@ */ import { route, sendMessage } from "@spacebar/api"; -import { Message, Channel, emitEvent, User, MessageUpdateEvent } from "@spacebar/util"; +import { Message, Channel, emitEvent, User, MessageUpdateEvent, Recipient } from "@spacebar/util"; import { MessageThreadCreationSchema, ChannelType, MessageType } from "@spacebar/schemas"; import { Request, Response, Router } from "express"; @@ -48,6 +48,8 @@ router.post( 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, @@ -61,6 +63,7 @@ router.post( 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, @@ -72,6 +75,9 @@ router.post( void 0, { skipPermissionCheck: true, keepId: true, skipEventEmit: true }, ); + + recipient.save(); + message.thread = thread; message.flags ||= 1 << 5; await sendMessage({ diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index d8ff0f102..9a1174cb2 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -137,7 +137,11 @@ router.get( sticker_items: true, attachments: true, }, - thread: true, + thread: { + recipients: { + user: true, + }, + }, }, }; @@ -247,6 +251,7 @@ router.get( return x; }); + //console.log(ret); await Promise.all( ret @@ -410,8 +415,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); @@ -465,7 +468,6 @@ 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 diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 5d1b9fed9..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); diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 13ce278e9..8478fcb7c 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -533,6 +533,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 5dea9bcfd..cd18fb6b7 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -244,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, }; } From 9bd97516fff36f092c98ac2be14e9d26a36b1235 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 11 Dec 2025 22:00:33 -0600 Subject: [PATCH 4/4] thread stuff ig --- .../routes/channels/#channel_id/messages/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 9a1174cb2..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 }); @@ -472,6 +474,16 @@ router.post( // 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(