From 27cad2e29179b042422b3e5f9d6b6ad1e810d340 Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:56:52 -0800 Subject: [PATCH 1/2] first pass --- .../inboxes/content-types/attachments.mdx | 237 +++++++++++++++++- 1 file changed, 228 insertions(+), 9 deletions(-) diff --git a/docs/pages/inboxes/content-types/attachments.mdx b/docs/pages/inboxes/content-types/attachments.mdx index 6dd4fcd..38cd989 100644 --- a/docs/pages/inboxes/content-types/attachments.mdx +++ b/docs/pages/inboxes/content-types/attachments.mdx @@ -1,27 +1,31 @@ --- -description: Learn how to use the attachment and remote attachment content types to support attachments in your app built with XMTP +description: Learn how to use the remote attachment, multiple remote attachment, or attachment content types to support attachments in your app built with XMTP --- # Support attachments in your app built with XMTP -Use the remote attachment or attachment content type to support attachments in your app. +Use the remote attachment, multiple remote attachments, or attachment content type to support attachments in your app. -- Use the [remote attachment content type](#support-remote-attachments-of-any-size) to send attachments of any size. +- Use the [remote attachment content type](#support-remote-attachments-of-any-size) to send one remote attachment of any size. + +- Use the [multiple remote attachments content type](#support-multiple-remote-attachments-of-any-size-in-a-message) to send multiple remote attachments of any size. - Use the [attachment content type](#support-attachments-smaller-than-1mb) to send attachments smaller than 1MB. ## Support remote attachments of any size -Remote attachments of any size can be sent using the `RemoteAttachmentCodec` and a storage provider. +One remote attachment of any size can be sent in a message using the `RemoteAttachmentCodec` and a storage provider. + +To send multiple remote attachments of any size in a single message, see [Support multiple remote attachments of any size in a message](#support-multiple-remote-attachments-of-any-size-in-a-message). ### Install the package +In some SDKs, the `AttachmentCodec` is already included in the SDK. If not, you can install the package using the following command: + ```bash npm i @xmtp/content-type-remote-attachment ``` -> In some SDKs, the `AttachmentCodec` is already included in the SDK. If not, you can install the package using the following command: - ### Configure the content type After importing the package, you can register the codec. @@ -110,7 +114,7 @@ const encryptedEncoded = await RemoteAttachmentCodec.encodeEncrypted( ); ``` -Upload an encrypted attachment to a location where it will be accessible via an HTTPS GET request. This location will depend on which storage provider you use based on your needs. For example, the [xmtp.chat](https://xmtp.chat/) example app uses web3.storage. (This information is shared for educational purposes only and is not an endorsement.) +Upload an encrypted attachment to a location where it will be accessible via an HTTPS GET request. This location will depend on which storage provider you use based on your needs.

Now that you have a `url`, you can create a `RemoteAttachment`:

