refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,114 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {AuditLogActionTypeSchema} from '@fluxer/schema/src/primitives/AuditLogValidators';
import {
coerceNumberFromString,
Int32Type,
SnowflakeStringType,
SnowflakeType,
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {WebhookTypeSchema} from '@fluxer/schema/src/primitives/WebhookValidators';
import {z} from 'zod';
const PermissionsDiffSchema = z.object({
added: z.array(z.string()),
removed: z.array(z.string()),
});
const AuditLogChangeValueSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.string()),
z.array(z.number()),
PermissionsDiffSchema,
z.null(),
]);
export const AuditLogChangeSchema = z.object({
key: z.string().describe('The field that changed'),
old_value: AuditLogChangeValueSchema.optional().describe('Value before the change'),
new_value: AuditLogChangeValueSchema.optional().describe('Value after the change'),
});
export type AuditLogChange = z.infer<typeof AuditLogChangeSchema>;
const AuditLogOptionsSchema = z.object({
channel_id: z.string().optional().describe('Channel ID for relevant actions'),
count: z.number().optional().describe('Count of items affected'),
delete_member_days: z.string().optional().describe('Number of days of messages to delete on member ban'),
id: z.string().optional().describe('ID of the affected entity'),
integration_type: z.number().optional().describe('Type of integration'),
message_id: z.string().optional().describe('Message ID for relevant actions'),
members_removed: z.number().optional().describe('Number of members removed'),
role_name: z.string().optional().describe('Name of the role'),
type: z.number().optional().describe('Type identifier'),
inviter_id: z.string().optional().describe('ID of the user who created the invite'),
max_age: z.number().optional().describe('Maximum age of the invite in seconds'),
max_uses: z.number().optional().describe('Maximum number of uses for the invite'),
temporary: z.boolean().optional().describe('Whether the invite grants temporary membership'),
uses: z.number().optional().describe('Number of times the invite has been used'),
});
export const GuildAuditLogEntryResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this audit log entry'),
action_type: AuditLogActionTypeSchema,
user_id: SnowflakeStringType.nullish().describe('The user ID of the user who performed the action'),
target_id: z.string().nullish().describe('The ID of the affected entity (user, channel, role, invite code, etc.)'),
reason: z.string().optional().describe('The reason provided for the action'),
options: AuditLogOptionsSchema.optional().describe('Additional options depending on action type'),
changes: z.array(AuditLogChangeSchema).optional().describe('Changes made to the target'),
});
export type GuildAuditLogEntryResponse = z.infer<typeof GuildAuditLogEntryResponse>;
export const AuditLogWebhookResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this webhook'),
type: WebhookTypeSchema,
guild_id: SnowflakeStringType.nullish().describe('The guild ID this webhook belongs to'),
channel_id: SnowflakeStringType.nullish().describe('The channel ID this webhook posts to'),
name: z.string().describe('The name of the webhook'),
avatar_hash: z.string().nullish().describe('The hash of the webhook avatar'),
});
export type AuditLogWebhookResponse = z.infer<typeof AuditLogWebhookResponse>;
export const GuildAuditLogListResponse = z.object({
audit_log_entries: z.array(GuildAuditLogEntryResponse).max(100).describe('Array of audit log entries'),
users: z.array(UserPartialResponse).max(100).describe('Users referenced in the audit log entries'),
webhooks: z.array(AuditLogWebhookResponse).max(100).describe('Webhooks referenced in the audit log entries'),
});
export type GuildAuditLogListResponse = z.infer<typeof GuildAuditLogListResponse>;
export const GuildAuditLogListQuery = z.object({
limit: coerceNumberFromString(Int32Type.max(100))
.optional()
.describe('Maximum number of audit log entries to return (1-100)'),
before: SnowflakeType.optional().describe('Get entries before this audit log entry ID'),
after: SnowflakeType.optional().describe('Get entries after this audit log entry ID'),
user_id: SnowflakeType.optional().describe('Filter entries by the user who performed the action'),
action_type: coerceNumberFromString(AuditLogActionTypeSchema)
.optional()
.describe('Filter entries by the type of action'),
});
export type GuildAuditLogListQuery = z.infer<typeof GuildAuditLogListQuery>;

