Skip to content

Commit c57c5fa

Browse files
authored
Multi remote attachment content type (#1609)
* 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 <cameronvoell@users.noreply.github.com>
1 parent 656e039 commit c57c5fa

14 files changed

+2280
-1546
lines changed

Cargo.lock

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings_ffi/src/mls.rs

+168-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use tokio::sync::Mutex;
77
use xmtp_api::{strategies, ApiClientWrapper};
88
use xmtp_api_grpc::grpc_api_helper::Client as TonicApiClient;
99
use xmtp_common::{AbortHandle, GenericStreamHandle, StreamHandle};
10+
use xmtp_content_types::multi_remote_attachment::MultiRemoteAttachmentCodec;
1011
use xmtp_content_types::reaction::ReactionCodec;
1112
use xmtp_content_types::ContentCodec;
1213
use xmtp_id::associations::{verify_signed_with_public_context, DeserializationError};
@@ -54,7 +55,9 @@ use xmtp_mls::{
5455
};
5556
use xmtp_proto::api_client::ApiBuilder;
5657
use xmtp_proto::xmtp::device_sync::BackupElementSelection;
57-
use xmtp_proto::xmtp::mls::message_contents::content_types::ReactionV2;
58+
use xmtp_proto::xmtp::mls::message_contents::content_types::{
59+
MultiRemoteAttachment, ReactionV2, RemoteAttachmentInfo,
60+
};
5861
use xmtp_proto::xmtp::mls::message_contents::{DeviceSyncKind, EncodedContent};
5962
pub type RustXmtpClient = MlsClient<TonicApiClient>;
6063

@@ -2228,6 +2231,111 @@ impl From<FfiReactionSchema> for i32 {
22282231
}
22292232
}
22302233