@@ -280,7 +284,7 @@ try await conversation.send( ### Receive, decode, and decrypt a remote attachment -Now that you can receive a remote attachment, you need a way to receive a remote attachment. For example: +Now that you can send a remote attachment, you need a way to receive it. For example: :::code-group @@ -386,6 +390,221 @@ struct ContentView: View { To handle unsupported content types, refer to the [fallback](/inboxes/build-inbox/#handle-unsupported-content-types) section. +## Support multiple remote attachments of any size + +Multiple remote attachments of any size can be sent in a single message using the `MultiRemoteAttachmentCodec` and a storage provider. + +### Create multiple attachment objects + +Each attachment in the attachments array contains a URL that points to an encrypted `EncodedContent` object. The content must be accessible by an HTTP `GET` request to the URL. + +:::code-group + +```ts [Browser] +code sample needed +``` + +```ts [Node] +code sample needed +``` + +```ts [React Native] +const attachment1: DecryptedLocalAttachment = { + fileUri: "content://media/external/images/media/image-1.png", + mimeType: "image/png", + filename: "image-1.png" +} + +const attachment2: DecryptedLocalAttachment = { + fileUri: "content://media/external/images/media/image-2.png", + mimeType: "image/png", + filename: "image-2.png" +} +``` + +```kotlin [Kotlin] +code sample needed +``` + +```swift [Swift] +code sample needed +``` + +::: + +### Encrypt and upload multiple attachments to a remote server + +:::code-group + +```ts [Browser] +code sample needed +``` + +```ts [Node] +code sample needed +``` + +```ts [React Native] +const remoteAttachments: RemoteAttachmentInfo[] = [] + for (const attachment of [attachment1, attachment2]) { + // Encrypt the attachment and receive the local URI of the encrypted file + const { encryptedLocalFileUri, metadata } = await alix.encryptAttachment(attachment) + + // Upload the attachment to a remote server and receive the URL + const url = testUploadAttachmentForUrl(encryptedLocalFileUri) + + // Build the remote attachment info + const remoteAttachmentInfo = + MultiRemoteAttachmentCodec.buildMultiRemoteAttachmentInfo(url, metadata) + remoteAttachments.push(remoteAttachmentInfo) + } +``` + +```kotlin [Kotlin] +code sample needed +``` + +```swift [Swift] +code sample needed +``` + +::: + +### Send a message with multiple remote attachments + +:::code-group + +```ts [Browser] +code sample needed +``` + +```ts [Node] +code sample needed +``` + +```ts [React Native] +await convo.send({ + multiRemoteAttachment: { + attachments: remoteAttachments, + }, + }) +``` + +```kotlin [Kotlin] +code sample needed +``` + +```swift [Swift] +code sample needed +``` + +::: + +### Decode a message with multiple remote attachments + +:::code-group + +```ts [Browser] +code sample needed +``` + +```ts [Node] +code sample needed +``` + +```ts [React Native] +const message = (await group.messages()).first() +if (message.contentTypeId == 'xmtp.org/multiRemoteStaticContent:1.0') { + const multiRemoteAttachment: MultiRemoteAttachment = message.content() +} +``` + +```kotlin [Kotlin] +code sample needed +``` + +```swift [Swift] +code sample needed +``` + +::: + +### Download and decrypt the attachments + +:::code-group + +```ts [Browser] +code sample needed +``` + +```ts [Node] +code sample needed +``` + +```ts [React Native] +const decryptedAttachments: DecryptedLocalAttachment[] = [] +for (const attachment of multiRemoteAttachment.attachments) { + // Downloading the encrypted payload from the attachment URL and save the local file + const encryptedFileLocalURIAfterDownload: string = downloadFromUrlForLocalUri( + attachment.url + ) + // Decrypt the local file + const decryptedLocalAttachment = await alix.decryptAttachment({ + encryptedLocalFileUri: encryptedFileLocalURIAfterDownload, + metadata: { + secret: attachment.secret, + salt: attachment.salt, + nonce: attachment.nonce, + contentDigest: attachment.contentDigest, + filename: attachment.filename, + } as RemoteAttachmentContent, + }) + decryptedAttachments.push(decryptedLocalAttachment) + } +``` + +```kotlin [Kotlin] +code sample needed +``` + +```swift [Swift] +code sample needed +``` + +::: + +### Display the attachments + +Use the file URIs in the decrypted attachments objects to display the attachments. + +:::code-group + +```ts [Browser] +code sample needed +``` + +```ts [Node] +code sample needed +``` + +```ts [React Native] +const attachment1 = decryptedAttachments[0] +const attachment2 = decryptedAttachments[1] + + + +``` + +```kotlin [Kotlin] +code sample needed +``` + +```swift [Swift] +code sample needed +``` + +::: + ## Support attachments smaller than 1MB Attachments smaller than 1MB can be sent using the `AttachmentCodec`. The codec will automatically encrypt the attachment and upload it to the XMTP network. @@ -475,4 +694,4 @@ if (message.contentType.sameAs(ContentTypeAttachment)) { }); const url = URL.createObjectURL(blobdecoded); } -``` \ No newline at end of file +``` From 90d07a108a77d4dc95a0b5263b785c17cf068578 Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:42:11 -0800 Subject: [PATCH 2/2] add code samples for kotlin, swift --- .../inboxes/content-types/attachments.mdx | 161 ++++++++++++++++-- 1 file changed, 149 insertions(+), 12 deletions(-) diff --git a/docs/pages/inboxes/content-types/attachments.mdx b/docs/pages/inboxes/content-types/attachments.mdx index 38cd989..9686796 100644 --- a/docs/pages/inboxes/content-types/attachments.mdx +++ b/docs/pages/inboxes/content-types/attachments.mdx @@ -394,6 +394,32 @@ To handle unsupported content types, refer to the [fallback](/inboxes/build-inbo Multiple remote attachments of any size can be sent in a single message using the `MultiRemoteAttachmentCodec` and a storage provider. +### Register necessary codecs + +:::code-group + +```tsx [React Native] +export const registerCodecs = () => { + Client.register(new AttachmentCodec()); + Client.register(new RemoteAttachmentCodec()); + Client.register(new MultiRemoteAttachmentCodec()); +}; +``` + +```kotin [Kotlin] +Client.register(codec = AttachmentCodec()) +Client.register(codec = RemoteAttachmentCodec()) +Client.register(codec = MultiRemoteAttachmentCodec()) +``` + +```swift [Swift] +Client.register(codec: AttachmentCodec()) +Client.register(codec: RemoteAttachmentCodec()) +Client.register(codec: MultiRemoteAttachmentCodec()) +``` + +::: + ### Create multiple attachment objects Each attachment in the attachments array contains a URL that points to an encrypted `EncodedContent` object. The content must be accessible by an HTTP `GET` request to the URL. @@ -423,11 +449,31 @@ const attachment2: DecryptedLocalAttachment = { ``` ```kotlin [Kotlin] -code sample needed +val attachment1 = Attachment( + filename = "test1.txt", + mimeType = "text/plain", + data = "hello world".toByteStringUtf8(), +) + +val attachment2 = Attachment( + filename = "test2.txt", + mimeType = "text/plain", + data = "hello world".toByteStringUtf8(), +) ``` ```swift [Swift] -code sample needed +let attachment1 = Attachment( + filename: "test1.txt", + mimeType: "text/plain", + data: Data("hello world".utf8) +) + +let attachment2 = Attachment( + filename: "test2.txt", + mimeType: "text/plain", + data: Data("hello world".utf8) +) ``` ::: @@ -461,11 +507,35 @@ const remoteAttachments: RemoteAttachmentInfo[] = [] ``` ```kotlin [Kotlin] -code sample needed +val attachmentCodec = AttachmentCodec() +val remoteAttachmentInfos: MutableList = ArrayList() + +for (attachment: Attachment in listOf(attachment1, attachment2)) { + val encodedBytes = attachmentCodec.encode(attachment).toByteArray() + val encryptedAttachment = MultiRemoteAttachmentCodec.encryptBytesForLocalAttachment(encodedBytes, attachment.filename) + val url = testUploadEncryptedPayload(encryptedAttachment.payload.toByteArray()) + val remoteAttachmentInfo = MultiRemoteAttachmentCodec.buildRemoteAttachmentInfo(encryptedAttachment, URL(url)) + remoteAttachmentInfos.add(remoteAttachmentInfo) +} ``` ```swift [Swift] -code sample needed +var remoteAttachmentInfos: [MultiRemoteAttachment.RemoteAttachmentInfo] = [] + +for att in [attachment1, attachment2] { + // 1) Encode the attachment to raw bytes + let encodedBytes = try AttachmentCodec().encode(content: att).serializedData() + // 2) Encrypt the bytes locally + let encrypted = try MultiRemoteAttachmentCodec.encryptBytesForLocalAttachment(encodedBytes, filename: att.filename) + // 3) "Upload" it, and get a random https:// URL back + let urlString = fakeUpload(encrypted.payload) + // 4) Build a RemoteAttachmentInfo for that URL + let info = try MultiRemoteAttachmentCodec.buildRemoteAttachmentInfo( + encryptedAttachment: encrypted, + remoteUrl: URL(string: urlString)! + ) + remoteAttachmentInfos.append(info) +} ``` ::: @@ -491,11 +561,20 @@ await convo.send({ ``` ```kotlin [Kotlin] -code sample needed +val multiRemoteAttachment = MultiRemoteAttachment(remoteAttachments = remoteAttachmentInfos.toList()) + +runBlocking { + aliceConversation.send( + content = multiRemoteAttachment, + options = SendOptions(contentType = ContentTypeMultiRemoteAttachment), + ) +} ``` ```swift [Swift] -code sample needed +let multiRemoteAttachment = MultiRemoteAttachment(remoteAttachments: remoteAttachmentInfos) +let encodedContent = try MultiRemoteAttachmentCodec().encode(content: multiRemoteAttachment) +_ = try await alixConversation.send(encodedContent: encodedContent) ``` ::: @@ -520,11 +599,24 @@ if (message.contentTypeId == 'xmtp.org/multiRemoteStaticContent:1.0') { ``` ```kotlin [Kotlin] -code sample needed +val loadedMultiRemoteAttachment: FfiMultiRemoteAttachment = messages[0].content()!! + +val remoteAttachments = loadedMultiRemoteAttachment.attachments.map { attachment -> + RemoteAttachment( + url = URL(attachment.url), + filename = attachment.filename, + contentDigest = attachment.contentDigest, + nonce = attachment.nonce.toByteString(), + scheme = attachment.scheme, + salt = attachment.salt.toByteString(), + secret = attachment.secret.toByteString(), + contentLength = attachment.contentLength?.toInt(), + ) +} ``` ```swift [Swift] -code sample needed +let loadedMultiRemoteAttachment: MultiRemoteAttachment = try received.content() ``` ::: @@ -564,11 +656,51 @@ for (const attachment of multiRemoteAttachment.attachments) { ``` ```kotlin [Kotlin] -code sample needed +val textAttachments: MutableList = ArrayList() + +for (remoteAttachment: RemoteAttachment in remoteAttachments) { + val url = remoteAttachment.url.toString() + // Simulate Download + val encryptedPayload: ByteArray = encryptedPayloadUrls[url]!! + // Combine encrypted payload with RemoteAttachmentInfo + val encryptedAttachment: EncryptedEncodedContent = MultiRemoteAttachmentCodec.buildEncryptAttachmentResult(remoteAttachment, encryptedPayload) + // Decrypt payload + val encodedContent: EncodedContent = MultiRemoteAttachmentCodec.decryptAttachment(encryptedAttachment) + // Convert EncodedContent to Attachment + val attachment = attachmentCodec.decode(encodedContent) + textAttachments.add(attachment) +} ``` ```swift [Swift] -code sample needed +var decodedAttachments: [Attachment] = [] +for info in loadedMultiRemoteAttachment.remoteAttachments { + let remoteAttachment = try RemoteAttachment( + url: info.url, + contentDigest: info.contentDigest, + secret: info.secret, + salt: info.salt, + nonce: info.nonce, + scheme: RemoteAttachment.Scheme(rawValue: info.scheme) ?? .https, + contentLength: Int(info.contentLength), + filename: info.filename + ) + + // Download the encrypted payload + guard let downloadedPayload = encryptedPayloads[remoteAttachment.url] else { + XCTFail("No stored payload for \(remoteAttachment.url)") + return + } + + let encryptedAttachment = MultiRemoteAttachmentCodec.buildEncryptAttachmentResult( + remoteAttachment: remoteAttachment, + encryptedPayload: downloadedPayload + ) + + let decodedContent = try MultiRemoteAttachmentCodec.decryptAttachment(encryptedAttachment) + let finalAttachment: Attachment = try decodedContent.decoded() + decodedAttachments.append(finalAttachment) +} ``` ::: @@ -596,11 +728,16 @@ const attachment2 = decryptedAttachments[1] ``` ```kotlin [Kotlin] -code sample needed + assertEquals(textAttachments[0].filename, "test1.txt") + assertEquals(textAttachments[1].filename, "test2.txt") +} else { + AssertionError("expected a MultiRemoteAttachment message") +} ``` ```swift [Swift] -code sample needed +decodedAttachments[0].filename // "test1.txt" +decodedAttachments[1].filename // "test2.txt" ``` :::