View File

@@ -0,0 +1,128 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {
DISCOVERY_DESCRIPTION_MAX_LENGTH,
DISCOVERY_DESCRIPTION_MIN_LENGTH,
} from '@fluxer/constants/src/DiscoveryConstants';
import {SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const DiscoveryApplicationRequest = z.object({
description: z
.string()
.min(DISCOVERY_DESCRIPTION_MIN_LENGTH)
.max(DISCOVERY_DESCRIPTION_MAX_LENGTH)
.describe('Description for discovery listing'),
category_id: z.number().int().min(0).max(8).describe('Discovery category ID'),
});
export type DiscoveryApplicationRequest = z.infer<typeof DiscoveryApplicationRequest>;
export const DiscoveryApplicationPatchRequest = z.object({
description: z
.string()
.min(DISCOVERY_DESCRIPTION_MIN_LENGTH)
.max(DISCOVERY_DESCRIPTION_MAX_LENGTH)
.optional()
.describe('Updated description for discovery listing'),
category_id: z.number().int().min(0).max(8).optional().describe('Updated discovery category ID'),
});
export type DiscoveryApplicationPatchRequest = z.infer<typeof DiscoveryApplicationPatchRequest>;
export const DiscoverySearchQuery = z.object({
query: z.string().max(100).optional().describe('Search query'),
category: z.coerce.number().int().min(0).max(8).optional().describe('Filter by category'),
sort_by: z.enum(['member_count', 'online_count', 'relevance']).optional().describe('Sort order'),
limit: z.coerce.number().int().min(1).max(48).optional().default(24).describe('Number of results to return'),
offset: z.coerce.number().int().min(0).optional().default(0).describe('Pagination offset'),
});
export type DiscoverySearchQuery = z.infer<typeof DiscoverySearchQuery>;
export const DiscoveryGuildResponse = z.object({
id: SnowflakeStringType.describe('Guild ID'),
name: z.string().describe('Guild name'),
icon: z.string().nullish().describe('Guild icon hash'),
description: z.string().nullish().describe('Discovery description'),
category_id: z.number().describe('Discovery category ID'),
member_count: z.number().describe('Approximate member count'),
online_count: z.number().describe('Approximate online member count'),
features: z.array(z.string()).describe('Guild feature flags'),
verification_level: z.number().describe('Verification level'),
});
export type DiscoveryGuildResponse = z.infer<typeof DiscoveryGuildResponse>;
export const DiscoveryGuildListResponse = z.object({
guilds: z.array(DiscoveryGuildResponse).describe('Discovery guild results'),
total: z.number().describe('Total number of matching guilds'),
});
export type DiscoveryGuildListResponse = z.infer<typeof DiscoveryGuildListResponse>;
export const DiscoveryApplicationResponse = z.object({
guild_id: SnowflakeStringType.describe('Guild ID'),
status: z.string().describe('Application status'),
description: z.string().describe('Discovery description'),
category_id: z.number().describe('Discovery category ID'),
applied_at: z.string().describe('Application timestamp'),
reviewed_at: z.string().nullish().describe('Review timestamp'),
review_reason: z.string().nullish().describe('Review reason'),
});
export type DiscoveryApplicationResponse = z.infer<typeof DiscoveryApplicationResponse>;
export const DiscoveryCategoryResponse = z.object({
id: z.number().describe('Category ID'),
name: z.string().describe('Category display name'),
});
export type DiscoveryCategoryResponse = z.infer<typeof DiscoveryCategoryResponse>;
export const DiscoveryCategoryListResponse = z.array(DiscoveryCategoryResponse);
export type DiscoveryCategoryListResponse = z.infer<typeof DiscoveryCategoryListResponse>;
export const DiscoveryAdminReviewRequest = z.object({
reason: z.string().max(500).optional().describe('Review reason'),
});
export type DiscoveryAdminReviewRequest = z.infer<typeof DiscoveryAdminReviewRequest>;
export const DiscoveryAdminRejectRequest = z.object({
reason: z.string().min(1).max(500).describe('Rejection reason'),
});
export type DiscoveryAdminRejectRequest = z.infer<typeof DiscoveryAdminRejectRequest>;
export const DiscoveryAdminRemoveRequest = z.object({
reason: z.string().min(1).max(500).describe('Removal reason'),
});
export type DiscoveryAdminRemoveRequest = z.infer<typeof DiscoveryAdminRemoveRequest>;
export const DiscoveryAdminListQuery = z.object({
status: z.enum(['pending', 'approved', 'rejected', 'removed']).optional().default('pending'),
limit: z.coerce.number().int().min(1).max(100).optional().default(25),
cursor: z.string().optional(),
});
export type DiscoveryAdminListQuery = z.infer<typeof DiscoveryAdminListQuery>;

View File

@@ -0,0 +1,122 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {type UserPartial, UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const GuildEmojiResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this emoji'),
name: z.string().describe('The name of the emoji'),
animated: z.boolean().describe('Whether this emoji is animated'),
});
export type GuildEmojiResponse = z.infer<typeof GuildEmojiResponse>;
export const GuildEmojiWithUserResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this emoji'),
name: z.string().describe('The name of the emoji'),
animated: z.boolean().describe('Whether this emoji is animated'),
user: z.lazy(() => UserPartialResponse).describe('The user who uploaded this emoji'),
});
export type GuildEmojiWithUserResponse = z.infer<typeof GuildEmojiWithUserResponse>;
export const GuildStickerResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this sticker'),
name: z.string().describe('The name of the sticker'),
description: z.string().describe('The description of the sticker'),
tags: z.array(z.string()).max(100).describe('Autocomplete/suggestion tags for the sticker'),
animated: z.boolean().describe('Whether this sticker is animated'),
});
export type GuildStickerResponse = z.infer<typeof GuildStickerResponse>;
export const GuildStickerWithUserResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this sticker'),
name: z.string().describe('The name of the sticker'),
description: z.string().describe('The description of the sticker'),
tags: z.array(z.string()).max(100).describe('Autocomplete/suggestion tags for the sticker'),
animated: z.boolean().describe('Whether this sticker is animated'),
user: z.lazy(() => UserPartialResponse).describe('The user who uploaded this sticker'),
});
export type GuildStickerWithUserResponse = z.infer<typeof GuildStickerWithUserResponse>;
export const GuildEmojiBulkCreateResponse = z.object({
success: z.array(GuildEmojiResponse).max(500).describe('Successfully created emojis'),
failed: z
.array(
z.object({
name: z.string().describe('The name of the emoji that failed to create'),
error: z.string().describe('The error message explaining why the emoji failed to create'),
}),
)
.max(500)
.describe('Emojis that failed to create'),
});
export type GuildEmojiBulkCreateResponse = z.infer<typeof GuildEmojiBulkCreateResponse>;
export const GuildStickerBulkCreateResponse = z.object({
success: z.array(GuildStickerResponse).max(500).describe('Successfully created stickers'),
failed: z
.array(
z.object({
name: z.string().describe('The name of the sticker that failed to create'),
error: z.string().describe('The error message explaining why the sticker failed to create'),
}),
)
.max(500)
.describe('Stickers that failed to create'),
});
export type GuildStickerBulkCreateResponse = z.infer<typeof GuildStickerBulkCreateResponse>;
export const GuildEmojiWithUserListResponse = z.array(GuildEmojiWithUserResponse);
export type GuildEmojiWithUserListResponse = z.infer<typeof GuildEmojiWithUserListResponse>;
export const GuildStickerWithUserListResponse = z.array(GuildStickerWithUserResponse);
export type GuildStickerWithUserListResponse = z.infer<typeof GuildStickerWithUserListResponse>;
export interface GuildEmoji {
readonly id: string;
readonly name: string;
readonly animated: boolean;
readonly user?: UserPartial;
}
export interface GuildEmojiWithUser extends GuildEmoji {
readonly user: UserPartial;
}
export interface GuildSticker {
readonly id: string;
readonly name: string;
readonly description: string;
readonly tags: Array<string>;
readonly animated: boolean;
readonly user?: UserPartial;
}
export interface GuildStickerWithUser extends GuildSticker {
readonly user: UserPartial;
}

