fix(guild): import all template channel types, fetch on desktop (#3473)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-14 18:39:09 +02:00
1 parent 00249f9b4d
commit 99be186fa1
50 files changed
+2008 -111

No files matched your search

@@ -0,0 +1,38 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export const DESKTOP_OTHER_PLATFORM_TEMPLATE_CHANNELS = Object.freeze({
fetch: 'desktop-other-platform-template:fetch',
} as const);
const OTHER_PLATFORM_TEMPLATE_CODE_PATTERN = /^[A-Za-z0-9]{1,64}$/;
export function isOtherPlatformTemplateCode(value: unknown): value is string {
return typeof value === 'string' && OTHER_PLATFORM_TEMPLATE_CODE_PATTERN.test(value);
}
export const OtherPlatformTemplateFetchFailure = Object.freeze({
INVALID_CODE: 'invalid_code',
NOT_FOUND: 'not_found',
RATE_LIMITED: 'rate_limited',
NETWORK: 'network',
INVALID_RESPONSE: 'invalid_response',
} as const);
export type OtherPlatformTemplateFetchFailure =
(typeof OtherPlatformTemplateFetchFailure)[keyof typeof OtherPlatformTemplateFetchFailure];
export interface OtherPlatformTemplateFetchSuccess {
readonly ok: true;
readonly template: Readonly<Record<string, unknown>>;
}
export interface OtherPlatformTemplateFetchError {
readonly ok: false;
readonly failure: OtherPlatformTemplateFetchFailure;
}
export type OtherPlatformTemplateFetchResult = OtherPlatformTemplateFetchSuccess | OtherPlatformTemplateFetchError;
export interface OtherPlatformTemplateAPI {
fetch: (code: string) => Promise<OtherPlatformTemplateFetchResult>;
}
@@ -3,7 +3,6 @@
import {
CHANNEL_RATE_LIMIT_PER_USER_MAX,
CHANNEL_RATE_LIMIT_PER_USER_MIN,
CHANNEL_TOPIC_MAX_LENGTH,
VOICE_CHANNEL_CONNECTION_LIMIT_MAX,
VOICE_CHANNEL_CONNECTION_LIMIT_MIN,
VOICE_CHANNEL_USER_LIMIT_MIN,
@@ -13,13 +12,8 @@ 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 TEMPLATE_FORUM_TAGS_MAX = 100;
const TEMPLATE_EMOJI_NAME_MAX_LENGTH = 64;
const TemplateEntityId = z
.union([
@@ -42,6 +36,17 @@ const TemplateOverwriteType = z
})
.pipe(z.number())
.describe('The type of overwrite (0/role = role, 1/member = member)');
const TemplateEmojiReference = z.union([z.string().max(64), z.number().int().nonnegative()]);
const TemplateForumTag = z.object({
name: z.string().max(TEMPLATE_NAME_MAX_LENGTH).describe('The name of the tag'),
moderated: z.boolean().nullish().describe('Whether only moderators can apply the tag'),
emoji_id: TemplateEmojiReference.nullish().describe('A custom emoji reference, ignored on import'),
emoji_name: z.string().max(TEMPLATE_EMOJI_NAME_MAX_LENGTH).nullish().describe('The unicode emoji of the tag'),
});
const TemplateReactionEmoji = z.object({
emoji_id: TemplateEmojiReference.nullish().describe('A custom emoji reference, ignored on import'),
emoji_name: z.string().max(TEMPLATE_EMOJI_NAME_MAX_LENGTH).nullish().describe('The unicode emoji'),
});
const TemplatePermissionOverwrite = z.object({
id: TemplateEntityId.describe('The ID of the role or user for this overwrite'),
type: TemplateOverwriteType,
@@ -51,7 +56,11 @@ const TemplatePermissionOverwrite = z.object({
export const TemplateChannel = z.object({
id: TemplateEntityId.describe('The template-local channel ID'),
type: z.number().describe('The channel type (0 = text, 2 = voice, 4 = category)'),
type: z
.number()
.describe(
'The channel type (0 = text, 2 = voice, 4 = category, 5 = announcement, 13 = stage, 15 = forum, 16 = media)',
),
name: z
.string()
.max(TEMPLATE_NAME_MAX_LENGTH)
@@ -61,9 +70,8 @@ export const TemplateChannel = z.object({
topic: z
.string()
.max(TEMPLATE_TOPIC_MAX_LENGTH)
.transform(clipTopic)
.nullish()
.describe('The channel topic, shortened to the Fluxer channel topic limit'),
.describe('The channel topic or forum guidelines, shortened to the limit for the channel type'),
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'),
@@ -80,18 +88,38 @@ export const TemplateChannel = z.object({
.max(VOICE_CHANNEL_CONNECTION_LIMIT_MAX)
.nullish()
.describe('The per-user voice connection limit for voice channels'),
nsfw: z.boolean().optional().describe('Whether the channel is NSFW'),
nsfw: z.boolean().nullish().describe('Whether the channel is NSFW'),
rate_limit_per_user: z
.number()
.int()
.min(CHANNEL_RATE_LIMIT_PER_USER_MIN)
.max(CHANNEL_RATE_LIMIT_PER_USER_MAX)
.optional()
.nullish()
.describe('Slowmode rate limit in seconds'),
permission_overwrites: z
.array(TemplatePermissionOverwrite)
.optional()
.nullish()
.describe('Permission overwrites for this channel'),
available_tags: z
.array(TemplateForumTag)
.max(TEMPLATE_FORUM_TAGS_MAX)
.nullish()
.describe('Tags for forum and media channels'),
default_reaction_emoji: TemplateReactionEmoji.nullish().describe('Default reaction for forum and media posts'),
default_sort_order: z.number().int().nullish().describe('Default post sort order for forum and media channels'),
default_forum_layout: z.number().int().nullish().describe('Default layout for forum channels'),
default_tag_setting: z.string().max(32).nullish().describe('How posts are filtered by several tags'),
default_thread_rate_limit_per_user: z
.number()
.int()
.nullish()
.describe('Slowmode in seconds copied onto new threads'),
default_auto_archive_duration: z
.number()
.int()
.nullish()
.describe('Default auto archive duration in minutes for new threads'),
flags: z.number().int().nullish().describe('Channel flags'),
});
export type TemplateChannel = z.infer<typeof TemplateChannel>;