From d616cfc426bdec697dbc0324e2da2d3a7383a8c5 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 25 Jun 2026 12:52:52 -0400 Subject: [PATCH] chore(protos): update flipcash protobuf definitions Migrate media model from MediaId/MediaMetadata to blob-based MediaItemRendition with role-typed renditions (ORIGINAL, DISPLAY, THUMBNAIL) backed by BlobId/BlobMetadata. Signed-off-by: Brandon McAnsh --- .../protos/src/main/proto/blob/v1/model.proto | 60 ++++++++++++++++++ .../src/main/proto/messaging/v1/model.proto | 63 +++++++++---------- .../network/extensions/LocalToProtobuf.kt | 21 ++++++- .../network/extensions/ProtobufToLocal.kt | 38 ++++++++--- .../models/chat/{MediaId.kt => BlobId.kt} | 2 +- .../services/models/chat/BlobMetadata.kt | 11 ++++ .../{MediaMetadata.kt => ImageMetadata.kt} | 5 +- .../services/models/chat/MediaItem.kt | 3 +- .../models/chat/MediaItemRendition.kt | 17 +++++ 9 files changed, 173 insertions(+), 47 deletions(-) create mode 100644 definitions/flipcash/protos/src/main/proto/blob/v1/model.proto rename services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/{MediaId.kt => BlobId.kt} (72%) create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/BlobMetadata.kt rename services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/{MediaMetadata.kt => ImageMetadata.kt} (62%) create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaItemRendition.kt diff --git a/definitions/flipcash/protos/src/main/proto/blob/v1/model.proto b/definitions/flipcash/protos/src/main/proto/blob/v1/model.proto new file mode 100644 index 000000000..8203029b2 --- /dev/null +++ b/definitions/flipcash/protos/src/main/proto/blob/v1/model.proto @@ -0,0 +1,60 @@ +syntax = "proto3"; + +package flipcash.blob.v1; + +option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/blob/v1;blobpb"; +option java_package = "com.codeinc.flipcash.gen.blob.v1"; +option objc_class_prefix = "FPBBlobV1"; + +import "validate/validate.proto"; + +// Opaque, client-held handle to a stored blob. This is the durable identity for +// the bytes; the bytes it points at are immutable once the upload is finalized. +message BlobId { + bytes value = 1 [(validate.rules).bytes = { + min_len: 16 + max_len: 16 + }]; +} + +// Server-authoritative metadata describing a stored blob. Never set by clients. +// With the exception of download_url, every field is intrinsic to the stored +// bytes and immutable, derived once by the server. +message BlobMetadata { + // MIME type (e.g. "image/jpeg"). + string mime_type = 1 [(validate.rules).string = { + min_len: 1 + max_len: 255 + }]; + + // Total size of the blob in bytes. + uint64 size_bytes = 2 [(validate.rules).uint64.gte = 1]; + + // Ephemeral, server-minted URL for fetching the blob bytes. Unlike the + // other fields it is NOT intrinsic to the blob: it is re-issued on every + // fetch, may expire (signed URL with a short TTL), and is authorized at + // mint time, not at fetch time. Clients MUST NOT persist or cache it across + // fetches; treat the BlobId as the durable handle and this as disposable. + string download_url = 3 [(validate.rules).string = { + uri: true + max_len: 2048 + }]; + + // Kind-specific metadata the server derived from the bytes. Exactly one + // variant is set for a recognized media kind; left unset for opaque blobs. + // Only images are supported today; video/audio/etc. will be added as new + // variants. + oneof kind { + ImageMetadata image = 4; + } +} + +// Intrinsic descriptors for a still image. +message ImageMetadata { + // Pixel dimensions, for reserving layout before the bytes arrive. + uint32 width = 1 [(validate.rules).uint32.gte = 1]; + uint32 height = 2 [(validate.rules).uint32.gte = 1]; + + // Compact preview shown while the full image downloads (BlurHash string). + string blurhash = 3 [(validate.rules).string.max_len = 64]; +} diff --git a/definitions/flipcash/protos/src/main/proto/messaging/v1/model.proto b/definitions/flipcash/protos/src/main/proto/messaging/v1/model.proto index 9ba8b88b5..7ba3a4418 100644 --- a/definitions/flipcash/protos/src/main/proto/messaging/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/messaging/v1/model.proto @@ -6,6 +6,7 @@ option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/g option java_package = "com.codeinc.flipcash.gen.messaging.v1"; option objc_class_prefix = "FPBMessagingV1"; +import "blob/v1/model.proto"; import "common/v1/common.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -140,9 +141,12 @@ message ReplyContent { }]; } -// Media content (images, video, etc.) +// Media content from blobs the user has already uploaded. The following media +// types are supported: +// - Images message MediaContent { - // The media items attached to this message + // The media items attached to this message. A single item today; raising + // this cap later enables albums (each item self-describes its kind). repeated MediaItem items = 1 [(validate.rules).repeated = { min_items: 1 max_items: 1 @@ -152,43 +156,38 @@ message MediaContent { TextContent caption = 2; } +// One logical media item, carried as its set of renditions (quality/size variants). message MediaItem { - // Client-provided reference to media already uploaded out-of-band - MediaId media_id = 1 [(validate.rules).message.required = true]; - - // Server-authoritative metadata, resolved from the upload record. It is - // omitted on SendMessage and populated on stored/returned messages - MediaMetadata metadata = 2; -} - -message MediaId { - bytes value = 1 [(validate.rules).bytes = { - min_len: 16 - max_len: 16 + // The renditions of this media, each an independently-stored blob. On + // SendMessage the client supplies exactly one ORIGINAL rendition (its + // blob_id); the server fills metadata and appends any derived renditions + // (e.g. a downscaled DISPLAY and a THUMBNAIL). + repeated MediaItemRendition renditions = 1 [(validate.rules).repeated = { + min_items: 1 }]; } -// Server-authoritative metadata describing an uploaded media item. Never set by -// clients; the server derives every field from the uploaded bytes. -message MediaMetadata { - // MIME type (e.g. "image/jpeg", "video/mp4") - string mime_type = 1 [(validate.rules).string = { - min_len: 1 - max_len: 255 +// A single stored variant of a MediaItem +message MediaItemRendition { + // The intended use of this rendition within the item. + Role role = 1 [(validate.rules).enum = { + not_in: [0] }]; + enum Role { + UNKNOWN = 0; + ORIGINAL = 1; // full-quality source the client uploaded + DISPLAY = 2; // downscaled/compressed for inline display + THUMBNAIL = 3; // tiny grid preview + } - // Total size of the media in bytes. - uint64 size_bytes = 2 [(validate.rules).uint64.gte = 1]; - - // Pixel dimensions, for reserving layout before the bytes arrive. - uint32 width = 3 [(validate.rules).uint32.gte = 1]; - uint32 height = 4 [(validate.rules).uint32.gte = 1]; - - // Compact preview shown while the full media downloads (BlurHash string). - string blurhash = 5 [(validate.rules).string.max_len = 64]; + // Handle to the blob holding this rendition's bytes. Client-set on the + // ORIGINAL at send time; server-set for derived renditions. + blob.v1.BlobId blob_id = 2 [(validate.rules).message.required = true]; - // Duration in milliseconds for audio/video; 0 for stills. - uint64 duration_ms = 6; + // Server-authoritative blob metadata (mime type, size, download URL, and + // the image dimensions/preview), resolved from the blob record. Omitted on + // SendMessage and populated on stored/returned messages. + blob.v1.BlobMetadata blob = 3; } // System message content diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt index 6051adeb9..9528fdcb5 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt @@ -151,10 +151,29 @@ internal fun MessageContent.asContent(): MessagingModel.Content { internal fun com.flipcash.services.models.chat.MediaItem.asMediaItem(): MessagingModel.MediaItem { return MessagingModel.MediaItem.newBuilder() - .setMediaId(MessagingModel.MediaId.newBuilder().setValue(mediaId.bytes.toByteString())) + .addAllRenditions(renditions.map { it.asRendition() }) .build() } +internal fun com.flipcash.services.models.chat.MediaItemRendition.asRendition(): MessagingModel.MediaItemRendition { + return MessagingModel.MediaItemRendition.newBuilder() + .setRole(role.asProtoRole()) + .setBlobId( + com.codeinc.flipcash.gen.blob.v1.Model.BlobId.newBuilder() + .setValue(blobId.bytes.toByteString()) + ) + .build() +} + +internal fun com.flipcash.services.models.chat.MediaItemRendition.Role.asProtoRole(): MessagingModel.MediaItemRendition.Role { + return when (this) { + com.flipcash.services.models.chat.MediaItemRendition.Role.ORIGINAL -> MessagingModel.MediaItemRendition.Role.ORIGINAL + com.flipcash.services.models.chat.MediaItemRendition.Role.DISPLAY -> MessagingModel.MediaItemRendition.Role.DISPLAY + com.flipcash.services.models.chat.MediaItemRendition.Role.THUMBNAIL -> MessagingModel.MediaItemRendition.Role.THUMBNAIL + com.flipcash.services.models.chat.MediaItemRendition.Role.UNKNOWN -> MessagingModel.MediaItemRendition.Role.UNKNOWN + } +} + internal fun com.flipcash.services.models.chat.Emoji.asEmoji(): MessagingModel.Emoji { return MessagingModel.Emoji.newBuilder().setValue(value).build() } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt index 8cc99c47b..39f0d6b32 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt @@ -23,9 +23,11 @@ import com.flipcash.services.models.chat.ChatType import com.flipcash.services.models.chat.ChatUpdate import com.flipcash.services.models.chat.Emoji import com.flipcash.services.models.chat.EmojiReaction -import com.flipcash.services.models.chat.MediaId +import com.flipcash.services.models.chat.BlobId +import com.flipcash.services.models.chat.BlobMetadata +import com.flipcash.services.models.chat.ImageMetadata import com.flipcash.services.models.chat.MediaItem -import com.flipcash.services.models.chat.MediaMetadata +import com.flipcash.services.models.chat.MediaItemRendition import com.flipcash.services.models.chat.MessageContent import com.flipcash.services.models.chat.MessagePointer import com.flipcash.services.models.chat.MetadataUpdate @@ -145,19 +147,41 @@ internal fun MessagingModel.Content.toMessageContent(): MessageContent { internal fun MessagingModel.MediaItem.toMediaItem(): MediaItem { return MediaItem( - mediaId = MediaId(mediaId.value.toByteArray()), - metadata = if (hasMetadata()) metadata.toMediaMetadata() else null, + renditions = renditionsList.map { it.toMediaItemRendition() }, ) } -internal fun MessagingModel.MediaMetadata.toMediaMetadata(): MediaMetadata { - return MediaMetadata( +internal fun MessagingModel.MediaItemRendition.toMediaItemRendition(): MediaItemRendition { + return MediaItemRendition( + role = role.toRole(), + blobId = BlobId(blobId.value.toByteArray()), + blob = if (hasBlob()) blob.toBlobMetadata() else null, + ) +} + +internal fun MessagingModel.MediaItemRendition.Role.toRole(): MediaItemRendition.Role { + return when (this) { + MessagingModel.MediaItemRendition.Role.ORIGINAL -> MediaItemRendition.Role.ORIGINAL + MessagingModel.MediaItemRendition.Role.DISPLAY -> MediaItemRendition.Role.DISPLAY + MessagingModel.MediaItemRendition.Role.THUMBNAIL -> MediaItemRendition.Role.THUMBNAIL + else -> MediaItemRendition.Role.UNKNOWN + } +} + +internal fun com.codeinc.flipcash.gen.blob.v1.Model.BlobMetadata.toBlobMetadata(): BlobMetadata { + return BlobMetadata( mimeType = mimeType, sizeBytes = sizeBytes, + downloadUrl = downloadUrl, + image = if (hasImage()) image.toImageMetadata() else null, + ) +} + +internal fun com.codeinc.flipcash.gen.blob.v1.Model.ImageMetadata.toImageMetadata(): ImageMetadata { + return ImageMetadata( width = width, height = height, blurhash = blurhash, - durationMs = durationMs, ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaId.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/BlobId.kt similarity index 72% rename from services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaId.kt rename to services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/BlobId.kt index 9f7f43805..ac6987e4e 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaId.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/BlobId.kt @@ -4,4 +4,4 @@ import kotlinx.serialization.Serializable @Serializable @JvmInline -value class MediaId(val bytes: ByteArray) +value class BlobId(val bytes: ByteArray) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/BlobMetadata.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/BlobMetadata.kt new file mode 100644 index 000000000..63e6ca02a --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/BlobMetadata.kt @@ -0,0 +1,11 @@ +package com.flipcash.services.models.chat + +import kotlinx.serialization.Serializable + +@Serializable +data class BlobMetadata( + val mimeType: String, + val sizeBytes: Long, + val downloadUrl: String, + val image: ImageMetadata?, +) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaMetadata.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ImageMetadata.kt similarity index 62% rename from services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaMetadata.kt rename to services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ImageMetadata.kt index 359aebb39..61e48a6f2 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaMetadata.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ImageMetadata.kt @@ -3,11 +3,8 @@ package com.flipcash.services.models.chat import kotlinx.serialization.Serializable @Serializable -data class MediaMetadata( - val mimeType: String, - val sizeBytes: Long, +data class ImageMetadata( val width: Int, val height: Int, val blurhash: String, - val durationMs: Long, ) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaItem.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaItem.kt index 89b2fd7b6..8194552e5 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaItem.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaItem.kt @@ -4,6 +4,5 @@ import kotlinx.serialization.Serializable @Serializable data class MediaItem( - val mediaId: MediaId, - val metadata: MediaMetadata?, + val renditions: List, ) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaItemRendition.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaItemRendition.kt new file mode 100644 index 000000000..81805edce --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MediaItemRendition.kt @@ -0,0 +1,17 @@ +package com.flipcash.services.models.chat + +import kotlinx.serialization.Serializable + +@Serializable +data class MediaItemRendition( + val role: Role, + val blobId: BlobId, + val blob: BlobMetadata?, +) { + enum class Role { + UNKNOWN, + ORIGINAL, + DISPLAY, + THUMBNAIL, + } +}