View File

@@ -0,0 +1,77 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {GuildMemberProfileFlags, GuildMemberProfileFlagsDescriptions} from '@fluxer/constants/src/GuildConstants';
import {UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {createBitflagInt32Type, Int32Type, SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const GuildMemberResponse = z.object({
user: z.lazy(() => UserPartialResponse).describe('The user this guild member represents'),
nick: z.string().nullish().describe('The nickname of the member in this guild'),
avatar: z.string().nullish().describe('The hash of the member guild-specific avatar'),
banner: z.string().nullish().describe('The hash of the member guild-specific banner'),
accent_color: Int32Type.nullish().describe('The accent colour of the member guild profile as an integer'),
roles: z.array(z.string()).max(250).describe('Array of role IDs the member has'),
joined_at: z.iso.datetime().describe('ISO8601 timestamp of when the user joined the guild'),
mute: z.boolean().describe('Whether the member is muted in voice channels'),
deaf: z.boolean().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'),
profile_flags: createBitflagInt32Type(
GuildMemberProfileFlags,
GuildMemberProfileFlagsDescriptions,
'Member profile flags',
'GuildMemberProfileFlags',
).nullish(),
});
export type GuildMemberResponse = z.infer<typeof GuildMemberResponse>;
export const GuildBanResponse = z.object({
user: z.lazy(() => UserPartialResponse).describe('The banned user'),
reason: z.string().nullish().describe('The reason for the ban'),
moderator_id: SnowflakeStringType.describe('The ID of the moderator who issued the ban'),
banned_at: z.iso.datetime().describe('ISO8601 timestamp of when the ban was issued'),
expires_at: z.iso.datetime().nullish().describe('ISO8601 timestamp of when the ban expires (null if permanent)'),
});
export type GuildBanResponse = z.infer<typeof GuildBanResponse>;
export const GuildMemberListResponse = z.array(GuildMemberResponse).max(1000).describe('A list of guild members');
export type GuildMemberListResponse = z.infer<typeof GuildMemberListResponse>;
export const GuildBanListResponse = z.array(GuildBanResponse).max(1000).describe('A list of guild bans');
export type GuildBanListResponse = z.infer<typeof GuildBanListResponse>;
export interface GuildMemberData {
readonly user: UserPartialResponse;
readonly nick?: string | null;
readonly avatar?: string | null;
readonly banner?: string | null;
readonly accent_color?: number | null;
readonly roles: ReadonlyArray<string>;
readonly joined_at: string;
readonly mute?: boolean;
readonly deaf?: boolean;
readonly communication_disabled_until?: string | null;
readonly profile_flags?: number | null;
}

View File

@@ -0,0 +1,87 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {JoinSourceTypeSchema} from '@fluxer/schema/src/primitives/GuildValidators';
import {z} from 'zod';
export const GuildMemberSearchRequest = z.object({
query: z.string().max(100).optional().describe('Text to search for in usernames, global names, and nicknames'),
limit: z.number().int().min(1).max(100).default(25).describe('Maximum number of results to return'),
offset: z.number().int().min(0).default(0).describe('Number of results to skip for pagination'),
role_ids: z
.array(z.string())
.max(10)
.optional()
.describe('Filter by role IDs (member must have all specified roles)'),
joined_at_gte: z.number().int().optional().describe('Filter members who joined at or after this unix timestamp'),
joined_at_lte: z.number().int().optional().describe('Filter members who joined at or before this unix timestamp'),
join_source_type: z.array(z.number().int()).max(10).optional().describe('Filter by join source types'),
source_invite_code: z.array(z.string()).max(10).optional().describe('Filter by invite codes used to join'),
is_bot: z.boolean().optional().describe('Filter by bot status'),
user_created_at_gte: z
.number()
.int()
.optional()
.describe('Filter members whose account was created at or after this unix timestamp'),
user_created_at_lte: z
.number()
.int()
.optional()
.describe('Filter members whose account was created at or before this unix timestamp'),
sort_by: z.enum(['joinedAt', 'relevance']).optional().describe('Sort results by field'),
sort_order: z.enum(['asc', 'desc']).optional().describe('Sort order'),
});
export type GuildMemberSearchRequest = z.infer<typeof GuildMemberSearchRequest>;
export const GuildMemberSearchSupplemental = z.object({
join_source_type: JoinSourceTypeSchema.nullish().describe('How the member joined'),
source_invite_code: z.string().nullable().describe('Invite code used to join'),
inviter_id: z.string().nullable().describe('User ID of the member who sent the invite'),
});
export type GuildMemberSearchSupplemental = z.infer<typeof GuildMemberSearchSupplemental>;
export const GuildMemberSearchResult = z.object({
id: z.string().describe('Composite ID (guildId:userId)'),
guild_id: z.string().describe('Guild ID'),
user_id: z.string().describe('User ID'),
username: z.string().describe('Username'),
discriminator: z.string().describe('Zero-padded 4-digit discriminator'),
global_name: z.string().nullable().describe('Global display name'),
nickname: z.string().nullable().describe('Guild nickname'),
role_ids: z.array(z.string()).describe('Role IDs'),
joined_at: z.number().describe('Unix timestamp of when the member joined'),
supplemental: GuildMemberSearchSupplemental.describe(
'Supplemental members-search-only metadata that is not part of the base guild member payload',
),
is_bot: z.boolean().describe('Whether the user is a bot'),
});
export type GuildMemberSearchResult = z.infer<typeof GuildMemberSearchResult>;
export const GuildMemberSearchResponse = z.object({
guild_id: z.string().describe('Guild ID'),
members: z.array(GuildMemberSearchResult).describe('Matching members'),
page_result_count: z.number().int().describe('Number of results in this page'),
total_result_count: z.number().int().describe('Total number of matching results'),
indexing: z.boolean().describe('Whether the guild members are currently being indexed'),
});
export type GuildMemberSearchResponse = z.infer<typeof GuildMemberSearchResponse>;

View File

@@ -0,0 +1,335 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<typeof GuildCreateRequest>;
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<typeof GuildUpdateRequest>;
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<typeof GuildMemberUpdateRequest>;
export const MyGuildMemberUpdateRequest = GuildMemberUpdateRequest.omit({roles: true}).partial();
export type MyGuildMemberUpdateRequest = z.infer<typeof MyGuildMemberUpdateRequest>;
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<typeof GuildRoleCreateRequest>;
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<typeof GuildRoleUpdateRequest>;
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<typeof GuildEmojiCreateRequest>;
export const GuildEmojiUpdateRequest = GuildEmojiCreateRequest.pick({name: true});
export type GuildEmojiUpdateRequest = z.infer<typeof GuildEmojiUpdateRequest>;
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<typeof GuildEmojiBulkCreateRequest>;
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<typeof GuildStickerCreateRequest>;
export const GuildStickerUpdateRequest = GuildStickerCreateRequest.pick({
name: true,
description: true,
tags: true,
});
export type GuildStickerUpdateRequest = z.infer<typeof GuildStickerUpdateRequest>;
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<typeof GuildStickerBulkCreateRequest>;
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<typeof GuildTransferOwnershipRequest>;
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<typeof GuildBanCreateRequest>;
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<typeof GuildListQuery>;
export const GuildDeleteRequest = z
.object({
password: PasswordType.optional().describe('The owner password for verification'),
})
.merge(SudoVerificationSchema);
export type GuildDeleteRequest = z.infer<typeof GuildDeleteRequest>;
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<typeof GuildVanityURLUpdateRequest>;
export const GuildVanityURLUpdateResponse = z.object({
code: createStringType(2, 32).describe('The new vanity URL code'),
});
export type GuildVanityURLUpdateResponse = z.infer<typeof GuildVanityURLUpdateResponse>;
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<typeof GuildRoleHoistPositionItem>;
export const GuildRoleHoistPositionsRequest = z.array(GuildRoleHoistPositionItem);
export type GuildRoleHoistPositionsRequest = z.infer<typeof GuildRoleHoistPositionsRequest>;
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<typeof GuildRolePositionItem>;
export const GuildRolePositionsRequest = z.array(GuildRolePositionItem);
export type GuildRolePositionsRequest = z.infer<typeof GuildRolePositionsRequest>;
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<typeof GuildMemberListQuery>;

View File

@@ -0,0 +1,218 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import type {GuildSplashCardAlignmentValue} from '@fluxer/constants/src/GuildConstants';
import {
GuildFeatures,
GuildOperations,
GuildOperationsDescriptions,
SystemChannelFlags,
SystemChannelFlagsDescriptions,
} from '@fluxer/constants/src/GuildConstants';
import {
DefaultMessageNotificationsSchema,
GuildExplicitContentFilterSchema,
GuildMFALevelSchema,
GuildVerificationLevelSchema,
NSFWLevelSchema,
SplashCardAlignmentSchema,
} from '@fluxer/schema/src/primitives/GuildValidators';
import {PermissionStringType} from '@fluxer/schema/src/primitives/PermissionValidators';
import {
createBitflagInt32Type,
createFlexibleStringLiteralUnion,
Int32Type,
SnowflakeStringType,
withFieldDescription,
withOpenApiType,
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
function normalizeGuildFeatures(features: Array<string>): Array<string> {
return Array.from(new Set(features)).sort((first, second) => first.localeCompare(second));
}
export const GuildFeatureSchema = withOpenApiType(
createFlexibleStringLiteralUnion(
[
[GuildFeatures.ANIMATED_ICON, 'ANIMATED_ICON', 'Guild can have an animated icon'],
[GuildFeatures.ANIMATED_BANNER, 'ANIMATED_BANNER', 'Guild can have an animated banner'],
[GuildFeatures.BANNER, 'BANNER', 'Guild can have a banner'],
[GuildFeatures.DETACHED_BANNER, 'DETACHED_BANNER', 'Guild banner is detached from splash'],
[GuildFeatures.INVITE_SPLASH, 'INVITE_SPLASH', 'Guild can have an invite splash'],
[GuildFeatures.INVITES_DISABLED, 'INVITES_DISABLED', 'Guild has invites disabled'],
[
GuildFeatures.TEXT_CHANNEL_FLEXIBLE_NAMES,
'TEXT_CHANNEL_FLEXIBLE_NAMES',
'Guild allows flexible text channel names',
],
[GuildFeatures.MORE_EMOJI, 'MORE_EMOJI', 'Guild has increased emoji slots'],
[GuildFeatures.MORE_STICKERS, 'MORE_STICKERS', 'Guild has increased sticker slots'],
[GuildFeatures.UNLIMITED_EMOJI, 'UNLIMITED_EMOJI', 'Guild has unlimited emoji slots'],
[GuildFeatures.UNLIMITED_STICKERS, 'UNLIMITED_STICKERS', 'Guild has unlimited sticker slots'],
[GuildFeatures.EXPRESSION_PURGE_ALLOWED, 'EXPRESSION_PURGE_ALLOWED', 'Guild allows purging expressions'],
[GuildFeatures.VANITY_URL, 'VANITY_URL', 'Guild can have a vanity URL'],
[GuildFeatures.VERIFIED, 'VERIFIED', 'Guild is verified'],
[GuildFeatures.VIP_VOICE, 'VIP_VOICE', 'Guild has VIP voice features'],
[GuildFeatures.UNAVAILABLE_FOR_EVERYONE, 'UNAVAILABLE_FOR_EVERYONE', 'Guild is unavailable for everyone'],
[
GuildFeatures.UNAVAILABLE_FOR_EVERYONE_BUT_STAFF,
'UNAVAILABLE_FOR_EVERYONE_BUT_STAFF',
'Guild is unavailable except for staff',
],
[GuildFeatures.VISIONARY, 'VISIONARY', 'Guild is a visionary guild'],
[GuildFeatures.OPERATOR, 'OPERATOR', 'Guild is an operator guild'],
[GuildFeatures.LARGE_GUILD_OVERRIDE, 'LARGE_GUILD_OVERRIDE', 'Guild has large guild overrides enabled'],
[GuildFeatures.VERY_LARGE_GUILD, 'VERY_LARGE_GUILD', 'Guild has increased member capacity enabled'],
[GuildFeatures.MANAGED_MESSAGE_SCHEDULING, 'MT_MESSAGE_SCHEDULING', 'Guild has managed message scheduling'],
[GuildFeatures.MANAGED_EXPRESSION_PACKS, 'MT_EXPRESSION_PACKS', 'Guild has managed expression packs'],
],
'A guild feature flag',
),
'GuildFeature',
);
const GuildFeatureListSchema = z
.array(GuildFeatureSchema)
.max(100)
.transform(normalizeGuildFeatures)
.describe('Array of guild feature flags');
export const GuildResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this guild'),
name: z.string().describe('The name of the guild'),
icon: z.string().nullish().describe('The hash of the guild icon'),
banner: z.string().nullish().describe('The hash of the guild banner'),
banner_width: Int32Type.nullish().describe('The width of the guild banner in pixels'),
banner_height: Int32Type.nullish().describe('The height of the guild banner in pixels'),
splash: z.string().nullish().describe('The hash of the guild splash screen'),
splash_width: Int32Type.nullish().describe('The width of the guild splash in pixels'),
splash_height: Int32Type.nullish().describe('The height of the guild splash in pixels'),
splash_card_alignment: SplashCardAlignmentSchema.describe('The alignment of the splash card'),
embed_splash: z.string().nullish().describe('The hash of the embedded invite splash'),
embed_splash_width: Int32Type.nullish().describe('The width of the embedded invite splash in pixels'),
embed_splash_height: Int32Type.nullish().describe('The height of the embedded invite splash in pixels'),
vanity_url_code: z.string().nullish().describe('The vanity URL code for the guild'),
owner_id: SnowflakeStringType.describe('The ID of the guild owner'),
system_channel_id: SnowflakeStringType.nullish().describe('The ID of the channel where system messages are sent'),
system_channel_flags: createBitflagInt32Type(
SystemChannelFlags,
SystemChannelFlagsDescriptions,
'System channel message flags',
'SystemChannelFlags',
),
rules_channel_id: SnowflakeStringType.nullish().describe('The ID of the rules channel'),
afk_channel_id: SnowflakeStringType.nullish().describe('The ID of the AFK voice channel'),
afk_timeout: Int32Type.describe('AFK timeout in seconds before moving users to the AFK channel'),
features: GuildFeatureListSchema,
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',
),
default_message_notifications: withFieldDescription(
DefaultMessageNotificationsSchema,
'Default notification level for new members',
),
disabled_operations: createBitflagInt32Type(
GuildOperations,
GuildOperationsDescriptions,
'Bitfield of disabled operations in the guild',
'GuildOperations',
),
message_history_cutoff: z.iso
.datetime()
.nullish()
.describe(
'ISO8601 timestamp controlling how far back members without Read Message History can access messages. When null, no historical access is allowed.',
),
permissions: PermissionStringType.describe(
'fluxer:PermissionStringType The current user permissions in this guild',
).nullish(),
});
export type GuildResponse = z.infer<typeof GuildResponse>;
export const GuildPartialResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this guild'),
name: z.string().describe('The name of the guild'),
icon: z.string().nullish().describe('The hash of the guild icon'),
banner: z.string().nullish().describe('The hash of the guild banner'),
banner_width: Int32Type.nullish().describe('The width of the guild banner in pixels'),
banner_height: Int32Type.nullish().describe('The height of the guild banner in pixels'),
splash: z.string().nullish().describe('The hash of the guild splash screen'),
splash_width: Int32Type.nullish().describe('The width of the guild splash in pixels'),
splash_height: Int32Type.nullish().describe('The height of the guild splash in pixels'),
splash_card_alignment: SplashCardAlignmentSchema.describe('The alignment of the splash card'),
embed_splash: z.string().nullish().describe('The hash of the embedded invite splash'),
embed_splash_width: Int32Type.nullish().describe('The width of the embedded invite splash in pixels'),
embed_splash_height: Int32Type.nullish().describe('The height of the embedded invite splash in pixels'),
features: GuildFeatureListSchema,
});
export type GuildPartialResponse = z.infer<typeof GuildPartialResponse>;
export const GuildVanityURLResponse = z.object({
code: z.string().nullish().describe('The vanity URL code for the guild'),
uses: Int32Type.describe('The number of times this vanity URL has been used'),
});
export type GuildVanityURLResponse = z.infer<typeof GuildVanityURLResponse>;
export const GuildListResponse = z.array(GuildResponse).max(200).describe('A list of guilds');
export type GuildListResponse = z.infer<typeof GuildListResponse>;
export interface Guild {
readonly id: string;
readonly name: string;
readonly icon: string | null;
readonly banner?: string | null;
readonly banner_width?: number | null;
readonly banner_height?: number | null;
readonly splash?: string | null;
readonly splash_width?: number | null;
readonly splash_height?: number | null;
readonly splash_card_alignment?: GuildSplashCardAlignmentValue;
readonly embed_splash?: string | null;
readonly embed_splash_width?: number | null;
readonly embed_splash_height?: number | null;
readonly vanity_url_code: string | null;
readonly owner_id: string;
readonly system_channel_id: string | null;
readonly system_channel_flags?: number;
readonly rules_channel_id?: string | null;
readonly afk_channel_id?: string | null;
readonly afk_timeout?: number;
readonly features: ReadonlyArray<string>;
readonly verification_level?: number;
readonly mfa_level?: number;
readonly nsfw_level?: number;
readonly explicit_content_filter?: number;
readonly default_message_notifications?: number;
readonly disabled_operations?: number;
readonly message_history_cutoff?: string | null;
readonly joined_at?: string;
readonly unavailable?: boolean;
readonly member_count?: number;
}