2234+
#[derive(uniffi::Record, Clone, Default)]
2235+
pub struct FfiRemoteAttachmentInfo {
2236+
pub secret: Vec<u8>,
2237+
pub content_digest: String,
2238+
pub nonce: Vec<u8>,
2239+
pub scheme: String,
2240+
pub url: String,
2241+
pub salt: Vec<u8>,
2242+
pub content_length: Option<u32>,
2243+
pub filename: Option<String>,
2244+
}
2245+
2246+
impl From<FfiRemoteAttachmentInfo> for RemoteAttachmentInfo {
2247+
fn from(ffi_remote_attachment_info: FfiRemoteAttachmentInfo) -> Self {
2248+
RemoteAttachmentInfo {
2249+
content_digest: ffi_remote_attachment_info.content_digest,
2250+
secret: ffi_remote_attachment_info.secret,
2251+
nonce: ffi_remote_attachment_info.nonce,
2252+
salt: ffi_remote_attachment_info.salt,
2253+
scheme: ffi_remote_attachment_info.scheme,
2254+
url: ffi_remote_attachment_info.url,
2255+
content_length: ffi_remote_attachment_info.content_length,
2256+
filename: ffi_remote_attachment_info.filename,
2257+
}
2258+
}
2259+
}
2260+
2261+
impl From<RemoteAttachmentInfo> for FfiRemoteAttachmentInfo {
2262+
fn from(remote_attachment_info: RemoteAttachmentInfo) -> Self {
2263+
FfiRemoteAttachmentInfo {
2264+
secret: remote_attachment_info.secret,
2265+
content_digest: remote_attachment_info.content_digest,
2266+
nonce: remote_attachment_info.nonce,
2267+
scheme: remote_attachment_info.scheme,
2268+
url: remote_attachment_info.url,
2269+
salt: remote_attachment_info.salt,
2270+
content_length: remote_attachment_info.content_length,
2271+
filename: remote_attachment_info.filename,
2272+
}
2273+
}
2274+
}
2275+
2276+
#[derive(uniffi::Record, Clone, Default)]
2277+
pub struct FfiMultiRemoteAttachment {
2278+
pub attachments: Vec<FfiRemoteAttachmentInfo>,
2279+
}
2280+
2281+
impl From<FfiMultiRemoteAttachment> for MultiRemoteAttachment {
2282+
fn from(ffi_multi_remote_attachment: FfiMultiRemoteAttachment) -> Self {
2283+
MultiRemoteAttachment {
2284+
attachments: ffi_multi_remote_attachment
2285+
.attachments
2286+
.into_iter()
2287+
.map(Into::into)
2288+
.collect(),
2289+
}
2290+
}
2291+
}
2292+
2293+
impl From<MultiRemoteAttachment> for FfiMultiRemoteAttachment {
2294+
fn from(multi_remote_attachment: MultiRemoteAttachment) -> Self {
2295+
FfiMultiRemoteAttachment {
2296+
attachments: multi_remote_attachment
2297+
.attachments
2298+
.into_iter()
2299+
.map(Into::into)
2300+
.collect(),
2301+
}
2302+
}
2303+
}
2304+
2305+
#[uniffi::export]
2306+
pub fn encode_multi_remote_attachment(
2307+
ffi_multi_remote_attachment: FfiMultiRemoteAttachment,
2308+
) -> Result<Vec<u8>, GenericError> {
2309+
// Convert FfiMultiRemoteAttachment to MultiRemoteAttachment
2310+
let multi_remote_attachment: MultiRemoteAttachment = ffi_multi_remote_attachment.into();
2311+
2312+
// Use MultiRemoteAttachmentCodec to encode the reaction
2313+
let encoded = MultiRemoteAttachmentCodec::encode(multi_remote_attachment)
2314+
.map_err(|e| GenericError::Generic { err: e.to_string() })?;
2315+
2316+
// Encode the EncodedContent to bytes
2317+
let mut buf = Vec::new();
2318+
encoded
2319+
.encode(&mut buf)
2320+
.map_err(|e| GenericError::Generic { err: e.to_string() })?;
2321+
2322+
Ok(buf)
2323+
}
2324+
2325+
#[uniffi::export]
2326+
pub fn decode_multi_remote_attachment(
2327+
bytes: Vec<u8>,
2328+
) -> Result<FfiMultiRemoteAttachment, GenericError> {
2329+
// Decode bytes into EncodedContent
2330+
let encoded_content = EncodedContent::decode(bytes.as_slice())
2331+
.map_err(|e| GenericError::Generic { err: e.to_string() })?;
2332+
2333+
// Use MultiRemoteAttachmentCodec to decode into MultiRemoteAttachment and convert to FfiMultiRemoteAttachment
2334+
MultiRemoteAttachmentCodec::decode(encoded_content)
2335+
.map(Into::into)
2336+
.map_err(|e| GenericError::Generic { err: e.to_string() })
2337+
}
2338+
22312339
#[derive(uniffi::Record, Clone)]
22322340
pub struct FfiMessage {
22332341
pub id: Vec<u8>,
@@ -2436,14 +2544,16 @@ mod tests {
24362544
FfiPreferenceUpdate, FfiXmtpClient,
24372545
};
24382546
use crate::{
2439-
connect_to_backend, decode_reaction, encode_reaction, get_inbox_id_for_address,
2547+
connect_to_backend, decode_multi_remote_attachment, decode_reaction,
2548+
encode_multi_remote_attachment, encode_reaction, get_inbox_id_for_address,
24402549
inbox_owner::SigningError, FfiConsent, FfiConsentEntityType, FfiConsentState,
24412550
FfiContentType, FfiConversation, FfiConversationCallback, FfiConversationMessageKind,
24422551
FfiCreateDMOptions, FfiCreateGroupOptions, FfiDirection, FfiGroupPermissionsOptions,
24432552
FfiInboxOwner, FfiListConversationsOptions, FfiListMessagesOptions,
24442553
FfiMessageDisappearingSettings, FfiMessageWithReactions, FfiMetadataField,
2445-
FfiPermissionPolicy, FfiPermissionPolicySet, FfiPermissionUpdateType, FfiReaction,
2446-
FfiReactionAction, FfiReactionSchema, FfiSubscribeError,
2554+
FfiMultiRemoteAttachment, FfiPermissionPolicy, FfiPermissionPolicySet,
2555+
FfiPermissionUpdateType, FfiReaction, FfiReactionAction, FfiReactionSchema,
2556+
FfiRemoteAttachmentInfo, FfiSubscribeError,
24472557
};
24482558
use ethers::utils::hex;
24492559
use prost::Message;
@@ -6857,4 +6967,58 @@ mod tests {
68576967
// Clean up stream
68586968
stream.end_and_wait().await.unwrap();
68596969
}
6970+
6971+
#[tokio::test]
6972+
async fn test_multi_remote_attachment_encode_decode() {
6973+
// Create a test attachment
6974+
let original_attachment = FfiMultiRemoteAttachment {
6975+
attachments: vec![
6976+
FfiRemoteAttachmentInfo {
6977+
filename: Some("test1.jpg".to_string()),
6978+
content_length: Some(1000),
6979+
secret: vec![1, 2, 3],
6980+
content_digest: "123".to_string(),
6981+
nonce: vec![7, 8, 9],
6982+
salt: vec![1, 2, 3],
6983+
scheme: "https".to_string(),
6984+
url: "https://example.com/test1.jpg".to_string(),
6985+
},
6986+
FfiRemoteAttachmentInfo {
6987+
filename: Some("test2.pdf".to_string()),
6988+
content_length: Some(2000),
6989+
secret: vec![4, 5, 6],
6990+
content_digest: "456".to_string(),
6991+
nonce: vec![10, 11, 12],
6992+
salt: vec![1, 2, 3],
6993+
scheme: "https".to_string(),
6994+
url: "https://example.com/test2.pdf".to_string(),
6995+
},
6996+
],
6997+
};
6998+
6999+
// Encode the attachment
7000+
let encoded_bytes = encode_multi_remote_attachment(original_attachment.clone())
7001+
.expect("Should encode multi remote attachment successfully");
7002+
7003+
// Decode the attachment
7004+
let decoded_attachment = decode_multi_remote_attachment(encoded_bytes)
7005+
.expect("Should decode multi remote attachment successfully");
7006+
7007+
assert_eq!(
7008+
decoded_attachment.attachments.len(),
7009+
original_attachment.attachments.len()
7010+
);
7011+
7012+
for (decoded, original) in decoded_attachment
7013+
.attachments
7014+
.iter()
7015+
.zip(original_attachment.attachments.iter())
7016+
{
7017+
assert_eq!(decoded.filename, original.filename);
7018+
assert_eq!(decoded.content_digest, original.content_digest);
7019+
assert_eq!(decoded.nonce, original.nonce);
7020+
assert_eq!(decoded.scheme, original.scheme);
7021+
assert_eq!(decoded.url, original.url);
7022+
}
7023+
}
68607024
}

