/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer 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. * * Fluxer 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 Fluxer. If not, see . */ import { GuildMemberProfileFlags, GuildMemberProfileFlagsDescriptions, SystemChannelFlags, SystemChannelFlagsDescriptions, } from '@fluxer/constants/src/GuildConstants'; import { AVATAR_MAX_SIZE, EMOJI_MAX_SIZE, STICKER_MAX_SIZE, VALID_TEMP_BAN_DURATIONS, } from '@fluxer/constants/src/LimitConstants'; import {SudoVerificationSchema} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import {VanityURLCodeType} from '@fluxer/schema/src/primitives/ChannelValidators'; import {createBase64StringType} from '@fluxer/schema/src/primitives/FileValidators'; import { DefaultMessageNotificationsSchema, GuildExplicitContentFilterSchema, GuildMFALevelSchema, GuildVerificationLevelSchema, NSFWLevelSchema, SplashCardAlignmentSchema, } from '@fluxer/schema/src/primitives/GuildValidators'; import {QueryBooleanType} from '@fluxer/schema/src/primitives/QueryValidators'; import { ColorType, createBitflagInt32Type, createStringType, SnowflakeType, UnsignedInt64Type, withFieldDescription, } from '@fluxer/schema/src/primitives/SchemaPrimitives'; import {PasswordType} from '@fluxer/schema/src/primitives/UserValidators'; import {z} from 'zod'; export const GuildCreateRequest = z.object({ name: createStringType(1, 100).describe('The name of the guild (1-100 characters)'), icon: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3))) .nullish() .describe('Base64-encoded image data for the guild icon'), empty_features: z.boolean().optional().describe('Whether to create the guild without default features'), }); export type GuildCreateRequest = z.infer; export const GuildUpdateRequest = z .object({ name: createStringType(1, 100).describe('The name of the guild (1-100 characters)'), icon: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3))) .nullish() .describe('Base64-encoded image data for the guild icon'), system_channel_id: SnowflakeType.nullish().describe('The ID of the channel where system messages are sent'), system_channel_flags: createBitflagInt32Type( SystemChannelFlags, SystemChannelFlagsDescriptions, 'Bitfield of system channel flags controlling which messages are suppressed', 'SystemChannelFlags', ), afk_channel_id: SnowflakeType.nullish().describe('The ID of the AFK voice channel'), afk_timeout: z .number() .int() .min(60) .max(3600) .describe('AFK timeout in seconds (60-3600) before moving users to the AFK channel'), default_message_notifications: withFieldDescription( DefaultMessageNotificationsSchema, 'Default notification level for new members', ), verification_level: withFieldDescription( GuildVerificationLevelSchema, 'Required verification level for members to participate', ), mfa_level: withFieldDescription(GuildMFALevelSchema, 'Required MFA level for moderation actions'), nsfw_level: withFieldDescription(NSFWLevelSchema, 'The NSFW level of the guild'), explicit_content_filter: withFieldDescription( GuildExplicitContentFilterSchema, 'Level of content filtering for explicit media', ), banner: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3))) .nullish() .describe('Base64-encoded image data for the guild banner'), splash: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3))) .nullish() .describe('Base64-encoded image data for the guild splash screen'), embed_splash: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3))) .nullish() .describe('Base64-encoded image data for the embedded invite splash'), splash_card_alignment: SplashCardAlignmentSchema.optional().describe( 'Alignment of the splash card (center, left, or right)', ), features: z.array(z.string()).describe('Array of guild feature strings'), message_history_cutoff: z.iso .datetime() .nullish() .describe( 'ISO8601 timestamp controlling how far back members without Read Message History can access messages. Set to null to disable historical access.', ), }) .partial() .merge(SudoVerificationSchema); export type GuildUpdateRequest = z.infer; export const GuildMemberUpdateRequest = z.object({ nick: createStringType(1, 32).nullish().describe('The nickname to set for the member (1-32 characters)'), roles: z .array(SnowflakeType) .max(100, 'Maximum 100 roles allowed') .optional() .transform((ids) => (ids ? new Set(ids) : undefined)) .describe('Array of role IDs to assign to the member (max 100)'), avatar: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3))) .nullish() .describe('Base64-encoded image data for the member guild avatar'), banner: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3))) .nullish() .describe('Base64-encoded image data for the member guild banner'), bio: createStringType(1, 320).nullish().describe('The member guild profile bio (1-320 characters)'), pronouns: createStringType(1, 40).nullish().describe('The member guild profile pronouns (1-40 characters)'), accent_color: ColorType.nullish().describe('The accent color for the member guild profile as an integer'), profile_flags: createBitflagInt32Type( GuildMemberProfileFlags, GuildMemberProfileFlagsDescriptions, 'Bitfield of profile flags for the member', 'GuildMemberProfileFlags', ).nullish(), mute: z.boolean().optional().describe('Whether the member is muted in voice channels'), deaf: z.boolean().optional().describe('Whether the member is deafened in voice channels'), communication_disabled_until: z.iso .datetime() .nullish() .describe('ISO8601 timestamp until which the member is timed out'), timeout_reason: createStringType(1, 512) .nullish() .describe('The reason for timing out the member (1-512 characters)'), channel_id: SnowflakeType.nullish().describe('The voice channel ID to move the member to'), connection_id: createStringType(1, 32).nullish().describe('The voice connection ID for the member'), }); export type GuildMemberUpdateRequest = z.infer; export const MyGuildMemberUpdateRequest = GuildMemberUpdateRequest.omit({roles: true}).partial(); export type MyGuildMemberUpdateRequest = z.infer; export const GuildRoleCreateRequest = z.object({ name: createStringType(1, 100).describe('The name of the role (1-100 characters)'), color: ColorType.default(0x000000).describe('The color of the role as an integer (default: 0)'), permissions: UnsignedInt64Type.optional().describe('fluxer:UnsignedInt64Type The permissions bitfield for the role'), }); export type GuildRoleCreateRequest = z.infer; export const GuildRoleUpdateRequest = z.object({ name: createStringType(1, 100).optional().describe('The name of the role (1-100 characters)'), color: ColorType.optional().describe('The color of the role as an integer'), permissions: UnsignedInt64Type.optional().describe('fluxer:UnsignedInt64Type The permissions bitfield for the role'), hoist: z.boolean().optional().describe('Whether the role should be displayed separately in the member list'), hoist_position: z.number().int().nullish().describe('The position of the role in the hoisted member list'), mentionable: z.boolean().optional().describe('Whether the role can be mentioned by anyone'), }); export type GuildRoleUpdateRequest = z.infer; export const GuildEmojiCreateRequest = z.object({ name: createStringType(2, 32) .refine((value) => /^[a-zA-Z0-9_]+$/.test(value), 'Emoji name can only contain letters, numbers, and underscores') .describe('The name of the emoji (2-32 characters, alphanumeric and underscores only)'), image: createBase64StringType(1, Math.ceil(EMOJI_MAX_SIZE * (4 / 3))).describe( 'Base64-encoded image data for the emoji', ), }); export type GuildEmojiCreateRequest = z.infer; export const GuildEmojiUpdateRequest = GuildEmojiCreateRequest.pick({name: true}); export type GuildEmojiUpdateRequest = z.infer; export const GuildEmojiBulkCreateRequest = z.object({ emojis: z .array(GuildEmojiCreateRequest) .min(1, 'At least one emoji is required') .max(50, 'Maximum 50 emojis per batch') .describe('Array of emoji objects to create (1-50 emojis per batch)'), }); export type GuildEmojiBulkCreateRequest = z.infer; export const GuildStickerCreateRequest = z.object({ name: createStringType(2, 30).describe('The name of the sticker (2-30 characters)'), description: createStringType(1, 500).nullish().describe('Description of the sticker (1-500 characters)'), tags: z .array(createStringType(1, 30)) .min(0) .max(10) .optional() .default([]) .describe('Array of autocomplete/suggestion tags (max 10 tags, each 1-30 characters)'), image: createBase64StringType(1, Math.ceil(STICKER_MAX_SIZE * (4 / 3))).describe( 'Base64-encoded image data for the sticker', ), }); export type GuildStickerCreateRequest = z.infer; export const GuildStickerUpdateRequest = GuildStickerCreateRequest.pick({ name: true, description: true, tags: true, }); export type GuildStickerUpdateRequest = z.infer; export const GuildStickerBulkCreateRequest = z.object({ stickers: z .array(GuildStickerCreateRequest) .min(1, 'At least one sticker is required') .max(50, 'Maximum 50 stickers per batch') .describe('Array of sticker objects to create (1-50 stickers per batch)'), }); export type GuildStickerBulkCreateRequest = z.infer; export const GuildTransferOwnershipRequest = z.object({ new_owner_id: SnowflakeType.describe('The ID of the user to transfer ownership to'), password: PasswordType.optional().describe('The current owner password for verification'), }); export type GuildTransferOwnershipRequest = z.infer; export const GuildBanCreateRequest = z.object({ delete_message_days: z .number() .int() .min(0) .max(7) .default(0) .describe('Number of days of messages to delete from the banned user (0-7)'), reason: createStringType(0, 512).nullish().describe('The reason for the ban (max 512 characters)'), ban_duration_seconds: z .number() .int() .refine((val) => val === 0 || VALID_TEMP_BAN_DURATIONS.has(val), { message: `Ban duration must be 0 (permanent) or one of the valid durations: ${Array.from(VALID_TEMP_BAN_DURATIONS).join(', ')} seconds`, }) .optional() .describe('Duration of the ban in seconds (0 for permanent, or a valid temporary duration)'), }); export type GuildBanCreateRequest = z.infer; export const GuildListQuery = z.object({ before: SnowflakeType.optional().describe('Get guilds before this guild ID'), after: SnowflakeType.optional().describe('Get guilds after this guild ID'), limit: z.coerce.number().int().min(1).max(200).default(200).describe('Maximum number of guilds to return (1-200)'), with_counts: QueryBooleanType.describe('Include approximate member and presence counts'), }); export type GuildListQuery = z.infer; export const GuildDeleteRequest = z .object({ password: PasswordType.optional().describe('The owner password for verification'), }) .merge(SudoVerificationSchema); export type GuildDeleteRequest = z.infer; export const GuildVanityURLUpdateRequest = z.object({ code: VanityURLCodeType.nullish().describe('The new vanity URL code (2-32 characters, alphanumeric and hyphens)'), }); export type GuildVanityURLUpdateRequest = z.infer; export const GuildVanityURLUpdateResponse = z.object({ code: createStringType(2, 32).describe('The new vanity URL code'), }); export type GuildVanityURLUpdateResponse = z.infer; export const GuildRoleHoistPositionItem = z.object({ id: SnowflakeType.describe('The ID of the role'), hoist_position: z.number().int().describe('The new hoist position for the role'), }); export type GuildRoleHoistPositionItem = z.infer; export const GuildRoleHoistPositionsRequest = z.array(GuildRoleHoistPositionItem); export type GuildRoleHoistPositionsRequest = z.infer; export const GuildRolePositionItem = z.object({ id: SnowflakeType.describe('The ID of the role'), position: z.number().int().optional().describe('The new position for the role'), }); export type GuildRolePositionItem = z.infer; export const GuildRolePositionsRequest = z.array(GuildRolePositionItem); export type GuildRolePositionsRequest = z.infer; export const GuildMemberListQuery = z.object({ limit: z.coerce .number() .int() .min(1) .max(1000) .default(1) .describe('Maximum number of members to return (1-1000, default 1)'), after: SnowflakeType.optional().describe('Get members after this user ID for pagination'), }); export type GuildMemberListQuery = z.infer;