mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-02 21:04:06 +03:00
fix(api): support int53 attachment sizes (#1261)
This commit is contained in:
@@ -8977,7 +8977,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8640000000000000,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Inclusive start timestamp in milliseconds"
|
||||
}
|
||||
},
|
||||
@@ -8989,7 +8989,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8640000000000000,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Inclusive end timestamp in milliseconds"
|
||||
}
|
||||
},
|
||||
@@ -9087,7 +9087,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8640000000000000,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Inclusive start timestamp in milliseconds"
|
||||
}
|
||||
},
|
||||
@@ -9099,7 +9099,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8640000000000000,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Inclusive end timestamp in milliseconds"
|
||||
}
|
||||
},
|
||||
@@ -10613,7 +10613,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Number of entries to skip"
|
||||
}
|
||||
}
|
||||
@@ -10645,7 +10645,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Number of entries to skip"
|
||||
}
|
||||
}
|
||||
@@ -11147,7 +11147,7 @@
|
||||
"amount_cents": {
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -11981,7 +11981,7 @@
|
||||
"properties": {
|
||||
"guild_id": {"$ref": "#/components/schemas/SnowflakeType"},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "format": "int32"},
|
||||
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
},
|
||||
"required": ["guild_id"]
|
||||
},
|
||||
@@ -12219,7 +12219,7 @@
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "format": "int32"},
|
||||
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
}
|
||||
},
|
||||
"ShutdownGuildRequest": {
|
||||
@@ -12439,9 +12439,9 @@
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64"
|
||||
"format": "int53"
|
||||
},
|
||||
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
"revoked_at": {"nullable": true, "type": "string", "format": "date-time"},
|
||||
"approval_required": {"type": "boolean"},
|
||||
"last_used_at": {"nullable": true, "type": "string", "format": "date-time"},
|
||||
@@ -12668,7 +12668,7 @@
|
||||
"logo_uri": {"nullable": true, "type": "string"},
|
||||
"tos_uri": {"nullable": true, "type": "string"},
|
||||
"policy_uri": {"nullable": true, "type": "string"},
|
||||
"key_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
"key_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
@@ -12698,7 +12698,7 @@
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -12706,7 +12706,7 @@
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -12715,7 +12715,7 @@
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -12723,7 +12723,7 @@
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -12737,14 +12737,14 @@
|
||||
"min_lifetime_days": {
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
"max_lifetime_days": {
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -12752,14 +12752,14 @@
|
||||
"renew_threshold_days": {
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
"renew_window_days": {
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
}
|
||||
@@ -12871,9 +12871,9 @@
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64"
|
||||
"format": "int53"
|
||||
},
|
||||
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
"revoked_at": {"nullable": true, "type": "string", "format": "date-time"},
|
||||
"approval_required": {"type": "boolean"},
|
||||
"last_used_at": {"nullable": true, "type": "string", "format": "date-time"},
|
||||
@@ -13064,7 +13064,7 @@
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -13072,7 +13072,7 @@
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -13081,7 +13081,7 @@
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
@@ -13089,7 +13089,7 @@
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
}
|
||||
@@ -13296,7 +13296,7 @@
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "number", "minimum": 0},
|
||||
"additionalProperties": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
|
||||
"description": "Per-limit key values"
|
||||
},
|
||||
"modifiedFields": {"type": "array", "items": {"type": "string"}}
|
||||
@@ -13352,6 +13352,7 @@
|
||||
"limit_keys"
|
||||
]
|
||||
},
|
||||
"NonNegativeSafeIntegerType": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
"LimitConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13387,7 +13388,7 @@
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "number", "minimum": 0},
|
||||
"additionalProperties": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
|
||||
"description": "Per-limit key values"
|
||||
}
|
||||
},
|
||||
@@ -13455,7 +13456,7 @@
|
||||
"content_type": {"nullable": true, "type": "string"},
|
||||
"width": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
|
||||
"height": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
|
||||
"size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
|
||||
"size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/NonNegativeSafeIntegerType"}]},
|
||||
"ncmec_status": {
|
||||
"type": "string",
|
||||
"enum": ["not_submitted", "submitted", "failed"],
|
||||
@@ -14124,8 +14125,8 @@
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 2147483647,
|
||||
"format": "int32",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int53",
|
||||
"description": "The size of the attachment in bytes"
|
||||
},
|
||||
"url": {"description": "The URL of the attachment", "nullable": true, "type": "string"},
|
||||
@@ -14387,7 +14388,7 @@
|
||||
"items": {"$ref": "#/components/schemas/MessageResponseSchema"},
|
||||
"maxItems": 100
|
||||
},
|
||||
"total": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
"total": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
},
|
||||
"required": ["messages", "total"]
|
||||
},
|
||||
@@ -14405,7 +14406,7 @@
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "enum": [true]},
|
||||
"job_id": {"type": "string"},
|
||||
"requested": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
"requested": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
},
|
||||
"required": ["success", "job_id"]
|
||||
},
|
||||
@@ -14558,7 +14559,10 @@
|
||||
"content_type": {"nullable": true, "type": "string"},
|
||||
"width": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
|
||||
"height": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
|
||||
"size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
|
||||
"size": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/NonNegativeSafeIntegerType"}]
|
||||
},
|
||||
"ncmec_status": {
|
||||
"type": "string",
|
||||
"enum": ["not_submitted", "submitted", "failed"],
|
||||
@@ -14664,7 +14668,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Number of reports to skip"
|
||||
}
|
||||
}
|
||||
@@ -14712,7 +14716,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Number of entries to skip"
|
||||
},
|
||||
"reporter_id": {"$ref": "#/components/schemas/SnowflakeType"},
|
||||
@@ -15435,7 +15439,7 @@
|
||||
"email": {"type": "string"},
|
||||
"last_active_ip": {"type": "string"},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "format": "int32"},
|
||||
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
}
|
||||
},
|
||||
"SendPasswordResetRequest": {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {getPurgeQueue, getStorageService} from '../../middleware/ServiceSingleto
|
||||
import {getMessageSearchService} from '../../SearchFactory';
|
||||
import {deleteMessageSearchDocuments} from '../../search/MessageSearchIndexCleanup';
|
||||
import {searchExistingMessages} from '../../search/MessageSearchResultReconciler';
|
||||
import {assertSafeByteSize} from '../../utils/ByteSizeUtils';
|
||||
import type {AdminAuditService} from './AdminAuditService';
|
||||
|
||||
interface AdminMessageServiceDeps {
|
||||
@@ -330,7 +331,7 @@ export class AdminMessageService {
|
||||
content_type: attachment.content_type ?? null,
|
||||
width: attachment.width ?? null,
|
||||
height: attachment.height ?? null,
|
||||
size: attachment.size ?? null,
|
||||
size: attachment.size == null ? null : assertSafeByteSize(attachment.size, 'admin message attachment size'),
|
||||
ncmec_status: attachmentStatuses.get(attachment.id)?.status ?? 'not_submitted',
|
||||
ncmec_report_id: attachmentStatuses.get(attachment.id)?.ncmec_report_id ?? null,
|
||||
ncmec_failure_reason: attachmentStatuses.get(attachment.id)?.failure_reason ?? null,
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {IARMessageContext, IARSubmission} from '../../report/IReportReposit
|
||||
import type {ReportService} from '../../report/ReportService';
|
||||
import {getReportSearchService} from '../../SearchFactory';
|
||||
import type {UserChannelService} from '../../user/services/UserChannelService';
|
||||
import {assertSafeByteSize} from '../../utils/ByteSizeUtils';
|
||||
import type {AdminAuditService} from './AdminAuditService';
|
||||
|
||||
interface AdminReportServiceDeps {
|
||||
@@ -580,7 +581,7 @@ export class AdminReportService {
|
||||
content_type: attachment.content_type ?? null,
|
||||
width: attachment.width ?? null,
|
||||
height: attachment.height ?? null,
|
||||
size: attachment.size != null ? Number(attachment.size) : null,
|
||||
size: attachment.size != null ? assertSafeByteSize(attachment.size, 'admin report attachment size') : null,
|
||||
ncmec_status: attachmentStatusesById.get(attachment.attachment_id.toString())?.status ?? 'not_submitted',
|
||||
ncmec_report_id: attachmentStatusesById.get(attachment.attachment_id.toString())?.ncmec_report_id ?? null,
|
||||
ncmec_failure_reason: attachmentStatusesById.get(attachment.attachment_id.toString())?.failure_reason ?? null,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {FavoriteMemeResponse} from '@fluxer/schema/src/domains/meme/MemeSch
|
||||
import {userIdToChannelId} from '../BrandedTypes';
|
||||
import {makeAttachmentCdnUrl} from '../channel/services/message/MessageHelpers';
|
||||
import type {FavoriteMeme} from '../models/FavoriteMeme';
|
||||
import {assertSafeByteSize} from '../utils/ByteSizeUtils';
|
||||
|
||||
export function mapFavoriteMemeToResponse(meme: FavoriteMeme): FavoriteMemeResponse {
|
||||
const url = makeAttachmentCdnUrl(userIdToChannelId(meme.userId), meme.attachmentId, meme.filename);
|
||||
@@ -17,7 +18,7 @@ export function mapFavoriteMemeToResponse(meme: FavoriteMeme): FavoriteMemeRespo
|
||||
filename: meme.filename,
|
||||
content_type: meme.contentType,
|
||||
content_hash: meme.contentHash ?? null,
|
||||
size: Number(meme.size),
|
||||
size: assertSafeByteSize(meme.size, 'favorite meme size'),
|
||||
width: meme.width ?? null,
|
||||
height: meme.height ?? null,
|
||||
duration: meme.duration ?? null,
|
||||
|
||||
@@ -6224,7 +6224,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Pagination offset"
|
||||
}
|
||||
}
|
||||
@@ -27115,8 +27115,8 @@
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 2147483647,
|
||||
"format": "int32",
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int53",
|
||||
"description": "The size of the attachment in bytes"
|
||||
},
|
||||
"url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The URL of the attachment"},
|
||||
@@ -27900,7 +27900,7 @@
|
||||
"type": "string",
|
||||
"description": "Temporary upload key to reference in message send payloads"
|
||||
},
|
||||
"file_size": {"$ref": "#/components/schemas/Int32Type"},
|
||||
"file_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
|
||||
"content_type": {"type": "string", "description": "Expected MIME type for this upload"},
|
||||
"upload_mode": {"type": "string", "enum": ["singlepart"]},
|
||||
"upload_url": {
|
||||
@@ -27910,6 +27910,7 @@
|
||||
},
|
||||
"required": ["id", "filename", "upload_filename", "file_size", "content_type", "upload_mode", "upload_url"]
|
||||
},
|
||||
"NonNegativeSafeIntegerType": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
"MultipartPresignedAttachmentUploadResponseItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -27919,14 +27920,14 @@
|
||||
"type": "string",
|
||||
"description": "Temporary upload key to reference in message send payloads"
|
||||
},
|
||||
"file_size": {"$ref": "#/components/schemas/Int32Type"},
|
||||
"file_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
|
||||
"content_type": {"type": "string", "description": "Expected MIME type for this upload"},
|
||||
"upload_mode": {"type": "string", "enum": ["multipart"]},
|
||||
"upload_id": {
|
||||
"type": "string",
|
||||
"description": "S3 multipart upload identifier; required to complete the upload"
|
||||
},
|
||||
"part_size": {"$ref": "#/components/schemas/Int32Type"},
|
||||
"part_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -27972,7 +27973,7 @@
|
||||
"properties": {
|
||||
"id": {"$ref": "#/components/schemas/Int32Type"},
|
||||
"filename": {"type": "string", "description": "The name of the file that will be uploaded"},
|
||||
"file_size": {"$ref": "#/components/schemas/Int32Type"},
|
||||
"file_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
|
||||
"content_type": {"type": "string", "description": "MIME type the client will upload"}
|
||||
},
|
||||
"required": ["id", "filename", "file_size", "content_type"]
|
||||
@@ -28482,7 +28483,7 @@
|
||||
"type": "string",
|
||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
|
||||
},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "ISO8601 timestamp for the embed"
|
||||
@@ -28599,7 +28600,7 @@
|
||||
"type": "string",
|
||||
"description": "Temporary upload key returned by the attachment upload endpoint"
|
||||
},
|
||||
"file_size": {"$ref": "#/components/schemas/Int32Type"},
|
||||
"file_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
|
||||
"content_type": {"type": "string", "description": "MIME type of the uploaded file"}
|
||||
},
|
||||
"required": ["id", "filename", "upload_filename", "file_size", "content_type"]
|
||||
@@ -29234,7 +29235,13 @@
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Hash of the file content for deduplication"
|
||||
},
|
||||
"size": {"type": "number", "description": "File size in bytes"},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int53",
|
||||
"description": "File size in bytes"
|
||||
},
|
||||
"width": {
|
||||
"anyOf": [{"type": "integer", "format": "int53"}, {"type": "null"}],
|
||||
"description": "Width of the image or video in pixels"
|
||||
@@ -29311,7 +29318,7 @@
|
||||
},
|
||||
"embed_index": {
|
||||
"anyOf": [
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "Index of the message embed to save as a meme"
|
||||
@@ -29529,14 +29536,14 @@
|
||||
},
|
||||
"started_at_ms": {
|
||||
"anyOf": [
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "Session start Unix timestamp in milliseconds"
|
||||
},
|
||||
"expires_at_ms": {
|
||||
"anyOf": [
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "Session expiration Unix timestamp in milliseconds"
|
||||
@@ -29605,7 +29612,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Unix timestamp in milliseconds when this heartbeat expires"
|
||||
}
|
||||
},
|
||||
@@ -30183,7 +30190,7 @@
|
||||
"explicit_content_filter": {"type": "number", "description": "The explicit content filter level"},
|
||||
"system_channel_id": {
|
||||
"oneOf": [
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
],
|
||||
@@ -30209,7 +30216,7 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"oneOf": [
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
{"type": "string"}
|
||||
],
|
||||
"description": "The template-local role ID"
|
||||
@@ -30218,14 +30225,14 @@
|
||||
"permissions": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
],
|
||||
"description": "The permissions bitfield as a string (legacy)"
|
||||
},
|
||||
"permissions_new": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
],
|
||||
"description": "The permissions bitfield as a string (preferred)"
|
||||
},
|
||||
@@ -30244,7 +30251,7 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"oneOf": [
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
{"type": "string"}
|
||||
],
|
||||
"description": "The template-local channel ID"
|
||||
@@ -30255,7 +30262,7 @@
|
||||
"position": {"type": "number", "description": "The position of the channel"},
|
||||
"parent_id": {
|
||||
"oneOf": [
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
],
|
||||
@@ -30279,7 +30286,7 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"oneOf": [
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
|
||||
{"type": "string"}
|
||||
],
|
||||
"description": "The ID of the role or user for this overwrite"
|
||||
@@ -30291,14 +30298,14 @@
|
||||
"allow": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
],
|
||||
"description": "The allowed permissions bitfield as a string"
|
||||
},
|
||||
"deny": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
|
||||
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
|
||||
],
|
||||
"description": "The denied permissions bitfield as a string"
|
||||
}
|
||||
@@ -30960,7 +30967,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "New position for the channel"
|
||||
},
|
||||
"parent_id": {
|
||||
@@ -31336,7 +31343,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Number of results to skip for pagination"
|
||||
},
|
||||
"role_ids": {
|
||||
@@ -33959,7 +33966,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Page number for pagination (ignored when cursor is provided)"
|
||||
},
|
||||
"cursor": {
|
||||
@@ -35434,7 +35441,7 @@
|
||||
"type": "string",
|
||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
|
||||
},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "When the mute expires"
|
||||
@@ -35479,7 +35486,7 @@
|
||||
"type": "string",
|
||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
|
||||
},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "When the mute expires"
|
||||
@@ -35530,7 +35537,7 @@
|
||||
"type": "string",
|
||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
|
||||
},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "When the mute expires"
|
||||
@@ -36416,7 +36423,7 @@
|
||||
"type": "integer",
|
||||
"maximum": 9007199254740991,
|
||||
"exclusiveMinimum": 0,
|
||||
"format": "int64"
|
||||
"format": "int53"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -36977,7 +36984,7 @@
|
||||
"type": "string",
|
||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
|
||||
},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "When status resets"
|
||||
@@ -37157,7 +37164,7 @@
|
||||
"type": "string",
|
||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
|
||||
},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
|
||||
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
|
||||
{"type": "null"}
|
||||
],
|
||||
"description": "When the custom status expires"
|
||||
@@ -37423,7 +37430,7 @@
|
||||
"description": "Description for the attachment (max 4096 characters)"
|
||||
},
|
||||
"content_type": {"type": "string", "description": "MIME type of the file"},
|
||||
"size": {"type": "integer", "format": "int53", "description": "Size of the file in bytes"},
|
||||
"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"},
|
||||
@@ -37896,7 +37903,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"format": "int64",
|
||||
"format": "int53",
|
||||
"description": "Unix timestamp for the attachment footer"
|
||||
},
|
||||
"author_name": {"type": "string", "description": "Name of the author"},
|
||||
|
||||
@@ -42,6 +42,7 @@ import type {Message} from '../../models/Message';
|
||||
import type {MessageSnapshot} from '../../models/MessageSnapshot';
|
||||
import type {StickerItem} from '../../models/StickerItem';
|
||||
import {mapUserToPartialResponse} from '../../user/UserMappers';
|
||||
import {assertSafeByteSize} from '../../utils/ByteSizeUtils';
|
||||
|
||||
class NoopNatsConnectionManager implements INatsConnectionManager {
|
||||
async connect(): Promise<void> {}
|
||||
@@ -373,7 +374,7 @@ export class RepositoryBackedMessageResponseDataService extends MessageResponseD
|
||||
description: attachment.description,
|
||||
content_type: attachment.contentType,
|
||||
content_hash: attachment.contentHash,
|
||||
size: Number(attachment.size),
|
||||
size: assertSafeByteSize(attachment.size, 'message attachment size'),
|
||||
url,
|
||||
proxy_url: url,
|
||||
width: attachment.width,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
export function assertSafeByteSize(value: bigint | number, fieldName = 'byte size'): number {
|
||||
if (typeof value === 'bigint') {
|
||||
if (value < 0n || value > MAX_SAFE_INTEGER_BIGINT) {
|
||||
throw new Error(`${fieldName} must fit a non-negative JavaScript safe integer`);
|
||||
}
|
||||
return Number(value);
|
||||
}
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new Error(`${fieldName} must fit a non-negative JavaScript safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -58,6 +58,15 @@ const ENRICHMENT_QUERY_CONCURRENCY: usize = 16;
|
||||
const REACTION_MESSAGE_BATCH_SIZE: usize = 64;
|
||||
const ATTACHMENT_DECAY_BATCH_SIZE: usize = 128;
|
||||
const BUCKET_INDEX_PAGE_SIZE: u32 = 200;
|
||||
const JS_MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
|
||||
|
||||
fn assert_safe_byte_size(value: i64) -> i64 {
|
||||
assert!(
|
||||
(0..=JS_MAX_SAFE_INTEGER).contains(&value),
|
||||
"attachment size must fit a non-negative JavaScript safe integer"
|
||||
);
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(feature = "scylla")]
|
||||
const MESSAGE_COLUMNS: &str = "\
|
||||
@@ -1232,7 +1241,7 @@ impl MessagesShard {
|
||||
description: attachment.description.clone(),
|
||||
content_type: Some(content_type),
|
||||
content_hash: attachment.content_hash.clone(),
|
||||
size: attachment.size.unwrap_or_default(),
|
||||
size: assert_safe_byte_size(attachment.size.unwrap_or_default()),
|
||||
url: (!expired).then_some(url.clone()),
|
||||
proxy_url: (!expired).then_some(url),
|
||||
width: (!is_audio).then_some(attachment.width).flatten(),
|
||||
|
||||
@@ -17,6 +17,13 @@ export const Int32TypeSchema: OpenAPISchema = {
|
||||
format: 'int32',
|
||||
};
|
||||
export const Int32TypeRef: OpenAPIRef = {$ref: '#/components/schemas/Int32Type'};
|
||||
export const NonNegativeSafeIntegerTypeSchema: OpenAPISchema = {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
format: 'int53',
|
||||
};
|
||||
export const NonNegativeSafeIntegerTypeRef: OpenAPIRef = {$ref: '#/components/schemas/NonNegativeSafeIntegerType'};
|
||||
export const Int64TypeSchema: OpenAPISchema = {
|
||||
type: 'string',
|
||||
format: 'int64',
|
||||
|
||||
@@ -371,7 +371,13 @@ export function zodToOpenAPISchema(schema: ZodTypeAny, depth = 0): OpenAPISchema
|
||||
const min = result.minimum ?? result.exclusiveMinimum;
|
||||
const max = result.maximum ?? result.exclusiveMaximum;
|
||||
if (min != null && max != null) {
|
||||
result.format = min >= -2147483648 && max <= 2147483647 ? 'int32' : 'int64';
|
||||
if (min >= -2147483648 && max <= 2147483647) {
|
||||
result.format = 'int32';
|
||||
} else if (min >= Number.MIN_SAFE_INTEGER && max <= Number.MAX_SAFE_INTEGER) {
|
||||
result.format = 'int53';
|
||||
} else {
|
||||
result.format = 'int64';
|
||||
}
|
||||
} else if (min == null && max == null) {
|
||||
result.format = 'int53';
|
||||
} else {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Int64StringTypeRef,
|
||||
Int64TypeRef,
|
||||
LocaleRef,
|
||||
NonNegativeSafeIntegerTypeRef,
|
||||
PasswordTypeRef,
|
||||
PhoneNumberTypeRef,
|
||||
SnowflakeTypeRef,
|
||||
@@ -51,6 +52,8 @@ function getRefForCustomTypeName(typeName: string): OpenAPISchemaOrRef | null {
|
||||
return SnowflakeTypeRef;
|
||||
case 'Int32Type':
|
||||
return Int32TypeRef;
|
||||
case 'NonNegativeSafeIntegerType':
|
||||
return NonNegativeSafeIntegerTypeRef;
|
||||
case 'Int64Type':
|
||||
return Int64TypeRef;
|
||||
case 'Int64StringType':
|
||||
@@ -217,6 +220,7 @@ const FLUXER_CUSTOM_TYPES: Record<string, OpenAPISchema> = {
|
||||
BitflagStringType: {type: 'string', format: 'int64', pattern: '^[0-9]+$'},
|
||||
ColorType: {type: 'integer', minimum: 0, maximum: 16777215, format: 'int32'},
|
||||
Int32Type: {type: 'integer', minimum: 0, maximum: 2147483647, format: 'int32'},
|
||||
NonNegativeSafeIntegerType: {type: 'integer', minimum: 0, maximum: 9007199254740991, format: 'int53'},
|
||||
EmailType: {type: 'string', format: 'email'},
|
||||
PasswordType: {type: 'string', minLength: 8, maxLength: 256},
|
||||
UsernameType: {type: 'string', minLength: 1, maxLength: 32, pattern: '^[a-zA-Z0-9_]+$'},
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Int64StringTypeSchema,
|
||||
Int64TypeSchema,
|
||||
LocaleSchema,
|
||||
NonNegativeSafeIntegerTypeSchema,
|
||||
PasswordTypeSchema,
|
||||
PhoneNumberTypeSchema,
|
||||
SnowflakeTypeSchema,
|
||||
@@ -127,6 +128,7 @@ const BUILT_IN_SCHEMAS: ReadonlyArray<readonly [string, OpenAPISchema]> = [
|
||||
['APIErrorCode', APIErrorCodeSchema],
|
||||
['SnowflakeType', SnowflakeTypeSchema],
|
||||
['Int32Type', Int32TypeSchema],
|
||||
['NonNegativeSafeIntegerType', NonNegativeSafeIntegerTypeSchema],
|
||||
['Int64Type', Int64TypeSchema],
|
||||
['Int64StringType', Int64StringTypeSchema],
|
||||
['UnsignedInt64Type', UnsignedInt64TypeSchema],
|
||||
|
||||
@@ -253,6 +253,9 @@ export class OpenAPIOperationBuilder {
|
||||
if (typeString.includes('UnsignedInt64Type')) {
|
||||
return {type: 'string', format: 'int64', pattern: '^[0-9]+$'};
|
||||
}
|
||||
if (typeString.includes('NonNegativeSafeIntegerType')) {
|
||||
return {type: 'integer', minimum: 0, maximum: 9007199254740991, format: 'int53'};
|
||||
}
|
||||
if (typeString.includes('Int64StringType')) {
|
||||
return {type: 'string', format: 'int64', pattern: '^-?[0-9]+$'};
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
createStringType,
|
||||
Int32Type,
|
||||
Int64StringType,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeStringType,
|
||||
SnowflakeType,
|
||||
withOpenApiType,
|
||||
@@ -800,7 +801,7 @@ const LimitRuleSchema = z.object({
|
||||
id: z.string().min(1).describe('Unique rule identifier'),
|
||||
filters: LimitFilterSchema.optional().describe('Optional filters that scope the rule'),
|
||||
limits: z
|
||||
.record(z.string(), z.number().min(0))
|
||||
.record(z.string(), NonNegativeSafeIntegerType)
|
||||
.refine(
|
||||
(limits) => {
|
||||
const limitKeys = Object.keys(limits);
|
||||
@@ -1258,7 +1259,7 @@ const AdminMessageAttachmentSchema = z.object({
|
||||
content_type: z.string().nullable(),
|
||||
width: Int32Type.nullable(),
|
||||
height: Int32Type.nullable(),
|
||||
size: Int32Type.nullable().optional(),
|
||||
size: NonNegativeSafeIntegerType.nullable().optional(),
|
||||
ncmec_status: NcmecSubmissionStatusEnum,
|
||||
ncmec_report_id: createStringType(1, 256).nullable(),
|
||||
ncmec_failure_reason: createStringType(1, 4000).nullable(),
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {GifMediaFormat} from '@fluxer/schema/src/domains/gif/GifSchemas';
|
||||
import {createStringType, SnowflakeStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {
|
||||
createStringType,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeStringType,
|
||||
SnowflakeType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
const FavoriteMemeBase = z.object({
|
||||
@@ -67,7 +72,7 @@ export const FavoriteMemeResponse = z.object({
|
||||
filename: z.string().describe('Original filename of the meme'),
|
||||
content_type: z.string().describe('MIME type of the meme file'),
|
||||
content_hash: z.string().nullish().describe('Hash of the file content for deduplication'),
|
||||
size: z.number().describe('File size in bytes'),
|
||||
size: NonNegativeSafeIntegerType.describe('File size in bytes'),
|
||||
width: z.number().int().nullish().describe('Width of the image or video in pixels'),
|
||||
height: z.number().int().nullish().describe('Height of the image or video in pixels'),
|
||||
duration: z.number().nullish().describe('Duration of the video in seconds'),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createBitflagInt32Type,
|
||||
createStringType,
|
||||
Int32Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
@@ -41,7 +42,7 @@ export const ClientUploadedAttachmentRequest = ClientAttachmentBase.extend({
|
||||
upload_filename: createStringType(1, 4096).describe(
|
||||
'Temporary upload key returned by the attachment upload endpoint',
|
||||
),
|
||||
file_size: coerceNumberFromString(Int32Type).describe('Uploaded file size in bytes'),
|
||||
file_size: coerceNumberFromString(NonNegativeSafeIntegerType).describe('Uploaded file size in bytes'),
|
||||
content_type: createStringType(1, 255).describe('MIME type of the uploaded file'),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
import {ATTACHMENT_UPLOAD_MAX_CHUNKS, MAX_ATTACHMENTS_PER_MESSAGE} from '@fluxer/constants/src/LimitConstants';
|
||||
import {FilenameType} from '@fluxer/schema/src/primitives/FileValidators';
|
||||
import {coerceNumberFromString, createStringType, Int32Type} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {
|
||||
coerceNumberFromString,
|
||||
createStringType,
|
||||
Int32Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {URLType} from '@fluxer/schema/src/primitives/UrlValidators';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const PresignedAttachmentUploadRequestItem = z.object({
|
||||
id: coerceNumberFromString(Int32Type).describe('The client-side identifier for this attachment'),
|
||||
filename: FilenameType.describe('The name of the file that will be uploaded'),
|
||||
file_size: coerceNumberFromString(Int32Type).describe('Expected file size in bytes'),
|
||||
file_size: coerceNumberFromString(NonNegativeSafeIntegerType).describe('Expected file size in bytes'),
|
||||
content_type: createStringType(1, 255).describe('MIME type the client will upload'),
|
||||
});
|
||||
|
||||
@@ -29,7 +34,7 @@ const PresignedAttachmentUploadBase = z.object({
|
||||
id: coerceNumberFromString(Int32Type).describe('The client-side identifier for this attachment'),
|
||||
filename: FilenameType.describe('The original filename for this upload'),
|
||||
upload_filename: createStringType(1, 4096).describe('Temporary upload key to reference in message send payloads'),
|
||||
file_size: coerceNumberFromString(Int32Type).describe('Expected file size in bytes'),
|
||||
file_size: coerceNumberFromString(NonNegativeSafeIntegerType).describe('Expected file size in bytes'),
|
||||
content_type: createStringType(1, 255).describe('Expected MIME type for this upload'),
|
||||
});
|
||||
const PresignedAttachmentUploadSinglepart = PresignedAttachmentUploadBase.extend({
|
||||
@@ -45,7 +50,7 @@ const PresignedAttachmentUploadPart = z.object({
|
||||
const PresignedAttachmentUploadMultipart = PresignedAttachmentUploadBase.extend({
|
||||
upload_mode: z.literal('multipart'),
|
||||
upload_id: createStringType(1, 1024).describe('S3 multipart upload identifier; required to complete the upload'),
|
||||
part_size: coerceNumberFromString(Int32Type).describe('Size in bytes of each part except the last'),
|
||||
part_size: coerceNumberFromString(NonNegativeSafeIntegerType).describe('Size in bytes of each part except the last'),
|
||||
parts: z
|
||||
.array(PresignedAttachmentUploadPart)
|
||||
.min(1)
|
||||
|
||||
@@ -11,7 +11,12 @@ import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMember
|
||||
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 {
|
||||
createBitflagInt32Type,
|
||||
Int32Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeStringType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const MessageAttachmentResponse = z.object({
|
||||
@@ -21,7 +26,7 @@ export const MessageAttachmentResponse = z.object({
|
||||
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'),
|
||||
size: NonNegativeSafeIntegerType.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)'),
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
PresignedAttachmentUploadRequest,
|
||||
PresignedAttachmentUploadResponse,
|
||||
} from '@fluxer/schema/src/domains/message/AttachmentUploadSchemas';
|
||||
import {ClientUploadedAttachmentRequest} from '@fluxer/schema/src/domains/message/AttachmentSchemas';
|
||||
import {MessageAttachmentResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('attachment byte size schemas', () => {
|
||||
it('accepts upload request file sizes above int32', () => {
|
||||
const result = PresignedAttachmentUploadRequest.safeParse({
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: 'large.bin',
|
||||
file_size: 2147483648,
|
||||
content_type: 'application/octet-stream',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts upload response file and part sizes above int32', () => {
|
||||
const result = PresignedAttachmentUploadResponse.safeParse({
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: 'large.bin',
|
||||
upload_filename: 'uploads/tmp-large',
|
||||
file_size: 2147483648,
|
||||
content_type: 'application/octet-stream',
|
||||
upload_mode: 'multipart',
|
||||
upload_id: 'multipart-id',
|
||||
part_size: 2147483648,
|
||||
parts: [{part_number: 1, upload_url: 'https://uploads.example.test/part'}],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts completed upload file sizes up to the JavaScript safe integer limit', () => {
|
||||
const result = ClientUploadedAttachmentRequest.safeParse({
|
||||
id: 0,
|
||||
filename: 'max.bin',
|
||||
upload_filename: 'uploads/tmp-max',
|
||||
file_size: Number.MAX_SAFE_INTEGER,
|
||||
content_type: 'application/octet-stream',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects unsafe upload file sizes', () => {
|
||||
const result = ClientUploadedAttachmentRequest.safeParse({
|
||||
id: 0,
|
||||
filename: 'unsafe.bin',
|
||||
upload_filename: 'uploads/tmp-unsafe',
|
||||
file_size: Number.MAX_SAFE_INTEGER + 1,
|
||||
content_type: 'application/octet-stream',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('emits message attachment sizes as safe integer numbers', () => {
|
||||
const result = MessageAttachmentResponse.safeParse({
|
||||
id: '123456789012345678',
|
||||
filename: 'large.bin',
|
||||
title: null,
|
||||
description: null,
|
||||
content_type: 'application/octet-stream',
|
||||
content_hash: null,
|
||||
size: Number.MAX_SAFE_INTEGER,
|
||||
url: 'https://cdn.example.test/large.bin',
|
||||
proxy_url: 'https://cdn.example.test/large.bin',
|
||||
width: null,
|
||||
height: null,
|
||||
placeholder: null,
|
||||
flags: 0,
|
||||
nsfw: null,
|
||||
duration: null,
|
||||
waveform: null,
|
||||
expires_at: null,
|
||||
expired: null,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createStringType,
|
||||
createUnboundedStringType,
|
||||
Int32Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {URLType} from '@fluxer/schema/src/primitives/UrlValidators';
|
||||
@@ -68,7 +69,7 @@ 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'),
|
||||
size: z.number().int().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'),
|
||||
proxy_url: URLType.optional().describe('Proxied URL of the attachment'),
|
||||
height: z.number().int().optional().describe('Height of the image/video in pixels'),
|
||||
|
||||
@@ -171,6 +171,12 @@ export const ColorType = z
|
||||
.describe('fluxer:ColorType');
|
||||
export const Int32Type = z.number().int().min(0).max(2147483647).describe('fluxer:Int32Type');
|
||||
export const SignedInt32Type = z.number().int().min(-2147483648).max(2147483647).describe('fluxer:SignedInt32Type');
|
||||
export const NonNegativeSafeIntegerType = z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(Number.MAX_SAFE_INTEGER)
|
||||
.describe('fluxer:NonNegativeSafeIntegerType');
|
||||
const INTEGER_STRING_REGEX = /^[+-]?\d+$/;
|
||||
|
||||
function coerceNumericStringToNumber(value: unknown): unknown {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Int32Type,
|
||||
Int64StringType,
|
||||
Int64Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
normalizeString,
|
||||
normalizeWhitespace,
|
||||
removeStandaloneSurrogates,
|
||||
@@ -214,6 +215,29 @@ describe('Int32Type', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('NonNegativeSafeIntegerType', () => {
|
||||
it('accepts maximum JavaScript safe integer', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(Number.MAX_SAFE_INTEGER);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
it('accepts values above int32', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(2147483648);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
it('rejects unsafe JavaScript integers', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(Number.MAX_SAFE_INTEGER + 1);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
it('rejects negative values', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(-1);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
it('rejects non-integer values', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(1.5);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('coerceNumberFromString', () => {
|
||||
it('coerces valid integer strings to numbers', () => {
|
||||
const schema = coerceNumberFromString(z.number().int().min(0).max(100));
|
||||
|
||||
Reference in New Issue
Block a user