View File

@@ -0,0 +1,51 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {PermissionStringType} from '@fluxer/schema/src/primitives/PermissionValidators';
import {Int32Type, SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const GuildRoleResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this role'),
name: z.string().describe('The name of the role'),
color: Int32Type.describe('The colour of the role as an integer'),
position: Int32Type.describe('The position of the role in the role hierarchy'),
hoist_position: Int32Type.nullish().describe('The position of the role in the hoisted member list'),
permissions: PermissionStringType.describe('fluxer:PermissionStringType The permissions bitfield for the role'),
hoist: z.boolean().describe('Whether this role is displayed separately in the member list'),
mentionable: z.boolean().describe('Whether this role can be mentioned by anyone'),
unicode_emoji: z.string().nullish().describe('The unicode emoji for this role'),
});
export type GuildRoleResponse = z.infer<typeof GuildRoleResponse>;
export const GuildRoleListResponse = z.array(GuildRoleResponse).max(250).describe('A list of guild roles');
export type GuildRoleListResponse = z.infer<typeof GuildRoleListResponse>;
export interface GuildRole {
readonly id: string;
readonly name: string;
readonly color: number;
readonly position: number;
readonly hoist_position?: number | null;
readonly permissions: string;
readonly hoist: boolean;
readonly mentionable: boolean;
readonly unicode_emoji?: string | null;
}