1use std::fmt::Display;
2
3use openmls::{extensions::Extensions, group::GroupContext};
4use prost::Message;
5use serde::Serialize;
6use thiserror::Error;
7use xmtp_common::ErrorCode;
8
9use xmtp_id::InboxId;
10use xmtp_proto::xmtp::mls::message_contents::{
11 ConversationType as ConversationTypeProto, DmMembers as DmMembersProto,
12 GroupMetadataV1 as GroupMetadataProto, Inbox as InboxProto, OneshotMessage,
13};
14
15use xmtp_proto::types::ConversationType;
16
17#[derive(Debug, Error, ErrorCode)]
18pub enum GroupMetadataError {
19 #[error("serialization: {0}")]
23 Serialization(#[from] prost::EncodeError),
24 #[error("deserialization: {0}")]
28 Deserialization(#[from] prost::DecodeError),
29 #[error("invalid conversation type")]
33 InvalidConversationType,
34 #[error("missing extension")]
38 MissingExtension,
39 #[error("invalid dm members")]
43 InvalidDmMembers,
44 #[error("missing a dm member")]
48 MissingDmMember,
49 #[error(transparent)]
50 #[error_code(inherit)]
51 Conversion(#[from] xmtp_proto::ConversionError),
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct GroupMetadata {
57 pub conversation_type: ConversationType,
58 pub creator_inbox_id: String,
60 pub dm_members: Option<DmMembers<InboxId>>,
61 pub oneshot_message: Option<OneshotMessage>,
62}
63
64impl GroupMetadata {
65 pub fn new(
66 conversation_type: ConversationType,
67 creator_inbox_id: String,
68 dm_members: Option<DmMembers<InboxId>>,
69 oneshot_message: Option<OneshotMessage>,
70 ) -> Self {
71 Self {
72 conversation_type,
73 creator_inbox_id,
74 dm_members,
75 oneshot_message,
76 }
77 }
78}
79
80impl TryFrom<GroupMetadata> for Vec<u8> {
81 type Error = GroupMetadataError;
82
83 fn try_from(value: GroupMetadata) -> Result<Self, Self::Error> {
84 let conversation_type: ConversationTypeProto = value.conversation_type.into();
85 let proto_val = GroupMetadataProto {
86 conversation_type: conversation_type as i32,
87 creator_inbox_id: value.creator_inbox_id.clone(),
88 creator_account_address: "".to_string(), dm_members: value.dm_members.clone().map(|dm| dm.into()),
90 oneshot_message: value.oneshot_message,
91 };
92 let mut buf: Vec<u8> = Vec::new();
93 proto_val.encode(&mut buf)?;
94
95 Ok(buf)
96 }
97}
98
99impl TryFrom<&Vec<u8>> for GroupMetadata {
100 type Error = GroupMetadataError;
101
102 fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
103 let proto_val = GroupMetadataProto::decode(value.as_slice())?;
104 proto_val.try_into()
105 }
106}
107
108impl TryFrom<GroupMetadataProto> for GroupMetadata {
109 type Error = GroupMetadataError;
110
111 fn try_from(value: GroupMetadataProto) -> Result<Self, Self::Error> {
112 let dm_members = value.dm_members.map(DmMembers::try_from).transpose()?;
113 Ok(Self::new(
114 value.conversation_type.try_into()?,
115 value.creator_inbox_id,
116 dm_members,
117 value.oneshot_message,
118 ))
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize)]
123pub struct DmMembers<Id: AsRef<str>> {
124 pub member_one_inbox_id: Id,
125 pub member_two_inbox_id: Id,
126}
127
128impl<'a> DmMembers<String> {
129 pub fn as_ref(&'a self) -> DmMembers<&'a str> {
130 DmMembers {
131 member_one_inbox_id: &*self.member_one_inbox_id,
132 member_two_inbox_id: &*self.member_two_inbox_id,
133 }
134 }
135}
136
137impl<Id> From<DmMembers<Id>> for DmMembersProto
138where
139 Id: AsRef<str>,
140{
141 fn from(value: DmMembers<Id>) -> Self {
142 DmMembersProto {
143 dm_member_one: Some(InboxProto {
144 inbox_id: value.member_one_inbox_id.as_ref().to_string(),
145 }),
146 dm_member_two: Some(InboxProto {
147 inbox_id: value.member_two_inbox_id.as_ref().to_string(),
148 }),
149 }
150 }
151}
152
153impl<Id> From<&DmMembers<Id>> for String
154where
155 Id: AsRef<str>,
156{
157 fn from(members: &DmMembers<Id>) -> Self {
158 members.to_string()
159 }
160}
161
162impl<Id> From<DmMembers<Id>> for String
163where
164 Id: AsRef<str>,
165{
166 fn from(members: DmMembers<Id>) -> Self {
167 members.to_string()
168 }
169}
170
171impl<Id> Display for DmMembers<Id>
172where
173 Id: AsRef<str>,
174{
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 let mut inbox_ids = [
177 self.member_one_inbox_id.as_ref(),
178 self.member_two_inbox_id.as_ref(),
179 ]
180 .into_iter()
181 .map(str::to_lowercase)
182 .collect::<Vec<_>>();
183 inbox_ids.sort();
184
185 write!(f, "dm:{}", inbox_ids.join(":"))
186 }
187}
188
189impl TryFrom<DmMembersProto> for DmMembers<InboxId> {
190 type Error = GroupMetadataError;
191
192 fn try_from(value: DmMembersProto) -> Result<Self, Self::Error> {
193 Ok(Self {
194 member_one_inbox_id: value
195 .dm_member_one
196 .ok_or(GroupMetadataError::MissingDmMember)?
197 .inbox_id,
198 member_two_inbox_id: value
199 .dm_member_two
200 .ok_or(GroupMetadataError::MissingDmMember)?
201 .inbox_id,
202 })
203 }
204}
205
206pub fn extract_group_metadata(
213 extensions: &Extensions<GroupContext>,
214) -> Result<GroupMetadata, GroupMetadataError> {
215 read_group_metadata_from_dict(extensions)
216}
217
218fn read_group_metadata_from_dict(
220 extensions: &Extensions<GroupContext>,
221) -> Result<GroupMetadata, GroupMetadataError> {
222 use crate::app_data::component_id::ComponentId;
223 use crate::inbox_id::InboxId as DictInboxId;
224 use crate::tls_set::TlsSet;
225 use tls_codec::Deserialize;
226
227 let ext = extensions
228 .app_data_dictionary()
229 .ok_or(GroupMetadataError::MissingExtension)?;
230 let dict = ext.dictionary();
231
232 if !dict.contains(&ComponentId::COMPONENT_REGISTRY.as_u16()) {
233 return Err(GroupMetadataError::MissingExtension);
234 }
235
236 let Some(ct_bytes) = dict.get(&ComponentId::CONVERSATION_TYPE.as_u16()) else {
237 return Err(GroupMetadataError::Conversion(
238 xmtp_proto::ConversionError::Missing {
239 item: "CONVERSATION_TYPE",
240 r#type: "AppData dictionary entry",
241 },
242 ));
243 };
244 let Some(creator_bytes) = dict.get(&ComponentId::CREATOR_INBOX_ID.as_u16()) else {
245 return Err(GroupMetadataError::Conversion(
246 xmtp_proto::ConversionError::Missing {
247 item: "CREATOR_INBOX_ID",
248 r#type: "AppData dictionary entry",
249 },
250 ));
251 };
252
253 let ct_arr: [u8; 4] = ct_bytes
255 .try_into()
256 .map_err(|_| GroupMetadataError::InvalidConversationType)?;
257 let conversation_type_i32 = i32::from_be_bytes(ct_arr);
258 let conversation_type: ConversationType = conversation_type_i32.try_into()?;
259
260 let creator_inbox_id = DictInboxId::tls_deserialize_exact(creator_bytes)
263 .map_err(|e| {
264 GroupMetadataError::Conversion(xmtp_proto::ConversionError::InvalidValue {
265 item: "CREATOR_INBOX_ID",
266 expected: "versioned InboxId TLS encoding",
267 got: format!("{e}"),
268 })
269 })?
270 .to_hex();
271
272 let dm_members = match dict.get(&ComponentId::DM_MEMBERS.as_u16()) {
277 Some(_) if conversation_type != ConversationType::Dm => {
278 return Err(GroupMetadataError::InvalidDmMembers);
279 }
280 Some(b) => {
281 let set = TlsSet::<DictInboxId>::tls_deserialize_exact(b)
282 .map_err(|_| GroupMetadataError::InvalidDmMembers)?;
283 let ids: Vec<DictInboxId> = set.iter().copied().collect();
284 if ids.len() != 2 {
285 return Err(GroupMetadataError::InvalidDmMembers);
286 }
287 Some(DmMembers {
288 member_one_inbox_id: ids[0].to_hex(),
289 member_two_inbox_id: ids[1].to_hex(),
290 })
291 }
292 None => None,
293 };
294
295 let oneshot_message = match dict.get(&ComponentId::ONESHOT_MESSAGE.as_u16()) {
297 Some(b) => Some(OneshotMessage::decode(b)?),
298 None => None,
299 };
300
301 Ok(GroupMetadata {
302 conversation_type,
303 creator_inbox_id,
304 dm_members,
305 oneshot_message,
306 })
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[xmtp_common::test(unwrap_try = true)]
314 fn test_dm_members_sort() {
315 let members = DmMembers {
316 member_one_inbox_id: "thats_me".to_string(),
317 member_two_inbox_id: "some_wise_guy".to_string(),
318 };
319
320 let members2 = DmMembers {
321 member_one_inbox_id: "some_wise_guy".to_string(),
322 member_two_inbox_id: "thats_me".to_string(),
323 };
324
325 assert_eq!(members.to_string(), members2.to_string());
326 }
327
328 fn extensions_with(
329 conversation_type: ConversationType,
330 dm_members: bool,
331 ) -> Extensions<GroupContext> {
332 use crate::app_data::component_id::ComponentId;
333 use crate::inbox_id::InboxId as DictInboxId;
334 use crate::tls_set::TlsSet;
335 use openmls::extensions::{AppDataDictionary, AppDataDictionaryExtension, Extension};
336 use tls_codec::Serialize;
337
338 let mut dict = AppDataDictionary::new();
339 let mut insert = |id: ComponentId, bytes: Vec<u8>| {
340 let _ = dict.insert(id.as_u16(), bytes);
341 };
342 insert(ComponentId::COMPONENT_REGISTRY, vec![]);
343 insert(
344 ComponentId::CONVERSATION_TYPE,
345 (conversation_type as i32).to_be_bytes().to_vec(),
346 );
347 insert(
348 ComponentId::CREATOR_INBOX_ID,
349 DictInboxId::from_bytes([1; 32])
350 .tls_serialize_detached()
351 .unwrap(),
352 );
353 if dm_members {
354 let pair = TlsSet::from_keys([
355 DictInboxId::from_bytes([1; 32]),
356 DictInboxId::from_bytes([2; 32]),
357 ]);
358 insert(
359 ComponentId::DM_MEMBERS,
360 pair.tls_serialize_detached().unwrap(),
361 );
362 }
363 Extensions::from_vec(vec![Extension::AppDataDictionary(
364 AppDataDictionaryExtension::new(dict),
365 )])
366 .unwrap()
367 }
368
369 #[xmtp_common::test(unwrap_try = true)]
374 fn dm_members_are_rejected_outside_a_dm() {
375 let dm = extract_group_metadata(&extensions_with(ConversationType::Dm, true))?;
376 assert!(dm.dm_members.is_some());
377 assert!(extract_group_metadata(&extensions_with(ConversationType::Group, false)).is_ok());
378 for conversation_type in [
379 ConversationType::Group,
380 ConversationType::Sync,
381 ConversationType::Oneshot,
382 ] {
383 assert!(matches!(
384 extract_group_metadata(&extensions_with(conversation_type, true)),
385 Err(GroupMetadataError::InvalidDmMembers)
386 ));
387 }
388 }
389}