Skip to content

Commit

Permalink
Multi remote attachment content type (#1609)
Browse files Browse the repository at this point in the history
* update protos and add new multi remote attachment content type

* add multi attachment codec to bindings + encode/decode bindings test

* adds functions for encrypt + decrypt multi attachments

* simplify multi remote attachment encrypt decrypt

* remove remote content encryption / decryption; too slow over uniffi bridge

* lint fixes

* update protos

* fmt fix

* update to match protos

---------

Co-authored-by: cameronvoell <[email protected]>
  • Loading branch information
cameronvoell and cameronvoell authored Feb 13, 2025
1 parent 656e039 commit c57c5fa
Show file tree
Hide file tree
Showing 14 changed files with 2,280 additions and 1,546 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

172 changes: 168 additions & 4 deletions bindings_ffi/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use tokio::sync::Mutex;
use xmtp_api::{strategies, ApiClientWrapper};
use xmtp_api_grpc::grpc_api_helper::Client as TonicApiClient;
use xmtp_common::{AbortHandle, GenericStreamHandle, StreamHandle};
use xmtp_content_types::multi_remote_attachment::MultiRemoteAttachmentCodec;
use xmtp_content_types::reaction::ReactionCodec;
use xmtp_content_types::ContentCodec;
use xmtp_id::associations::{verify_signed_with_public_context, DeserializationError};
Expand Down Expand Up @@ -54,7 +55,9 @@ use xmtp_mls::{
};
use xmtp_proto::api_client::ApiBuilder;
use xmtp_proto::xmtp::device_sync::BackupElementSelection;
use xmtp_proto::xmtp::mls::message_contents::content_types::ReactionV2;
use xmtp_proto::xmtp::mls::message_contents::content_types::{
MultiRemoteAttachment, ReactionV2, RemoteAttachmentInfo,
};
use xmtp_proto::xmtp::mls::message_contents::{DeviceSyncKind, EncodedContent};
pub type RustXmtpClient = MlsClient<TonicApiClient>;

Expand Down Expand Up @@ -2228,6 +2231,111 @@ impl From<FfiReactionSchema> for i32 {
}
}

#[derive(uniffi::Record, Clone, Default)]
pub struct FfiRemoteAttachmentInfo {
pub secret: Vec<u8>,
pub content_digest: String,
pub nonce: Vec<u8>,
pub scheme: String,
pub url: String,
pub salt: Vec<u8>,
pub content_length: Option<u32>,
pub filename: Option<String>,
}

impl From<FfiRemoteAttachmentInfo> for RemoteAttachmentInfo {
fn from(ffi_remote_attachment_info: FfiRemoteAttachmentInfo) -> Self {
RemoteAttachmentInfo {
content_digest: ffi_remote_attachment_info.content_digest,
secret: ffi_remote_attachment_info.secret,
nonce: ffi_remote_attachment_info.nonce,
salt: ffi_remote_attachment_info.salt,
scheme: ffi_remote_attachment_info.scheme,
url: ffi_remote_attachment_info.url,
content_length: ffi_remote_attachment_info.content_length,
filename: ffi_remote_attachment_info.filename,
}
}
}

impl From<RemoteAttachmentInfo> for FfiRemoteAttachmentInfo {
fn from(remote_attachment_info: RemoteAttachmentInfo) -> Self {
FfiRemoteAttachmentInfo {
secret: remote_attachment_info.secret,
content_digest: remote_attachment_info.content_digest,
nonce: remote_attachment_info.nonce,
scheme: remote_attachment_info.scheme,
url: remote_attachment_info.url,
salt: remote_attachment_info.salt,
content_length: remote_attachment_info.content_length,
filename: remote_attachment_info.filename,
}
}
}

#[derive(uniffi::Record, Clone, Default)]
pub struct FfiMultiRemoteAttachment {
pub attachments: Vec<FfiRemoteAttachmentInfo>,
}

impl From<FfiMultiRemoteAttachment> for MultiRemoteAttachment {
fn from(ffi_multi_remote_attachment: FfiMultiRemoteAttachment) -> Self {
MultiRemoteAttachment {
attachments: ffi_multi_remote_attachment
.attachments
.into_iter()
.map(Into::into)
.collect(),
}
}
}

impl From<MultiRemoteAttachment> for FfiMultiRemoteAttachment {
fn from(multi_remote_attachment: MultiRemoteAttachment) -> Self {
FfiMultiRemoteAttachment {
attachments: multi_remote_attachment
.attachments
.into_iter()
.map(Into::into)
.collect(),
}
}
}

#[uniffi::export]
pub fn encode_multi_remote_attachment(
ffi_multi_remote_attachment: FfiMultiRemoteAttachment,
) -> Result<Vec<u8>, GenericError> {
// Convert FfiMultiRemoteAttachment to MultiRemoteAttachment
let multi_remote_attachment: MultiRemoteAttachment = ffi_multi_remote_attachment.into();

// Use MultiRemoteAttachmentCodec to encode the reaction
let encoded = MultiRemoteAttachmentCodec::encode(multi_remote_attachment)
.map_err(|e| GenericError::Generic { err: e.to_string() })?;

// Encode the EncodedContent to bytes
let mut buf = Vec::new();
encoded
.encode(&mut buf)
.map_err(|e| GenericError::Generic { err: e.to_string() })?;

Ok(buf)
}

