From c26b0f3a4c248b249cee04a68d65d1198ff7adcd Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 5 Feb 2026 10:50:48 -0600 Subject: [PATCH] create tags --- src/api/routes/channels/#channel_id/tags.ts | 66 +++++++++++++++++++ src/gateway/opcodes/Identify.ts | 1 + src/schemas/uncategorised/TagCreateSchema.ts | 24 +++++++ src/schemas/uncategorised/index.ts | 1 + src/util/entities/Channel.ts | 3 + src/util/entities/Tag.ts | 4 +- .../postgres/1770310028511-nullableTags.ts | 15 +++++ 7 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 src/api/routes/channels/#channel_id/tags.ts create mode 100644 src/schemas/uncategorised/TagCreateSchema.ts create mode 100644 src/util/migration/postgres/1770310028511-nullableTags.ts diff --git a/src/api/routes/channels/#channel_id/tags.ts b/src/api/routes/channels/#channel_id/tags.ts new file mode 100644 index 000000000..34b491565 --- /dev/null +++ b/src/api/routes/channels/#channel_id/tags.ts @@ -0,0 +1,66 @@ +/* + 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 } from "@spacebar/api"; +import { Channel, ChannelUpdateEvent, emitEvent, Tag } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { TagCreateSchema } from "@spacebar/schemas"; + +const router: Router = Router({ mergeParams: true }); + +router.post( + "/", + route({ + requestBody: "TagCreateSchema", + permission: "MANAGE_CHANNELS", + responses: { + 201: {}, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const body = req.body as TagCreateSchema; + const { channel_id } = req.params as Record; + + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + relations: ["available_tags"], + }); + + if (!channel.isForum()) throw new Error("is not thread only channel"); + + const tag = Tag.create({ + channel, + ...body, + }); + channel.available_tags?.push(tag); + + await Promise.all([ + tag.save(), + emitEvent({ + event: "CHANNEL_UPDATE", + data: channel.toJSON(), + channel_id, + } as ChannelUpdateEvent), + ]); + + res.json(channel.toJSON()); + }, +); + +export default router; diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index efed94fc6..00635e30b 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -304,6 +304,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { type: Not(In([ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD, ChannelType.GUILD_NEWS_THREAD])), }, order: { guild_id: "ASC" }, + relations: ["available_tags"], }), ), timePromise(() => diff --git a/src/schemas/uncategorised/TagCreateSchema.ts b/src/schemas/uncategorised/TagCreateSchema.ts new file mode 100644 index 000000000..70c5cc30e --- /dev/null +++ b/src/schemas/uncategorised/TagCreateSchema.ts @@ -0,0 +1,24 @@ +/* + 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 TagCreateSchema { + name: string; + moderated?: boolean; + emoji_id?: string; + emoji_name?: string; +} diff --git a/src/schemas/uncategorised/index.ts b/src/schemas/uncategorised/index.ts index 9083fd2ab..a18105453 100644 --- a/src/schemas/uncategorised/index.ts +++ b/src/schemas/uncategorised/index.ts @@ -95,3 +95,4 @@ export * from "./MessageThreadCreationSchema"; export * from "./ThreadCreationSchema"; export * from "./MessageActivity"; export * from "./PostDataSchema"; +export * from "./TagCreateSchema"; diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index afd907bab..52867a0c0 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -593,6 +593,9 @@ export class Channel extends BaseClass { isThread() { return this.type === ChannelType.GUILD_NEWS_THREAD || this.type === ChannelType.GUILD_PUBLIC_THREAD || this.type === ChannelType.GUILD_PRIVATE_THREAD; } + isForum() { + return this.type === ChannelType.GUILD_FORUM || this.type === ChannelType.GUILD_MEDIA; + } isPrivateThread() { return this.type === ChannelType.GUILD_PRIVATE_THREAD; diff --git a/src/util/entities/Tag.ts b/src/util/entities/Tag.ts index 3b4136d78..143fad54b 100644 --- a/src/util/entities/Tag.ts +++ b/src/util/entities/Tag.ts @@ -39,10 +39,10 @@ export class Tag extends BaseClass { @Column() moderated: boolean = false; - @Column() + @Column({ nullable: true }) emoji_id?: string; - @Column() + @Column({ nullable: true }) emoji_name?: string; toJSON() { diff --git a/src/util/migration/postgres/1770310028511-nullableTags.ts b/src/util/migration/postgres/1770310028511-nullableTags.ts new file mode 100644 index 000000000..ce4b26096 --- /dev/null +++ b/src/util/migration/postgres/1770310028511-nullableTags.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class NullableTags1770310028511 implements MigrationInterface { + name = "NullableTags1770310028511"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tags" ALTER COLUMN "emoji_id" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "tags" ALTER COLUMN "emoji_name" DROP NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tags" ALTER COLUMN "emoji_name" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "tags" ALTER COLUMN "emoji_id" SET NOT NULL`); + } +}