From 80456861ac72156030136647391555d5be79c7ea Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 6 Oct 2026 22:46:47 +0200 Subject: [PATCH] fix(api): accept long forum topics in imported templates (#3244) --- .../guild/tests/GuildTemplateImport.test.ts | 24 +++++++++++++++++-- fluxer_api/src/api/openapi/openapi.json | 4 ++-- .../src/domains/guild/GuildTemplateSchemas.ts | 15 +++++++++++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/fluxer_api/src/api/guild/tests/GuildTemplateImport.test.ts b/fluxer_api/src/api/guild/tests/GuildTemplateImport.test.ts index d7166a795..9e63719d6 100644 --- a/fluxer_api/src/api/guild/tests/GuildTemplateImport.test.ts +++ b/fluxer_api/src/api/guild/tests/GuildTemplateImport.test.ts @@ -6,7 +6,7 @@ import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHa import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {SystemChannelFlags} from '@fluxer/constants/src/GuildConstants'; -import {VOICE_CHANNEL_USER_LIMIT_MAX} from '@fluxer/constants/src/LimitConstants'; +import {CHANNEL_TOPIC_MAX_LENGTH, VOICE_CHANNEL_USER_LIMIT_MAX} from '@fluxer/constants/src/LimitConstants'; import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; import {afterAll, beforeAll, beforeEach, describe, expect, test} from 'vitest'; @@ -215,7 +215,7 @@ describe('Guild Template Import', () => { ['a slowmode above the channel maximum', {rate_limit_per_user: 1_000_000_000}], ['a fractional position', {position: 0.5}], ['a negative position', {position: -3}], - ['a topic above the channel maximum', {topic: 'x'.repeat(1025)}], + ['a topic above the template maximum', {topic: 'x'.repeat(4097)}], ['a name above the channel maximum', {name: 'x'.repeat(101)}], ['a negative user limit', {type: ChannelTypes.GUILD_VOICE, user_limit: -1}], ['a voice connection limit above the maximum', {type: ChannelTypes.GUILD_VOICE, voice_connection_limit: 100_000}], @@ -273,6 +273,26 @@ describe('Guild Template Import', () => { expect(channels.find((channel) => channel.name === 'town-hall')?.user_limit).toBe(VOICE_CHANNEL_USER_LIMIT_MAX); expect(channels.find((channel) => channel.name === 'lounge')?.voice_connection_limit).toBe(100); }); + test('accepts forum-length topics and shortens them to the channel topic limit', async () => { + const account = await createTestAccount(harness); + const longTopic = `${'a'.repeat(CHANNEL_TOPIC_MAX_LENGTH - 1)}\u{1F600}${'b'.repeat(300)}`; + const guild = await createBuilder(harness, account.token) + .post('/guilds') + .body({ + name: 'Forum Guild', + template: buildMinimalTemplate({ + channels: [ + {id: 6001, type: ChannelTypes.GUILD_TEXT, name: 'general', position: 0, topic: longTopic}, + {id: 6002, type: 15, name: 'projects', position: 1, topic: 'c'.repeat(1356)}, + ], + }), + }) + .execute(); + const channels = await getGuildChannels(harness, account.token, guild.id); + const general = channels.find((channel) => channel.name === 'general'); + expect(general?.topic).toBe('a'.repeat(CHANNEL_TOPIC_MAX_LENGTH - 1)); + expect(channels.find((channel) => channel.name === 'projects')).toBeUndefined(); + }); }); const DEFAULT_EVERYONE_PERMISSIONS = Permissions.VIEW_CHANNEL.toString(); diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index d397fdbd0..070697f57 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -33075,8 +33075,8 @@ "anyOf": [{"type": "string", "maxLength": 100}, {"type": "null"}] }, "topic": { - "description": "The channel topic", - "anyOf": [{"type": "string", "maxLength": 1024}, {"type": "null"}] + "description": "The channel topic, shortened to the Fluxer channel topic limit", + "anyOf": [{"type": "string", "maxLength": 4096}, {"type": "null"}] }, "position": {"description": "The position of the channel", "$ref": "#/components/schemas/Int32Type"}, "parent_id": { diff --git a/packages/schema/src/domains/guild/GuildTemplateSchemas.ts b/packages/schema/src/domains/guild/GuildTemplateSchemas.ts index 6c7379f35..705563325 100644 --- a/packages/schema/src/domains/guild/GuildTemplateSchemas.ts +++ b/packages/schema/src/domains/guild/GuildTemplateSchemas.ts @@ -12,6 +12,14 @@ import {ColorType, createStringType, Int32Type} from '@fluxer/schema/src/primiti import {z} from 'zod'; const TEMPLATE_NAME_MAX_LENGTH = 100; +const TEMPLATE_TOPIC_MAX_LENGTH = 4096; + +function clipTopic(value: string): string { + if (value.length <= CHANNEL_TOPIC_MAX_LENGTH) return value; + const last = value.charCodeAt(CHANNEL_TOPIC_MAX_LENGTH - 1); + const end = last >= 0xd800 && last <= 0xdbff ? CHANNEL_TOPIC_MAX_LENGTH - 1 : CHANNEL_TOPIC_MAX_LENGTH; + return value.slice(0, end); +} const TemplateEntityId = z .union([ @@ -50,7 +58,12 @@ export const TemplateChannel = z.object({ .nullish() .transform((value) => value ?? '') .describe('The name of the channel'), - topic: z.string().max(CHANNEL_TOPIC_MAX_LENGTH).nullish().describe('The channel topic'), + topic: z + .string() + .max(TEMPLATE_TOPIC_MAX_LENGTH) + .nullish() + .transform((value) => (value == null ? value : clipTopic(value))) + .describe('The channel topic, shortened to the Fluxer channel topic limit'), position: Int32Type.describe('The position of the channel'), parent_id: TemplateEntityId.nullish().describe('The template-local ID of the parent category'), bitrate: z.number().int().nonnegative().nullish().describe('The bitrate for voice channels'),