#[uniffi::export]
pub fn decode_multi_remote_attachment(
bytes: Vec<u8>,
) -> Result<FfiMultiRemoteAttachment, GenericError> {
// Decode bytes into EncodedContent
let encoded_content = EncodedContent::decode(bytes.as_slice())
.map_err(|e| GenericError::Generic { err: e.to_string() })?;

// Use MultiRemoteAttachmentCodec to decode into MultiRemoteAttachment and convert to FfiMultiRemoteAttachment
MultiRemoteAttachmentCodec::decode(encoded_content)
.map(Into::into)
.map_err(|e| GenericError::Generic { err: e.to_string() })
}

#[derive(uniffi::Record, Clone)]
pub struct FfiMessage {
pub id: Vec<u8>,
Expand Down Expand Up @@ -2436,14 +2544,16 @@ mod tests {
FfiPreferenceUpdate, FfiXmtpClient,
};
use crate::{
connect_to_backend, decode_reaction, encode_reaction, get_inbox_id_for_address,
connect_to_backend, decode_multi_remote_attachment, decode_reaction,
encode_multi_remote_attachment, encode_reaction, get_inbox_id_for_address,
inbox_owner::SigningError, FfiConsent, FfiConsentEntityType, FfiConsentState,
FfiContentType, FfiConversation, FfiConversationCallback, FfiConversationMessageKind,
FfiCreateDMOptions, FfiCreateGroupOptions, FfiDirection, FfiGroupPermissionsOptions,
FfiInboxOwner, FfiListConversationsOptions, FfiListMessagesOptions,
FfiMessageDisappearingSettings, FfiMessageWithReactions, FfiMetadataField,
FfiPermissionPolicy, FfiPermissionPolicySet, FfiPermissionUpdateType, FfiReaction,
FfiReactionAction, FfiReactionSchema, FfiSubscribeError,
FfiMultiRemoteAttachment, FfiPermissionPolicy, FfiPermissionPolicySet,
FfiPermissionUpdateType, FfiReaction, FfiReactionAction, FfiReactionSchema,
FfiRemoteAttachmentInfo, FfiSubscribeError,
};
use ethers::utils::hex;
use prost::Message;
Expand Down Expand Up @@ -6857,4 +6967,58 @@ mod tests {
// Clean up stream
stream.end_and_wait().await.unwrap();
}

#[tokio::test]
async fn test_multi_remote_attachment_encode_decode() {
// Create a test attachment
let original_attachment = FfiMultiRemoteAttachment {
attachments: vec![
FfiRemoteAttachmentInfo {
filename: Some("test1.jpg".to_string()),
content_length: Some(1000),
secret: vec![1, 2, 3],
content_digest: "123".to_string(),
nonce: vec![7, 8, 9],
salt: vec![1, 2, 3],
scheme: "https".to_string(),
url: "https://example.com/test1.jpg".to_string(),
},
FfiRemoteAttachmentInfo {
filename: Some("test2.pdf".to_string()),
content_length: Some(2000),
secret: vec![4, 5, 6],
content_digest: "456".to_string(),
nonce: vec![10, 11, 12],
salt: vec![1, 2, 3],
scheme: "https".to_string(),
url: "https://example.com/test2.pdf".to_string(),
},
],
};

// Encode the attachment
let encoded_bytes = encode_multi_remote_attachment(original_attachment.clone())
.expect("Should encode multi remote attachment successfully");

// Decode the attachment
let decoded_attachment = decode_multi_remote_attachment(encoded_bytes)
.expect("Should decode multi remote attachment successfully");

assert_eq!(
decoded_attachment.attachments.len(),
original_attachment.attachments.len()
);

for (decoded, original) in decoded_attachment
.attachments
.iter()
.zip(original_attachment.attachments.iter())
{
assert_eq!(decoded.filename, original.filename);
assert_eq!(decoded.content_digest, original.content_digest);
assert_eq!(decoded.nonce, original.nonce);
assert_eq!(decoded.scheme, original.scheme);
assert_eq!(decoded.url, original.url);
}
}
}
6 changes: 6 additions & 0 deletions xmtp_content_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ version.workspace = true
license.workspace = true

[dependencies]
hex = { workspace = true }
libsecp256k1 = { version = "0.7.1", default-features = false, features = [
"static-context",
] }
prost = { workspace = true, features = ["prost-derive"] }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing.workspace = true
xmtp_cryptography = { path = "../xmtp_cryptography" }
xmtp_v2 = { path = "../xmtp_v2" }

