refactor progress

This commit is contained in:
Hampus Kraft committed 2026-02-17 12:22:36 +00:00
1 parent cb31608523
commit d5abd1a7e4
8257 files changed
+1190205 -761038

No files matched your search

@@ -0,0 +1,60 @@
/*
* 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 {MessageAttachmentFlags, MessageAttachmentFlagsDescriptions} from '@fluxer/constants/src/ChannelConstants';
import {FilenameType} from '@fluxer/schema/src/primitives/FileValidators';
import {
coerceNumberFromString,
createBitflagInt32Type,
createStringType,
Int32Type,
SnowflakeType,
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
const ClientAttachmentBase = z.object({
title: createStringType(1, 1024).nullish().describe('A title for the attachment (1-1024 characters)'),
description: createStringType(1, 4096)
.nullish()
.describe('An alt text description of the attachment (1-4096 characters)'),
flags: coerceNumberFromString(
createBitflagInt32Type(
MessageAttachmentFlags,
MessageAttachmentFlagsDescriptions,
'Attachment flags',
'MessageAttachmentFlags',
),
).default(0),
duration: Int32Type.nullish().describe('The duration of the audio file in seconds'),
waveform: createStringType(1, 4096).nullish().describe('Base64 encoded audio waveform data'),
});
export const ClientAttachmentRequest = ClientAttachmentBase.extend({
id: coerceNumberFromString(Int32Type).describe('The client-side identifier for this attachment'),
filename: FilenameType.describe('The name of the file being uploaded'),
});
export type ClientAttachmentRequest = z.infer<typeof ClientAttachmentRequest>;
export const ClientAttachmentReferenceRequest = ClientAttachmentBase.extend({
id: z
.union([Int32Type, SnowflakeType])
.describe('The identifier of the attachment being referenced (snowflake ID or file index)'),
filename: FilenameType.optional().describe('A new filename for the attachment'),
});
export type ClientAttachmentReferenceRequest = z.infer<typeof ClientAttachmentReferenceRequest>;
@@ -0,0 +1,193 @@
/*
* 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 {EmbedMediaFlags, EmbedMediaFlagsDescriptions} from '@fluxer/constants/src/ChannelConstants';
import {createBitflagInt32Type, Int32Type} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const EmbedAuthorResponse = z.object({
name: z.string().describe('The name of the author'),
url: z.url().nullish().describe('The URL of the author'),
icon_url: z.url().nullish().describe('The URL of the author icon'),
proxy_icon_url: z.url().nullish().describe('The proxied URL of the author icon'),
});
export type EmbedAuthorResponse = z.infer<typeof EmbedAuthorResponse>;
export const EmbedFooterResponse = z.object({
text: z.string().describe('The footer text'),
icon_url: z.url().nullish().describe('The URL of the footer icon'),
proxy_icon_url: z.url().nullish().describe('The proxied URL of the footer icon'),
});
export type EmbedFooterResponse = z.infer<typeof EmbedFooterResponse>;
export const EmbedMediaResponse = z.object({
url: z.string().describe('The URL of the media'),
proxy_url: z.url().nullish().describe('The proxied URL of the media'),
content_type: z.string().nullish().describe('The MIME type of the media'),
content_hash: z.string().nullish().describe('The hash of the media content'),
width: Int32Type.nullish().describe('The width of the media in pixels'),
height: Int32Type.nullish().describe('The height of the media in pixels'),
description: z.string().nullish().describe('The description of the media'),
placeholder: z.string().nullish().describe('The base64 encoded placeholder image for lazy loading'),
duration: Int32Type.nullish().describe('The duration of the media in seconds'),
flags: createBitflagInt32Type(
EmbedMediaFlags,
EmbedMediaFlagsDescriptions,
'The bitwise flags for this media',
'EmbedMediaFlags',
),
});
export type EmbedMediaResponse = z.infer<typeof EmbedMediaResponse>;
export const EmbedFieldResponse = z.object({
name: z.string().describe('The name of the field'),
value: z.string().describe('The value of the field'),
inline: z.boolean().describe('Whether the field should be displayed inline'),
});
export type EmbedFieldResponse = z.infer<typeof EmbedFieldResponse>;
interface MessageEmbedChildResponseData {
type: string;
url?: string | null;
title?: string | null;
color?: number | null;
timestamp?: string | null;
description?: string | null;
author?: EmbedAuthorResponse | null;
image?: EmbedMediaResponse | null;
thumbnail?: EmbedMediaResponse | null;
footer?: EmbedFooterResponse | null;
fields?: Array<EmbedFieldResponse> | null;
provider?: EmbedAuthorResponse | null;
video?: EmbedMediaResponse | null;
audio?: EmbedMediaResponse | null;
nsfw?: boolean | null;
}
interface MessageEmbedResponseData extends MessageEmbedChildResponseData {
children?: Array<MessageEmbedChildResponseData> | null;
}
export const MessageEmbedChildResponse = z.object({
type: z.string().describe('The type of embed (e.g., rich, image, video, gifv, article, link)'),
url: z.url().nullish().describe('The URL of the embed'),
title: z.string().nullish().describe('The title of the embed'),
color: Int32Type.nullish().describe('The color code of the embed sidebar'),
timestamp: z.iso.datetime().nullish().describe('The ISO 8601 timestamp of the embed content'),
description: z.string().nullish().describe('The description of the embed'),
author: EmbedAuthorResponse.nullish().describe('The author information of the embed'),
image: EmbedMediaResponse.nullish().describe('The image of the embed'),
thumbnail: EmbedMediaResponse.nullish().describe('The thumbnail of the embed'),
footer: EmbedFooterResponse.nullish().describe('The footer of the embed'),
fields: z.array(EmbedFieldResponse).max(25).nullish().describe('The fields of the embed'),
provider: EmbedAuthorResponse.nullish().describe('The provider of the embed (e.g., YouTube, Twitter)'),
video: EmbedMediaResponse.nullish().describe('The video of the embed'),
audio: EmbedMediaResponse.nullish().describe('The audio of the embed'),
nsfw: z.boolean().nullish().describe('Whether the embed is flagged as NSFW'),
});
export type MessageEmbedChildResponse = z.infer<typeof MessageEmbedChildResponse>;
export const MessageEmbedResponse: z.ZodType<MessageEmbedResponseData> = MessageEmbedChildResponse.extend({
children: z
.array(MessageEmbedChildResponse)
.max(1)
.nullish()
.describe('Internal nested embeds generated by unfurlers'),
});
export type MessageEmbedResponse = z.infer<typeof MessageEmbedResponse>;
export interface EmbedAuthor {
readonly name: string;
readonly url?: string;
readonly icon_url?: string;
readonly proxy_icon_url?: string;
}
export interface EmbedFooter {
readonly text: string;
readonly icon_url?: string;
readonly proxy_icon_url?: string;
}
export interface EmbedMedia {
readonly url: string;
readonly proxy_url?: string;
readonly content_type?: string;
readonly content_hash?: string | null;
readonly width?: number;
readonly height?: number;
readonly placeholder?: string;
readonly flags: number;
readonly description?: string;
readonly duration?: number;
readonly nsfw?: boolean;
}
export interface EmbedField {
readonly name: string;
readonly value: string;
readonly inline: boolean;
}
export interface MessageEmbed {
readonly id?: string;
readonly type: string;
readonly url?: string;
readonly title?: string;
readonly color?: number;
readonly timestamp?: string;
readonly description?: string;
readonly author?: EmbedAuthor;
readonly image?: EmbedMedia;
readonly thumbnail?: EmbedMedia;
readonly footer?: EmbedFooter;
readonly fields?: ReadonlyArray<EmbedField>;
readonly provider?: EmbedAuthor;
readonly video?: EmbedMedia;
readonly audio?: EmbedMedia;
readonly children?: ReadonlyArray<MessageEmbedChild>;
readonly flags?: number;
readonly nsfw?: boolean;
}
export interface MessageEmbedChild {
readonly id?: string;
readonly type: string;
readonly url?: string;
readonly title?: string;
readonly color?: number;
readonly timestamp?: string;
readonly description?: string;
readonly author?: EmbedAuthor;
readonly image?: EmbedMedia;
readonly thumbnail?: EmbedMedia;
readonly footer?: EmbedFooter;
readonly fields?: ReadonlyArray<EmbedField>;
readonly provider?: EmbedAuthor;
readonly video?: EmbedMedia;
readonly audio?: EmbedMedia;
readonly flags?: number;
readonly nsfw?: boolean;
}
@@ -0,0 +1,330 @@
/*
* 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 {MessageFlags, MessageFlagsDescriptions} from '@fluxer/constants/src/ChannelConstants';
import {
ClientAttachmentReferenceRequest,
ClientAttachmentRequest,
} from '@fluxer/schema/src/domains/message/AttachmentSchemas';
import {AllowedMentionsRequest, MessageReferenceRequest} from '@fluxer/schema/src/domains/message/SharedMessageSchemas';
import {createQueryIntegerType, DateTimeType} from '@fluxer/schema/src/primitives/QueryValidators';
import {
ColorType,
createBitflagInt32Type,
createNamedStringLiteralUnion,
createStringType,
createUnboundedStringType,
Int32Type,
SnowflakeType,
withOpenApiType,
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {AttachmentURLType, URLType} from '@fluxer/schema/src/primitives/UrlValidators';
import {z} from 'zod';
export const RichEmbedAuthorRequest = z.object({
name: createStringType().describe('Name of the embed author'),
url: URLType.nullish().describe('URL to link from the author name'),
icon_url: URLType.nullish().describe('URL of the author icon'),
});
export type RichEmbedAuthorRequest = z.infer<typeof RichEmbedAuthorRequest>;
export const RichEmbedMediaRequest = z.object({
url: AttachmentURLType.describe('URL of the media (image, video, etc.)'),
description: createStringType(1, 4096).nullish().describe('Alt text description of the media'),
});
export type RichEmbedMediaRequest = z.infer<typeof RichEmbedMediaRequest>;
export const RichEmbedFooterRequest = z.object({
text: createStringType(1, 2048).describe('Footer text (1-2048 characters)'),
icon_url: URLType.nullish().describe('URL of the footer icon'),
});
export type RichEmbedFooterRequest = z.infer<typeof RichEmbedFooterRequest>;
export const RichEmbedFieldRequest = z.object({
name: createStringType().describe('Name of the field'),
value: createStringType(1, 1024).describe('Value of the field (1-1024 characters)'),
inline: z.boolean().default(false).describe('Whether the field should display inline'),
});
export type RichEmbedFieldRequest = z.infer<typeof RichEmbedFieldRequest>;
export const RichEmbedRequest = z.object({
url: URLType.nullish().describe('URL of the embed'),
title: createStringType().nullish().describe('Title of the embed'),
color: ColorType.nullish().describe('Color code of the embed (hex integer)'),
timestamp: DateTimeType.nullish().describe('ISO8601 timestamp for the embed'),
description: createStringType(1, 4096).nullish().describe('Description of the embed (1-4096 characters)'),
author: RichEmbedAuthorRequest.nullish().describe('Author information'),
image: RichEmbedMediaRequest.nullish().describe('Image to display in the embed'),
thumbnail: RichEmbedMediaRequest.nullish().describe('Thumbnail image for the embed'),
footer: RichEmbedFooterRequest.nullish().describe('Footer information'),
fields: z.array(RichEmbedFieldRequest).max(25).nullish().describe('Array of field objects (max 25)'),
});
export type RichEmbedRequest = z.infer<typeof RichEmbedRequest>;
export const MessageAuthorType = withOpenApiType(
createNamedStringLiteralUnion(
[
['user', 'user', 'A regular user account'],
['bot', 'bot', 'An automated bot account'],
['webhook', 'webhook', 'A webhook-generated message'],
],
'The type of author who sent the message',
),
'MessageAuthorType',
);
export type MessageAuthorType = z.infer<typeof MessageAuthorType>;
export const MessageContentType = withOpenApiType(
createNamedStringLiteralUnion(
[
['image', 'image', 'Message contains an image attachment'],
['sound', 'sound', 'Message contains an audio attachment'],
['video', 'video', 'Message contains a video attachment'],
['file', 'file', 'Message contains a file attachment'],
['sticker', 'sticker', 'Message contains a sticker'],
['embed', 'embed', 'Message contains an embed'],
['link', 'link', 'Message contains a URL link'],
['poll', 'poll', 'Message contains a poll'],
['snapshot', 'snapshot', 'Message contains a forwarded message snapshot'],
],
'The type of content contained in a message',
),
'MessageContentType',
);
export type MessageContentType = z.infer<typeof MessageContentType>;
export const MessageEmbedType = withOpenApiType(
createNamedStringLiteralUnion(
[
['image', 'image', 'An image embed from a linked URL'],
['video', 'video', 'A video embed from a linked URL'],
['sound', 'sound', 'An audio embed from a linked URL'],
['article', 'article', 'An article or webpage embed with metadata'],
],
'The type of embed content',
),
'MessageEmbedType',
);
export type MessageEmbedType = z.infer<typeof MessageEmbedType>;
export const MessageSortField = withOpenApiType(
createNamedStringLiteralUnion(
[
['timestamp', 'timestamp', 'Sort results by message timestamp'],
['relevance', 'relevance', 'Sort results by search relevance score'],
],
'The field to sort search results by',
),
'MessageSortField',
);
export type MessageSortField = z.infer<typeof MessageSortField>;
export const MessageSortOrder = withOpenApiType(
createNamedStringLiteralUnion(
[
['asc', 'asc', 'Sort in ascending order (oldest/lowest first)'],
['desc', 'desc', 'Sort in descending order (newest/highest first)'],
],
'The order to sort search results',
),
'MessageSortOrder',
);
export type MessageSortOrder = z.infer<typeof MessageSortOrder>;
export const MessageSearchScope = withOpenApiType(
createNamedStringLiteralUnion(
[
['current', 'current', 'Search only in the current channel or community context'],
['open_dms', 'open_dms', 'Search across all DMs you currently have open'],
['all_dms', 'all_dms', "Search across all DMs you've ever been in"],
['all_guilds', 'all_guilds', "Search across all Communities you're currently in"],
['all', 'all', "Search across all DMs you've ever been in and all Communities you're currently in"],
[
'open_dms_and_all_guilds',
'open_dms_and_all_guilds',
"Search across all DMs you currently have open and all Communities you're currently in",
],
],
'Search scope for message searches',
),
'MessageSearchScope',
);
export type MessageSearchScope = z.infer<typeof MessageSearchScope>;
export const MessageSearchRequest = z.object({
hits_per_page: z.number().int().min(1).max(25).default(25).describe('Number of results per page (1-25)'),
page: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(1).describe('Page number for pagination'),
max_id: SnowflakeType.optional().describe('Maximum message ID to include in results'),
min_id: SnowflakeType.optional().describe('Minimum message ID to include in results'),
content: createStringType(1, 1024).optional().describe('Text content to search for'),
contents: z.array(createStringType(1, 1024)).max(100).optional().describe('Multiple content queries to search for'),
exact_phrases: z
.array(createStringType(1, 1024))
.max(10)
.optional()
.describe('Exact phrases that must appear contiguously in message content'),
channel_id: z.array(SnowflakeType).max(500).optional().describe('Channel IDs to search in'),
exclude_channel_id: z.array(SnowflakeType).max(500).optional().describe('Channel IDs to exclude from search'),
author_type: z.array(MessageAuthorType).optional().describe('Author types to filter by'),
exclude_author_type: z.array(MessageAuthorType).optional().describe('Author types to exclude'),
author_id: z.array(SnowflakeType).optional().describe('Author user IDs to filter by'),
exclude_author_id: z.array(SnowflakeType).optional().describe('Author user IDs to exclude'),
mentions: z.array(SnowflakeType).optional().describe('User IDs that must be mentioned'),
exclude_mentions: z.array(SnowflakeType).optional().describe('User IDs that must not be mentioned'),
mention_everyone: z.boolean().optional().describe('Filter by whether message mentions everyone'),
pinned: z.boolean().optional().describe('Filter by pinned status'),
has: z.array(MessageContentType).optional().describe('Content types the message must have'),
exclude_has: z.array(MessageContentType).optional().describe('Content types the message must not have'),
embed_type: z.array(MessageEmbedType).optional().describe('Embed types to filter by'),
exclude_embed_type: z.array(MessageEmbedType).optional().describe('Embed types to exclude'),
embed_provider: z.array(createStringType(1, 256)).optional().describe('Embed providers to filter by'),
exclude_embed_provider: z.array(createStringType(1, 256)).optional().describe('Embed providers to exclude'),
link_hostname: z.array(createStringType(1, 255)).optional().describe('Link hostnames to filter by'),
exclude_link_hostname: z.array(createStringType(1, 255)).optional().describe('Link hostnames to exclude'),
attachment_filename: z.array(createStringType(1, 1024)).optional().describe('Attachment filenames to filter by'),
exclude_attachment_filename: z
.array(createStringType(1, 1024))
.optional()
.describe('Attachment filenames to exclude'),
attachment_extension: z.array(createStringType(1, 32)).optional().describe('File extensions to filter by'),
exclude_attachment_extension: z.array(createStringType(1, 32)).optional().describe('File extensions to exclude'),
sort_by: MessageSortField.default('timestamp').describe('Field to sort results by'),
sort_order: MessageSortOrder.default('desc').describe('Sort order for results'),
include_nsfw: z.boolean().default(false).describe('Whether to include NSFW channel results'),
scope: MessageSearchScope.optional().describe('Scope to search within when querying messages'),
});
export type MessageSearchRequest = z.infer<typeof MessageSearchRequest>;
export const GlobalSearchMessagesRequest = MessageSearchRequest.extend({
context_channel_id: SnowflakeType.optional().describe(
'Channel ID for context when searching across multiple channels',
),
context_guild_id: SnowflakeType.optional().describe('Guild ID for context when searching across multiple guilds'),
channel_ids: z.array(SnowflakeType).max(500).optional().describe('Specific channel IDs to search in'),
});
export type GlobalSearchMessagesRequest = z.infer<typeof GlobalSearchMessagesRequest>;
export const MessageRequestSchema = z
.object({
content: createUnboundedStringType().nullish().describe('The message content (up to 2000 characters)'),
embeds: z.array(RichEmbedRequest).describe('Array of embed objects to include in the message'),
attachments: z.array(ClientAttachmentRequest).describe('Array of attachment objects'),
message_reference: MessageReferenceRequest.nullish().describe(
'Reference to another message (for replies or forwards)',
),
allowed_mentions: AllowedMentionsRequest.nullish().describe('Controls which mentions trigger notifications'),
flags: createBitflagInt32Type(
MessageFlags,
MessageFlagsDescriptions,
'Message flags bitfield',
'MessageFlags',
).default(0),
nonce: createStringType(1, 32).describe('Client-generated identifier for the message'),
favorite_meme_id: SnowflakeType.nullish().describe('ID of a favorite meme to attach'),
sticker_ids: z.array(SnowflakeType).max(3).nullish().describe('Array of sticker IDs to include (max 3)'),
tts: z.boolean().optional().describe('Whether this is a text-to-speech message'),
})
.partial();
export type MessageRequestSchemaType = z.infer<typeof MessageRequestSchema>;
export const MessageUpdateRequestSchema = MessageRequestSchema.pick({
content: true,
embeds: true,
allowed_mentions: true,
}).extend({
flags: createBitflagInt32Type(
MessageFlags,
MessageFlagsDescriptions,
'Message flags bitfield',
'MessageFlags',
).optional(),
attachments: z
.array(ClientAttachmentReferenceRequest)
.optional()
.describe('Array of attachment objects to keep or add'),
});
export type MessageUpdateRequestSchemaType = z.infer<typeof MessageUpdateRequestSchema>;
export const MessagesQuery = z.object({
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 100}).describe(
'Number of messages to return (1-100, default 50)',
),
before: SnowflakeType.optional().describe('Get messages before this message ID'),
after: SnowflakeType.optional().describe('Get messages after this message ID'),
around: SnowflakeType.optional().describe('Get messages around this message ID'),
});
export type MessagesQuery = z.infer<typeof MessagesQuery>;
export const BulkDeleteMessagesRequest = z.object({
message_ids: z.array(SnowflakeType).describe('Array of message IDs to delete'),
});
export type BulkDeleteMessagesRequest = z.infer<typeof BulkDeleteMessagesRequest>;
export const MessageAckRequest = z.object({
mention_count: Int32Type.optional().describe('Number of mentions to acknowledge'),
manual: z.boolean().optional().describe('Whether this is a manual acknowledgement'),
});
export type MessageAckRequest = z.infer<typeof MessageAckRequest>;
export const ChannelPinsQuerySchema = z.object({
limit: z.coerce
.number()
.int()
.min(1)
.max(50)
.optional()
.describe('Maximum number of pinned messages to return (1-50)'),
before: z.coerce.date().optional().describe('Get pinned messages before this timestamp'),
});
export type ChannelPinsQuerySchema = z.infer<typeof ChannelPinsQuerySchema>;
export const ReactionUsersQuerySchema = z.object({
limit: z.coerce.number().int().min(1).max(100).optional().describe('Maximum number of users to return (1-100)'),
after: SnowflakeType.optional().describe('Get users after this user ID'),
});
export type ReactionUsersQuerySchema = z.infer<typeof ReactionUsersQuerySchema>;
@@ -0,0 +1,343 @@
/*
* 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 {
MessageAttachmentFlags,
MessageAttachmentFlagsDescriptions,
MessageFlags,
MessageFlagsDescriptions,
} from '@fluxer/constants/src/ChannelConstants';
import {MAX_REACTIONS_PER_MESSAGE} from '@fluxer/constants/src/LimitConstants';
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import {type MessageEmbed, MessageEmbedResponse} from '@fluxer/schema/src/domains/message/EmbedSchemas';
import {type UserPartial, UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {MessageReferenceTypeSchema, MessageTypeSchema} from '@fluxer/schema/src/primitives/MessageValidators';
import {createBitflagInt32Type, Int32Type, SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const MessageAttachmentResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier for this attachment'),
filename: z.string().describe('The name of the attached file'),
title: z.string().nullish().describe('The title of the attachment'),
description: z.string().nullish().describe('The description of the attachment'),
content_type: z.string().nullish().describe('The MIME type of the attachment'),
content_hash: z.string().nullish().describe('The hash of the attachment content'),
size: Int32Type.describe('The size of the attachment in bytes'),
url: z.string().nullish().describe('The URL of the attachment'),
proxy_url: z.string().nullish().describe('The proxied URL of the attachment'),
width: Int32Type.nullish().describe('The width of the attachment in pixels (for images/videos)'),
height: Int32Type.nullish().describe('The height of the attachment in pixels (for images/videos)'),
placeholder: z.string().nullish().describe('The base64 encoded placeholder image for lazy loading'),
flags: createBitflagInt32Type(
MessageAttachmentFlags,
MessageAttachmentFlagsDescriptions,
'The bitwise flags for this attachment',
'MessageAttachmentFlags',
),
nsfw: z.boolean().nullish().describe('Whether the attachment is flagged as NSFW'),
duration: Int32Type.nullish().describe('The duration of the media in seconds'),
waveform: z.string().nullish().describe('The base64 encoded audio waveform data'),
expires_at: z.string().nullish().describe('The ISO 8601 timestamp when the attachment URL expires'),
expired: z.boolean().nullish().describe('Whether the attachment URL has expired'),
});
export type MessageAttachmentResponse = z.infer<typeof MessageAttachmentResponse>;
export const MessageReferenceResponse = z.object({
channel_id: SnowflakeStringType.describe('The ID of the channel containing the referenced message'),
message_id: SnowflakeStringType.describe('The ID of the referenced message'),
guild_id: SnowflakeStringType.nullish().describe('The ID of the guild containing the referenced message'),
type: MessageReferenceTypeSchema,
});
export type MessageReferenceResponse = z.infer<typeof MessageReferenceResponse>;
export const ReactionEmojiResponse = z.object({
id: SnowflakeStringType.nullish().describe('The ID of the custom emoji (null for Unicode emojis)'),
name: z.string().describe('The name of the emoji (or Unicode character for standard emojis)'),
animated: z.boolean().nullish().describe('Whether the emoji is animated'),
});
export type ReactionEmojiResponse = z.infer<typeof ReactionEmojiResponse>;
export const MessageReactionResponse = z.object({
emoji: ReactionEmojiResponse.describe('The emoji used for the reaction'),
count: Int32Type.describe('The total number of times this reaction has been used'),
me: z.literal(true).nullish().describe('Whether the current user has reacted with this emoji'),
});
export type MessageReactionResponse = z.infer<typeof MessageReactionResponse>;
export const MessageStickerResponse = z.object({
id: SnowflakeStringType.describe('The unique identifier of the sticker'),
name: z.string().describe('The name of the sticker'),
animated: z.boolean().describe('Whether the sticker is animated'),
});
export type MessageStickerResponse = z.infer<typeof MessageStickerResponse>;
export const MessageSnapshotResponse = z.object({
content: z.string().nullish().describe('The text content of the snapshot'),
timestamp: z.iso.datetime().describe('The ISO 8601 timestamp of when the original message was created'),
edited_timestamp: z.iso
.datetime()
.nullish()
.describe('The ISO 8601 timestamp of when the original message was last edited'),
mentions: z.array(z.string()).max(100).nullish().describe('The user IDs mentioned in the snapshot'),
mention_roles: z.array(z.string()).max(100).nullish().describe('The role IDs mentioned in the snapshot'),
embeds: z.array(MessageEmbedResponse).max(10).nullish().describe('The embeds included in the snapshot'),
attachments: z
.array(MessageAttachmentResponse)
.max(10)
.nullish()
.describe('The attachments included in the snapshot'),
stickers: z.array(MessageStickerResponse).max(3).nullish().describe('The stickers included in the snapshot'),
type: MessageTypeSchema,
flags: createBitflagInt32Type(
MessageFlags,
MessageFlagsDescriptions,
'The bitwise flags of the original message',
'MessageFlags',
),
});
export type MessageSnapshotResponse = z.infer<typeof MessageSnapshotResponse>;
export const MessageCallResponse = z.object({
participants: z.array(z.string()).max(100).describe('The user IDs of participants in the call'),
ended_timestamp: z.iso.datetime().nullish().describe('The ISO 8601 timestamp of when the call ended'),
});
export type MessageCallResponse = z.infer<typeof MessageCallResponse>;
export const MessageBaseResponseSchema = z.object({
id: SnowflakeStringType.describe('The unique identifier (snowflake) for this message'),
channel_id: SnowflakeStringType.describe('The ID of the channel this message was sent in'),
author: z.lazy(() => UserPartialResponse).describe('The author of the message'),
webhook_id: SnowflakeStringType.nullish().describe('The ID of the webhook that sent this message'),
type: MessageTypeSchema,
flags: createBitflagInt32Type(
MessageFlags,
MessageFlagsDescriptions,
'The bitwise flags for this message',
'MessageFlags',
),
content: z.string().describe('The text content of the message'),
timestamp: z.iso.datetime().describe('The ISO 8601 timestamp of when the message was created'),
edited_timestamp: z.iso.datetime().nullish().describe('The ISO 8601 timestamp of when the message was last edited'),
pinned: z.boolean().describe('Whether the message is pinned'),
mention_everyone: z.boolean().describe('Whether the message mentions @everyone'),
tts: z.boolean().optional().describe('Whether the message was sent as text-to-speech'),
mentions: z
.array(z.lazy(() => UserPartialResponse))
.max(100)
.nullish()
.describe('The users mentioned in the message'),
mention_roles: z.array(z.string()).max(100).nullish().describe('The role IDs mentioned in the message'),
embeds: z.array(MessageEmbedResponse).max(10).nullish().describe('The embeds attached to the message'),
attachments: z.array(MessageAttachmentResponse).max(10).nullish().describe('The files attached to the message'),
stickers: z.array(MessageStickerResponse).max(3).nullish().describe('The stickers sent with the message'),
reactions: z
.array(MessageReactionResponse)
.max(MAX_REACTIONS_PER_MESSAGE)
.nullish()
.describe('The reactions on the message'),
message_reference: MessageReferenceResponse.nullish().describe('Reference data for replies or forwards'),
message_snapshots: z.array(MessageSnapshotResponse).max(10).nullish().describe('Snapshots of forwarded messages'),
nonce: z.string().nullish().describe('A client-provided value for message deduplication'),
call: MessageCallResponse.nullish().describe('Call information if this message represents a call'),
});
export type MessageBaseResponse = z.infer<typeof MessageBaseResponseSchema>;
export interface MessageResponse extends MessageBaseResponse {
referenced_message?: MessageResponse | null;
}
export const MessageResponseSchema = MessageBaseResponseSchema.extend({
referenced_message: MessageBaseResponseSchema.nullish().describe(
'The message that this message is replying to or forwarding',
),
});
export const ChannelPinMessageResponse = MessageResponseSchema.omit({
referenced_message: true,
reactions: true,
}).describe('The pinned message');
export type ChannelPinMessageResponse = z.infer<typeof ChannelPinMessageResponse>;
export const ChannelPinResponse = z.object({
message: ChannelPinMessageResponse,
pinned_at: z.iso.datetime().describe('The ISO 8601 timestamp of when the message was pinned'),
});
export type ChannelPinResponse = z.infer<typeof ChannelPinResponse>;
export const ChannelPinsResponse = z.object({
items: z.array(ChannelPinResponse).describe('Pinned messages in this channel'),
has_more: z.boolean().describe('Whether more pins can be fetched with pagination'),
});
export type ChannelPinsResponse = z.infer<typeof ChannelPinsResponse>;
export const ReactionUsersListResponse = z.array(z.lazy(() => UserPartialResponse));
export type ReactionUsersListResponse = z.infer<typeof ReactionUsersListResponse>;
export const MessageSearchResultsResponse = z.object({
messages: z
.array(MessageResponseSchema.omit({referenced_message: true}))
.max(100)
.describe('The messages matching the search query'),
total: Int32Type.describe('The total number of messages matching the search'),
hits_per_page: Int32Type.describe('The maximum number of messages returned per page'),
page: Int32Type.describe('The current page number'),
});
export type MessageSearchResultsResponse = z.infer<typeof MessageSearchResultsResponse>;
export const MessageSearchIndexingResponse = z.object({
indexing: z.literal(true).describe('Indicates that one or more channels are being indexed'),
});
export type MessageSearchIndexingResponse = z.infer<typeof MessageSearchIndexingResponse>;
export const MessageSearchResponse = z.union([MessageSearchResultsResponse, MessageSearchIndexingResponse]);
export type MessageSearchResponse = z.infer<typeof MessageSearchResponse>;
export const MessageListResponse = z.array(MessageResponseSchema);
export type MessageListResponse = z.infer<typeof MessageListResponse>;
export interface MessageReference {
readonly message_id: string;
readonly channel_id: string;
readonly guild_id?: string;
readonly type?: number;
}
export interface ReactionEmoji {
readonly id?: string | null;
readonly name: string;
readonly animated?: boolean;
readonly url?: string | null;
}
export interface MessageReaction {
readonly emoji: ReactionEmoji;
readonly count: number;
readonly me?: true;
readonly me_burst?: boolean;
readonly count_details?: {
readonly burst: number;
readonly normal: number;
};
}
export interface MessageAttachment {
readonly id: string;
readonly filename: string;
readonly title?: string;
readonly description?: string;
readonly content_type?: string;
readonly size: number;
readonly url: string | null;
readonly proxy_url: string | null;
readonly width?: number;
readonly height?: number;
readonly placeholder?: string;
readonly flags: number;
readonly duration?: number;
readonly waveform?: string;
readonly content_hash?: string | null;
readonly nsfw?: boolean;
readonly expires_at?: string | null;
readonly expired?: boolean;
}
export interface MessageCall {
readonly participants: ReadonlyArray<string>;
readonly ended_timestamp?: string | null;
}
export interface MessageSnapshot {
readonly type: number;
readonly content: string;
readonly embeds?: ReadonlyArray<MessageEmbed>;
readonly attachments?: ReadonlyArray<MessageAttachment>;
readonly timestamp: string;
}
export interface MessageStickerItem {
readonly id: string;
readonly name: string;
readonly animated: boolean;
}
export interface AllowedMentions {
readonly parse?: ReadonlyArray<'roles' | 'users' | 'everyone'>;
readonly roles?: ReadonlyArray<string>;
readonly users?: ReadonlyArray<string>;
readonly replied_user?: boolean;
}
export interface ChannelMention {
readonly id: string;
readonly guild_id: string;
readonly type: number;
readonly name: string;
readonly parent_id?: string | null;
}
export interface MessageMention extends UserPartial {
readonly member?: Omit<GuildMemberData, 'user'>;
}
export interface Message {
readonly id: string;
readonly channel_id: string;
readonly guild_id?: string;
readonly author: UserPartial;
readonly member?: Omit<GuildMemberData, 'user'>;
readonly webhook_id?: string;
readonly type: number;
readonly flags: number;
readonly pinned: boolean;
readonly tts?: boolean;
readonly mention_everyone: boolean;
readonly content: string;
readonly timestamp: string;
readonly edited_timestamp?: string;
readonly mentions?: ReadonlyArray<MessageMention>;
readonly mention_roles?: ReadonlyArray<string>;
readonly mention_channels?: ReadonlyArray<ChannelMention>;
readonly embeds?: ReadonlyArray<MessageEmbed>;
readonly attachments?: ReadonlyArray<MessageAttachment>;
readonly stickers?: ReadonlyArray<MessageStickerItem>;
readonly reactions?: ReadonlyArray<MessageReaction>;
readonly message_reference?: MessageReference;
readonly referenced_message?: Message | null;
readonly message_snapshots?: ReadonlyArray<MessageSnapshot>;
readonly call?: MessageCall | null;
readonly state?: string;
readonly nonce?: string;
readonly blocked?: boolean;
readonly loggingName?: string;
readonly _allowedMentions?: AllowedMentions;
readonly _favoriteMemeId?: string;
}
@@ -0,0 +1,125 @@
/*
* 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 {MessageFlags, MessageFlagsDescriptions} from '@fluxer/constants/src/ChannelConstants';
import {MessageEmbedResponse} from '@fluxer/schema/src/domains/message/EmbedSchemas';
import {
MessageAttachmentResponse,
MessageStickerResponse,
} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import {
AllowedMentionParseTypeSchema,
MessageReferenceTypeSchema,
} from '@fluxer/schema/src/primitives/MessageValidators';
import {
createBitflagInt32Type,
createNamedStringLiteralUnion,
SnowflakeStringType,
withFieldDescription,
withOpenApiType,
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const ScheduledMessageAllowedMentionsSchema = z.object({
parse: z.array(AllowedMentionParseTypeSchema).optional().describe('Types of mentions to parse from content'),
users: z.array(SnowflakeStringType).optional().describe('Array of user IDs to mention'),
roles: z.array(SnowflakeStringType).optional().describe('Array of role IDs to mention'),
replied_user: z.boolean().optional().describe('Whether to mention the author of the replied message'),
});
export type ScheduledMessageAllowedMentionsSchema = z.infer<typeof ScheduledMessageAllowedMentionsSchema>;
export const ScheduledMessageReferenceSchema = z.object({
message_id: SnowflakeStringType.describe('ID of the message being referenced'),
channel_id: SnowflakeStringType.optional().describe('ID of the channel containing the referenced message'),
guild_id: SnowflakeStringType.optional().describe('ID of the guild containing the referenced message'),
type: withFieldDescription(MessageReferenceTypeSchema, 'The type of message reference').optional(),
});
export type ScheduledMessageReferenceSchema = z.infer<typeof ScheduledMessageReferenceSchema>;
export const ScheduledMessagePayloadResponseSchema = z.object({
content: z.string().nullish().describe('The text content of the scheduled message'),
tts: z.boolean().optional().describe('Whether this is a text-to-speech message'),
embeds: z.array(MessageEmbedResponse).max(10).optional().describe('Array of embed objects attached to the message'),
attachments: z
.array(MessageAttachmentResponse)
.max(10)
.optional()
.describe('Array of attachment objects for the message'),
stickers: z
.array(MessageStickerResponse)
.max(3)
.optional()
.describe('Array of sticker objects attached to the message'),
sticker_ids: z
.array(SnowflakeStringType)
.max(3)
.optional()
.describe('Array of sticker IDs to include in the message'),
allowed_mentions: ScheduledMessageAllowedMentionsSchema.optional().describe(
'Controls which mentions trigger notifications',
),
message_reference: ScheduledMessageReferenceSchema.optional().describe(
'Reference to another message (for replies or forwards)',
),
flags: createBitflagInt32Type(MessageFlags, MessageFlagsDescriptions, 'Message flags', 'MessageFlags').optional(),
nonce: z.string().optional().describe('Client-generated identifier for the message'),
favorite_meme_id: SnowflakeStringType.optional().describe('ID of a favorite meme to attach'),
});
export type ScheduledMessagePayloadResponseSchema = z.infer<typeof ScheduledMessagePayloadResponseSchema>;
export const ScheduledMessageStatus = withOpenApiType(
createNamedStringLiteralUnion(
[
['pending', 'pending', 'The message is pending validation and has not yet been scheduled'],
['invalid', 'invalid', 'The message failed validation and cannot be sent'],
['scheduled', 'scheduled', 'The message has been validated and is scheduled for delivery'],
['sent', 'sent', 'The message has been successfully sent'],
['failed', 'failed', 'The message failed to send after being scheduled'],
['cancelled', 'cancelled', 'The scheduled message was cancelled by the user'],
],
'The current status of the scheduled message',
),
'ScheduledMessageStatus',
);
export type ScheduledMessageStatus = z.infer<typeof ScheduledMessageStatus>;
export const ScheduledMessageResponseSchema = z.object({
id: SnowflakeStringType.describe('The unique identifier for this scheduled message'),
channel_id: SnowflakeStringType.describe('The ID of the channel this message will be sent to'),
scheduled_at: z.string().describe('The ISO 8601 UTC timestamp when the message is scheduled to be sent'),
scheduled_local_at: z.string().describe('The ISO 8601 timestamp in the user local timezone'),
timezone: z.string().describe('The IANA timezone identifier used for scheduling'),
status: ScheduledMessageStatus.describe('The current status of the scheduled message'),
status_reason: z.string().nullable().describe('A human-readable reason for the current status, if applicable'),
payload: ScheduledMessagePayloadResponseSchema.describe('The message content and metadata to be sent'),
created_at: z.string().describe('The ISO 8601 timestamp when this scheduled message was created'),
invalidated_at: z.string().nullable().describe('The ISO 8601 timestamp when the message was marked invalid'),
});
export type ScheduledMessageResponseSchema = z.infer<typeof ScheduledMessageResponseSchema>;
export const ScheduledMessageListResponse = z
.array(ScheduledMessageResponseSchema)
.max(100)
.describe('A list of scheduled messages');
export type ScheduledMessageListResponse = z.infer<typeof ScheduledMessageListResponse>;
@@ -0,0 +1,62 @@
/*
* 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 {AllowedMentionParseTypes, MessageReferenceTypes} from '@fluxer/constants/src/ChannelConstants';
import {
AllowedMentionParseTypeSchema,
MessageReferenceTypeSchema,
} from '@fluxer/schema/src/primitives/MessageValidators';
import {SnowflakeType, withFieldDescription} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const ALLOWED_MENTIONS_PARSE = [
AllowedMentionParseTypes.USERS,
AllowedMentionParseTypes.ROLES,
AllowedMentionParseTypes.EVERYONE,
] as const;
export const AllowedMentionsRequest = z.object({
parse: z.array(AllowedMentionParseTypeSchema).optional().describe('Types of mentions to parse from content'),
users: z.array(SnowflakeType).max(100).optional().describe('Array of user IDs to mention (max 100)'),
roles: z.array(SnowflakeType).max(100).optional().describe('Array of role IDs to mention (max 100)'),
replied_user: z.boolean().optional().describe('Whether to mention the author of the replied message'),
});
export type AllowedMentionsRequest = z.infer<typeof AllowedMentionsRequest>;
export const MessageReferenceRequest = z
.object({
message_id: SnowflakeType.describe('ID of the message being referenced'),
channel_id: SnowflakeType.optional().describe('ID of the channel containing the referenced message'),
guild_id: SnowflakeType.optional().describe('ID of the guild containing the referenced message'),
type: withFieldDescription(MessageReferenceTypeSchema, 'Type of reference (0 = default, 1 = forward)').optional(),
})
.refine(
(data) => {
if (data.type === MessageReferenceTypes.FORWARD) {
return data.channel_id !== undefined && data.message_id !== undefined;
}
return true;
},
{
message: 'Forward message reference must include channel_id and message_id',
},
);
export type MessageReferenceRequest = z.infer<typeof MessageReferenceRequest>;