fix(api): support int53 attachment sizes (#1261)

This commit is contained in:
Hampus authored and GitHub committed 2026-07-02 23:41:55 +02:00
1 parent 40d1e90a3c
commit 3a8ed1012f
22 files changed
+289 -89

No files matched your search

@@ -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,
+41 -34
View File
@@ -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,
+16
View File
@@ -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;
}