# XMTP/Local
xmtp_common = { workspace = true }
Expand Down
35 changes: 35 additions & 0 deletions xmtp_content_types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod attachment;
pub mod group_updated;
pub mod membership_change;
pub mod multi_remote_attachment;
pub mod reaction;
pub mod read_receipt;
pub mod remote_attachment;
Expand Down Expand Up @@ -35,3 +36,37 @@ pub fn encoded_content_to_bytes(content: EncodedContent) -> Vec<u8> {
pub fn bytes_to_encoded_content(bytes: Vec<u8>) -> EncodedContent {
EncodedContent::decode(&mut bytes.as_slice()).unwrap()
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use super::*;

#[test]
fn test_encoded_content_conversion() {
// Create a sample EncodedContent
let original = EncodedContent {
r#type: Some(ContentTypeId {
authority_id: "".to_string(),
type_id: "test".to_string(),
version_major: 0,
version_minor: 0,
}),
parameters: HashMap::new(),
compression: None,
content: vec![1, 2, 3, 4],
fallback: Some("test".to_string()),
};

// Convert to bytes
let bytes = encoded_content_to_bytes(original.clone());

// Convert back to EncodedContent
let recovered = bytes_to_encoded_content(bytes);

// Verify the recovered content matches the original
assert_eq!(recovered.content, original.content);
assert_eq!(recovered.fallback, original.fallback);
}
}
104 changes: 104 additions & 0 deletions xmtp_content_types/src/multi_remote_attachment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::collections::HashMap;

use crate::{CodecError, ContentCodec};
use prost::Message;
use xmtp_proto::xmtp::mls::message_contents::{
content_types::MultiRemoteAttachment, ContentTypeId, EncodedContent,
};

pub struct MultiRemoteAttachmentCodec {}

impl MultiRemoteAttachmentCodec {
const AUTHORITY_ID: &'static str = "xmtp.org";
pub const TYPE_ID: &'static str = "multiRemoteStaticAttachment";
}

impl ContentCodec<MultiRemoteAttachment> for MultiRemoteAttachmentCodec {
fn content_type() -> ContentTypeId {
ContentTypeId {
authority_id: MultiRemoteAttachmentCodec::AUTHORITY_ID.to_string(),
type_id: MultiRemoteAttachmentCodec::TYPE_ID.to_string(),
version_major: 1,
version_minor: 0,
}
}

fn encode(data: MultiRemoteAttachment) -> Result<EncodedContent, CodecError> {
let mut buf = Vec::new();
data.encode(&mut buf)
.map_err(|e| CodecError::Encode(e.to_string()))?;

Ok(EncodedContent {
r#type: Some(MultiRemoteAttachmentCodec::content_type()),
parameters: HashMap::new(),
fallback: Some(
"Can’t display. This app doesn’t support multi remote attachments.".to_string(),
),
compression: None,
content: buf,
})
}

fn decode(content: EncodedContent) -> Result<MultiRemoteAttachment, CodecError> {
let decoded = MultiRemoteAttachment::decode(content.content.as_slice())
.map_err(|e| CodecError::Decode(e.to_string()))?;

Ok(decoded)
}
}

#[cfg(test)]
pub(crate) mod tests {
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);

use xmtp_proto::xmtp::mls::message_contents::content_types::RemoteAttachmentInfo;

use super::*;

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
fn test_encode_decode() {
let attachment_info_1 = RemoteAttachmentInfo {
content_digest: "0123456789abcdef".to_string(),
secret: vec![0; 32],
nonce: vec![0; 16],
salt: vec![0; 16],
scheme: "https".to_string(),
url: "https://example.com/attachment".to_string(),
content_length: Some(1000),
filename: Some("attachment_1.jpg".to_string()),
};
let attachment_info_2 = RemoteAttachmentInfo {
content_digest: "0123456789abcdef".to_string(),
secret: vec![0; 32],
nonce: vec![0; 16],
salt: vec![0; 16],
scheme: "https".to_string(),
url: "https://example.com/attachment".to_string(),
content_length: Some(1000),
filename: Some("attachment_2.jpg".to_string()),
};

// Store the filenames before moving the attachment_info structs
let filename_1 = attachment_info_1.filename.clone();
let filename_2 = attachment_info_2.filename.clone();

let new_multi_remote_attachment_data: MultiRemoteAttachment = MultiRemoteAttachment {
attachments: vec![attachment_info_1.clone(), attachment_info_2.clone()],
};

let encoded = MultiRemoteAttachmentCodec::encode(new_multi_remote_attachment_data).unwrap();
assert_eq!(
encoded.clone().r#type.unwrap().type_id,
"multiRemoteStaticAttachment"
);
assert!(!encoded.content.is_empty());

let decoded = MultiRemoteAttachmentCodec::decode(encoded).unwrap();
assert_eq!(decoded.attachments[0].filename, filename_1);
assert_eq!(decoded.attachments[1].filename, filename_2);
assert_eq!(decoded.attachments[0].content_length, Some(1000));
assert_eq!(decoded.attachments[1].content_length, Some(1000));
}
}
Loading

0 comments on commit c57c5fa

Please sign in to comment.