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

This commit is contained in:
Hampus authored and GitHub committed 2026-08-31 19:13:51 +02:00
1 parent 240b7e4388
commit ef8d1225b5
3 files changed
+102 -54

No files matched your search

+72 -44
View File
@@ -22220,7 +22220,7 @@
"required": true, "required": true,
"content": { "content": {
"application/json": {"schema": {"$ref": "#/components/schemas/WebhookMessageRequest"}}, "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"}, "items": {"$ref": "#/components/schemas/RichEmbedRequest"},
"description": "Array of embed objects to include in the message" "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": { "attachments": {
"type": "array", "type": "array",
"items": { "items": {"$ref": "#/components/schemas/ClientUploadedAttachmentRequest"},
"oneOf": [ "description": "Array of attachments uploaded through the presigned upload endpoint"
{"$ref": "#/components/schemas/ClientUploadedAttachmentRequest"}, }
{ }
"type": "object", },
"properties": { "WebhookMultipartMessageRequest": {
"id": { "type": "object",
"oneOf": [ "properties": {
{"$ref": "#/components/schemas/SnowflakeType"}, "content": {"anyOf": [{"$ref": "#/components/schemas/MessageContentRequest"}, {"type": "null"}]},
{"$ref": "#/components/schemas/Int32Type"} "embeds": {
], "type": "array",
"description": "Attachment ID for referencing uploaded files" "items": {"$ref": "#/components/schemas/RichEmbedRequest"},
}, "description": "Array of embed objects to include in the message"
"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"
}, },
"message_reference": { "message_reference": {
"anyOf": [{"$ref": "#/components/schemas/MessageReferenceRequest"}, {"type": "null"}], "anyOf": [{"$ref": "#/components/schemas/MessageReferenceRequest"}, {"type": "null"}],
@@ -34790,6 +34790,34 @@
"avatar_url": { "avatar_url": {
"anyOf": [{"type": "string"}, {"type": "null"}], "anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Override the default avatar URL of the webhook for this message" "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, WebhookExecuteQueryRequest,
WebhookMessageEditRequest, WebhookMessageEditRequest,
WebhookMessageRequest, WebhookMessageRequest,
WebhookMultipartMessageRequest,
WebhookTokenUpdateRequest, WebhookTokenUpdateRequest,
WebhookUpdateRequest, WebhookUpdateRequest,
} from '@fluxer/schema/src/domains/webhook/WebhookRequestSchemas'; } from '@fluxer/schema/src/domains/webhook/WebhookRequestSchemas';
@@ -47,6 +48,14 @@ function validateWebhookMessagePayload(data: unknown): WebhookMessageRequest {
return validationResult.data; 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> { async function parseWebhookJsonMessageData(ctx: Context<HonoEnv>): Promise<WebhookExecuteMessageData> {
let data: unknown; let data: unknown;
try { try {
@@ -89,10 +98,11 @@ async function parseWebhookMultipartMessageData(
if (!parsedPayload) { if (!parsedPayload) {
throw InputValidationError.fromCode('message_data', ValidationErrorCodes.INVALID_MESSAGE_DATA); throw InputValidationError.fromCode('message_data', ValidationErrorCodes.INVALID_MESSAGE_DATA);
} }
const webhookData = validateWebhookMessagePayload(parsedPayload); const webhookData = validateWebhookMultipartMessagePayload(parsedPayload);
return { return {
...webhookData, ...webhookData,
...messageData, ...messageData,
attachments: messageData.attachments,
username: webhookData.username, username: webhookData.username,
avatar_url: webhookData.avatar_url, 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.', '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, responseSchema: MessageResponseSchema,
requestSchema: WebhookMessageRequest, requestSchema: WebhookMessageRequest,
requestFormSchema: WebhookMessageRequest, requestFormSchema: WebhookMultipartMessageRequest,
statusCode: 200, statusCode: 200,
tags: ['Webhooks'], tags: ['Webhooks'],
}), }),
@@ -62,7 +62,7 @@ export const WebhookTokenUpdateRequest = z
export type WebhookTokenUpdateRequest = z.infer<typeof WebhookTokenUpdateRequest>; export type WebhookTokenUpdateRequest = z.infer<typeof WebhookTokenUpdateRequest>;
const WebhookAttachmentRequest = z.object({ const WebhookMultipartAttachmentRequest = z.object({
id: z id: z
.union([SnowflakeType, coerceNumberFromString(Int32Type)]) .union([SnowflakeType, coerceNumberFromString(Int32Type)])
.optional() .optional()
@@ -70,8 +70,6 @@ const WebhookAttachmentRequest = z.object({
filename: createStringType(1, 1024).optional().describe('Name of the file (1-1024 characters)'), 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)'), 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'), 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'), size: NonNegativeSafeIntegerType.optional().describe('Size of the file in bytes'),
url: URLType.optional().describe('URL of the attachment'), url: URLType.optional().describe('URL of the attachment'),
proxy_url: URLType.optional().describe('Proxied URL of the attachment'), proxy_url: URLType.optional().describe('Proxied URL of the attachment'),
@@ -88,14 +86,10 @@ const WebhookAttachmentRequest = z.object({
).optional(), ).optional(),
}); });
export const WebhookMessageRequest = z const WebhookMessageRequestBase = z
.object({ .object({
content: MessageContentRequest.nullish(), content: MessageContentRequest.nullish(),
embeds: z.array(RichEmbedRequest).optional().describe('Array of embed objects to include in the message'), 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( message_reference: MessageReferenceRequest.nullish().describe(
'Reference to another message (for replies or forwards)', 'Reference to another message (for replies or forwards)',
), ),
@@ -115,8 +109,24 @@ export const WebhookMessageRequest = z
}) })
.partial(); .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 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 export const WebhookMessageEditRequest = z
.object({ .object({
content: MessageContentRequest.nullish().describe( content: MessageContentRequest.nullish().describe(