refactor(api): split webhook attachment schemas (#2268)

This commit is contained in:
Hampus
2026-08-31 19:13:51 +02:00
committed by GitHub
parent 240b7e4388
commit ef8d1225b5
3 changed files with 102 additions and 54 deletions
+72 -44
View File
@@ -22220,7 +22220,7 @@
"required": true,
"content": {
"application/json": {"schema": {"$ref": "#/components/schemas/WebhookMessageRequest"}},
"multipart/form-data": {"schema": {"$ref": "#/components/schemas/WebhookMessageRequest"}}
"multipart/form-data": {"schema": {"$ref": "#/components/schemas/WebhookMultipartMessageRequest"}}
}
}
}
@@ -34715,51 +34715,51 @@
"items": {"$ref": "#/components/schemas/RichEmbedRequest"},
"description": "Array of embed objects to include in the message"
},
"message_reference": {
"anyOf": [{"$ref": "#/components/schemas/MessageReferenceRequest"}, {"type": "null"}],
"description": "Reference to another message (for replies or forwards)"
},
"allowed_mentions": {
"anyOf": [{"$ref": "#/components/schemas/AllowedMentionsRequest"}, {"type": "null"}],
"description": "Controls which mentions trigger notifications"
},
"flags": {"$ref": "#/components/schemas/MessageFlags"},
"nonce": {"$ref": "#/components/schemas/MessageNonceRequest"},
"favorite_meme_id": {
"anyOf": [{"$ref": "#/components/schemas/SnowflakeType"}, {"type": "null"}],
"description": "ID of a favorite meme to attach"
},
"sticker_ids": {
"anyOf": [
{"type": "array", "items": {"$ref": "#/components/schemas/SnowflakeType"}, "maxItems": 3},
{"type": "null"}
],
"description": "Array of sticker IDs to include (max 3)"
},
"tts": {"type": "boolean", "description": "Whether this is a text-to-speech message"},
"username": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Override the default username of the webhook for this message"
},
"avatar_url": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Override the default avatar URL of the webhook for this message"
},
"attachments": {
"type": "array",
"items": {
"oneOf": [
{"$ref": "#/components/schemas/ClientUploadedAttachmentRequest"},
{
"type": "object",
"properties": {
"id": {
"oneOf": [
{"$ref": "#/components/schemas/SnowflakeType"},
{"$ref": "#/components/schemas/Int32Type"}
],
"description": "Attachment ID for referencing uploaded files"
},
"filename": {"type": "string", "description": "Name of the file (1-1024 characters)"},
"description": {
"type": "string",
"description": "Description for the attachment (max 4096 characters)"
},
"content_type": {"type": "string", "description": "MIME type of the file"},
"upload_filename": {"not": {}},
"file_size": {"not": {}},
"size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"url": {"type": "string", "description": "URL of the attachment"},
"proxy_url": {"type": "string", "description": "Proxied URL of the attachment"},
"height": {
"type": "integer",
"format": "int53",
"description": "Height of the image/video in pixels"
},
"width": {
"type": "integer",
"format": "int53",
"description": "Width of the image/video in pixels"
},
"ephemeral": {"type": "boolean", "description": "Whether this attachment is ephemeral"},
"duration": {"type": "number", "description": "Duration of audio file in seconds"},
"waveform": {"type": "string", "description": "Base64-encoded bytearray of audio waveform"},
"flags": {"$ref": "#/components/schemas/MessageAttachmentFlags"}
}
}
]
},
"description": "Array of attachment objects"
"items": {"$ref": "#/components/schemas/ClientUploadedAttachmentRequest"},
"description": "Array of attachments uploaded through the presigned upload endpoint"
}
}
},
"WebhookMultipartMessageRequest": {
"type": "object",
"properties": {
"content": {"anyOf": [{"$ref": "#/components/schemas/MessageContentRequest"}, {"type": "null"}]},
"embeds": {
"type": "array",
"items": {"$ref": "#/components/schemas/RichEmbedRequest"},
"description": "Array of embed objects to include in the message"
},
"message_reference": {
"anyOf": [{"$ref": "#/components/schemas/MessageReferenceRequest"}, {"type": "null"}],
@@ -34790,6 +34790,34 @@
"avatar_url": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Override the default avatar URL of the webhook for this message"
},
"attachments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"oneOf": [{"$ref": "#/components/schemas/SnowflakeType"}, {"$ref": "#/components/schemas/Int32Type"}],
"description": "Attachment ID for referencing uploaded files"
},
"filename": {"type": "string", "description": "Name of the file (1-1024 characters)"},
"description": {
"type": "string",
"description": "Description for the attachment (max 4096 characters)"
},
"content_type": {"type": "string", "description": "MIME type of the file"},
"size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"url": {"type": "string", "description": "URL of the attachment"},
"proxy_url": {"type": "string", "description": "Proxied URL of the attachment"},
"height": {"type": "integer", "format": "int53", "description": "Height of the image/video in pixels"},
"width": {"type": "integer", "format": "int53", "description": "Width of the image/video in pixels"},
"ephemeral": {"type": "boolean", "description": "Whether this attachment is ephemeral"},
"duration": {"type": "number", "description": "Duration of audio file in seconds"},
"waveform": {"type": "string", "description": "Base64-encoded bytearray of audio waveform"},
"flags": {"$ref": "#/components/schemas/MessageAttachmentFlags"}
}
},
"description": "Array of multipart attachment metadata objects"
}
}
},
@@ -19,6 +19,7 @@ import {
WebhookExecuteQueryRequest,
WebhookMessageEditRequest,
WebhookMessageRequest,
WebhookMultipartMessageRequest,
WebhookTokenUpdateRequest,
WebhookUpdateRequest,
} from '@fluxer/schema/src/domains/webhook/WebhookRequestSchemas';
@@ -47,6 +48,14 @@ function validateWebhookMessagePayload(data: unknown): WebhookMessageRequest {
return validationResult.data;
}
function validateWebhookMultipartMessagePayload(data: unknown): WebhookMultipartMessageRequest {
const validationResult = WebhookMultipartMessageRequest.safeParse(normalizeMessageRequestPayload(data));
if (!validationResult.success) {
throw InputValidationError.fromCode('message_data', ValidationErrorCodes.INVALID_MESSAGE_DATA);
}
return validationResult.data;
}
async function parseWebhookJsonMessageData(ctx: Context<HonoEnv>): Promise<WebhookExecuteMessageData> {
let data: unknown;
try {
@@ -89,10 +98,11 @@ async function parseWebhookMultipartMessageData(
if (!parsedPayload) {
throw InputValidationError.fromCode('message_data', ValidationErrorCodes.INVALID_MESSAGE_DATA);
}
const webhookData = validateWebhookMessagePayload(parsedPayload);
const webhookData = validateWebhookMultipartMessagePayload(parsedPayload);
return {
...webhookData,
...messageData,
attachments: messageData.attachments,
username: webhookData.username,
avatar_url: webhookData.avatar_url,
};
@@ -333,7 +343,7 @@ export function WebhookController(app: HonoApp) {
'Executes the webhook by sending a message to its configured channel. If the wait query parameter is true, returns the created message object; otherwise returns a 204 status with no content.',
responseSchema: MessageResponseSchema,
requestSchema: WebhookMessageRequest,
requestFormSchema: WebhookMessageRequest,
requestFormSchema: WebhookMultipartMessageRequest,
statusCode: 200,
tags: ['Webhooks'],
}),
@@ -62,7 +62,7 @@ export const WebhookTokenUpdateRequest = z
export type WebhookTokenUpdateRequest = z.infer<typeof WebhookTokenUpdateRequest>;
const WebhookAttachmentRequest = z.object({
const WebhookMultipartAttachmentRequest = z.object({
id: z
.union([SnowflakeType, coerceNumberFromString(Int32Type)])
.optional()
@@ -70,8 +70,6 @@ const WebhookAttachmentRequest = z.object({
filename: createStringType(1, 1024).optional().describe('Name of the file (1-1024 characters)'),
description: createStringType(1, 4096).optional().describe('Description for the attachment (max 4096 characters)'),
content_type: createStringType(1, 256).optional().describe('MIME type of the file'),
upload_filename: z.never().optional(),
file_size: z.never().optional(),
size: NonNegativeSafeIntegerType.optional().describe('Size of the file in bytes'),
url: URLType.optional().describe('URL of the attachment'),
proxy_url: URLType.optional().describe('Proxied URL of the attachment'),
@@ -88,14 +86,10 @@ const WebhookAttachmentRequest = z.object({
).optional(),
});
export const WebhookMessageRequest = z
const WebhookMessageRequestBase = z
.object({
content: MessageContentRequest.nullish(),
embeds: z.array(RichEmbedRequest).optional().describe('Array of embed objects to include in the message'),
attachments: z
.array(z.union([ClientUploadedAttachmentRequest, WebhookAttachmentRequest]))
.optional()
.describe('Array of attachment objects'),
message_reference: MessageReferenceRequest.nullish().describe(
'Reference to another message (for replies or forwards)',
),
@@ -115,8 +109,24 @@ export const WebhookMessageRequest = z
})
.partial();
export const WebhookMessageRequest = WebhookMessageRequestBase.extend({
attachments: z
.array(ClientUploadedAttachmentRequest)
.optional()
.describe('Array of attachments uploaded through the presigned upload endpoint'),
});
export type WebhookMessageRequest = z.infer<typeof WebhookMessageRequest>;
export const WebhookMultipartMessageRequest = WebhookMessageRequestBase.extend({
attachments: z
.array(WebhookMultipartAttachmentRequest)
.optional()
.describe('Array of multipart attachment metadata objects'),
});
export type WebhookMultipartMessageRequest = z.infer<typeof WebhookMultipartMessageRequest>;
export const WebhookMessageEditRequest = z
.object({
content: MessageContentRequest.nullish().describe(