xmtp_content_types/Cargo.toml

+6
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ version.workspace = true
55
license.workspace = true
66

77
[dependencies]
8+
hex = { workspace = true }
9+
libsecp256k1 = { version = "0.7.1", default-features = false, features = [
10+
"static-context",
11+
] }
812
prost = { workspace = true, features = ["prost-derive"] }
913
rand = { workspace = true }
1014
serde = { workspace = true, features = ["derive"] }
1115
serde_json = { workspace = true }
1216
thiserror = { workspace = true }
1317
tracing.workspace = true
18+
xmtp_cryptography = { path = "../xmtp_cryptography" }
19+
xmtp_v2 = { path = "../xmtp_v2" }
1420

1521
# XMTP/Local
1622
xmtp_common = { workspace = true }

xmtp_content_types/src/lib.rs

+35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod attachment;
22
pub mod group_updated;
33
pub mod membership_change;
4+
pub mod multi_remote_attachment;
45
pub mod reaction;
56
pub mod read_receipt;
67
pub mod remote_attachment;
@@ -35,3 +36,37 @@ pub fn encoded_content_to_bytes(content: EncodedContent) -> Vec<u8> {
3536
pub fn bytes_to_encoded_content(bytes: Vec<u8>) -> EncodedContent {
3637
EncodedContent::decode(&mut bytes.as_slice()).unwrap()
3738
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use std::collections::HashMap;
43+
44+
use super::*;
45+
46+
#[test]
47+
fn test_encoded_content_conversion() {
48+
// Create a sample EncodedContent
49+
let original = EncodedContent {
50+
r#type: Some(ContentTypeId {
51+
authority_id: "".to_string(),
52+
type_id: "test".to_string(),
53+
version_major: 0,
54+
version_minor: 0,
55+
}),
56+
parameters: HashMap::new(),
57+
compression: None,
58+
content: vec![1, 2, 3, 4],
59+
fallback: Some("test".to_string()),
60+
};
61+
62+
// Convert to bytes
63+
let bytes = encoded_content_to_bytes(original.clone());
64+
65+
// Convert back to EncodedContent
66+
let recovered = bytes_to_encoded_content(bytes);
67+
68+
// Verify the recovered content matches the original
69+
assert_eq!(recovered.content, original.content);
70+
assert_eq!(recovered.fallback, original.fallback);
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::collections::HashMap;
2+
3+
use crate::{CodecError, ContentCodec};
4+
use prost::Message;
5+
use xmtp_proto::xmtp::mls::message_contents::{
6+
content_types::MultiRemoteAttachment, ContentTypeId, EncodedContent,
7+
};
8+
9+
pub struct MultiRemoteAttachmentCodec {}
10+
11+
impl MultiRemoteAttachmentCodec {
12+
const AUTHORITY_ID: &'static str = "xmtp.org";
13+
pub const TYPE_ID: &'static str = "multiRemoteStaticAttachment";
14+
}
15+
16+
impl ContentCodec<MultiRemoteAttachment> for MultiRemoteAttachmentCodec {
17+
fn content_type() -> ContentTypeId {
18+
ContentTypeId {
19+
authority_id: MultiRemoteAttachmentCodec::AUTHORITY_ID.to_string(),
20+
type_id: MultiRemoteAttachmentCodec::TYPE_ID.to_string(),
21+
version_major: 1,
22+
version_minor: 0,
23+
}
24+
}
25+
26+
fn encode(data: MultiRemoteAttachment) -> Result<EncodedContent, CodecError> {
27+
let mut buf = Vec::new();
28+
data.encode(&mut buf)
29+
.map_err(|e| CodecError::Encode(e.to_string()))?;
30+
31+
Ok(EncodedContent {
32+
r#type: Some(MultiRemoteAttachmentCodec::content_type()),
33+
parameters: HashMap::new(),
34+
fallback: Some(
35+
"Can’t display. This app doesn’t support multi remote attachments.".to_string(),
36+
),
37+
compression: None,
38+
content: buf,
39+
})
40+
}
41+
42+
fn decode(content: EncodedContent) -> Result<MultiRemoteAttachment, CodecError> {
43+
let decoded = MultiRemoteAttachment::decode(content.content.as_slice())
44+
.map_err(|e| CodecError::Decode(e.to_string()))?;
45+
46+
Ok(decoded)
47+
}
48+
}
49+
50+
#[cfg(test)]
51+
pub(crate) mod tests {
52+
#[cfg(target_arch = "wasm32")]
53+
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
54+
55+
use xmtp_proto::xmtp::mls::message_contents::content_types::RemoteAttachmentInfo;
56+
57+
use super::*;
58+
59+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
60+
#[cfg_attr(not(target_arch = "wasm32"), test)]
61+
fn test_encode_decode() {
62+
let attachment_info_1 = RemoteAttachmentInfo {
63+
content_digest: "0123456789abcdef".to_string(),
64+
secret: vec![0; 32],
65+
nonce: vec![0; 16],
66+
salt: vec![0; 16],
67+
scheme: "https".to_string(),
68+
url: "https://example.com/attachment".to_string(),
69+
content_length: Some(1000),
70+
filename: Some("attachment_1.jpg".to_string()),
71+
};
72+
let attachment_info_2 = RemoteAttachmentInfo {
73+
content_digest: "0123456789abcdef".to_string(),
74+
secret: vec![0; 32],
75+
nonce: vec![0; 16],
76+
salt: vec![0; 16],
77+
scheme: "https".to_string(),
78+
url: "https://example.com/attachment".to_string(),
79+
content_length: Some(1000),
80+
filename: Some("attachment_2.jpg".to_string()),
81+
};
82+
83+
// Store the filenames before moving the attachment_info structs
84+
let filename_1 = attachment_info_1.filename.clone();
85+
let filename_2 = attachment_info_2.filename.clone();
86+
87+
let new_multi_remote_attachment_data: MultiRemoteAttachment = MultiRemoteAttachment {
88+
attachments: vec![attachment_info_1.clone(), attachment_info_2.clone()],
89+
};
90+
91+
let encoded = MultiRemoteAttachmentCodec::encode(new_multi_remote_attachment_data).unwrap();
92+
assert_eq!(
93+
encoded.clone().r#type.unwrap().type_id,
94+
"multiRemoteStaticAttachment"
95+
);
96+
assert!(!encoded.content.is_empty());
97+
98+
let decoded = MultiRemoteAttachmentCodec::decode(encoded).unwrap();
99+
assert_eq!(decoded.attachments[0].filename, filename_1);
100+
assert_eq!(decoded.attachments[1].filename, filename_2);
101+
assert_eq!(decoded.attachments[0].content_length, Some(1000));
102+
assert_eq!(decoded.attachments[1].content_length, Some(1000));
103+
}
104+
}

0 commit comments

Comments
 (0)