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
122impl TryFrom<&Extensions<GroupContext>> for GroupMetadata {
123 type Error = GroupMetadataError;
124
125 fn try_from(extensions: &Extensions<GroupContext>) -> Result<Self, Self::Error> {
126 let data = extensions
127 .immutable_metadata()
128 .ok_or(GroupMetadataError::MissingExtension)?;
129 data.metadata().try_into()
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Serialize)]
134pub struct DmMembers<Id: AsRef<str>> {
135 pub member_one_inbox_id: Id,
136 pub member_two_inbox_id: Id,
137}
138
139impl<'a> DmMembers<String> {
140 pub fn as_ref(&'a self) -> DmMembers<&'a str> {
141 DmMembers {
142 member_one_inbox_id: &*self.member_one_inbox_id,
143 member_two_inbox_id: &*self.member_two_inbox_id,
144 }
145 }
146}
147
148impl<Id> From<DmMembers<Id>> for DmMembersProto
149where
150 Id: AsRef<str>,
151{
152 fn from(value: DmMembers<Id>) -> Self {
153 DmMembersProto {
154 dm_member_one: Some(InboxProto {
155 inbox_id: value.member_one_inbox_id.as_ref().to_string(),
156 }),
157 dm_member_two: Some(InboxProto {
158 inbox_id: value.member_two_inbox_id.as_ref().to_string(),
159 }),
160 }
161 }
162}
163
164impl<Id> From<&DmMembers<Id>> for String
165where
166 Id: AsRef<str>,
167{
168 fn from(members: &DmMembers<Id>) -> Self {
169 members.to_string()
170 }
171}
172
173impl<Id> From<DmMembers<Id>> for String
174where
175 Id: AsRef<str>,
176{
177 fn from(members: DmMembers<Id>) -> Self {
178 members.to_string()
179 }
180}
181
182impl<Id> Display for DmMembers<Id>
183where
184 Id: AsRef<str>,
185{
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 let mut inbox_ids = [
188 self.member_one_inbox_id.as_ref(),
189 self.member_two_inbox_id.as_ref(),
190 ]
191 .into_iter()
192 .map(str::to_lowercase)
193 .collect::<Vec<_>>();
194 inbox_ids.sort();
195
196 write!(f, "dm:{}", inbox_ids.join(":"))
197 }
198}
199
200impl TryFrom<DmMembersProto> for DmMembers<InboxId> {
201 type Error = GroupMetadataError;
202
203 fn try_from(value: DmMembersProto) -> Result<Self, Self::Error> {
204 Ok(Self {
205 member_one_inbox_id: value
206 .dm_member_one
207 .ok_or(GroupMetadataError::MissingDmMember)?
208 .inbox_id,
209 member_two_inbox_id: value
210 .dm_member_two
211 .ok_or(GroupMetadataError::MissingDmMember)?
212 .inbox_id,
213 })
214 }
215}
216
217pub fn extract_group_metadata(
226 extensions: &Extensions<GroupContext>,
227) -> Result<GroupMetadata, GroupMetadataError> {
228 if let Some(metadata) = read_group_metadata_from_dict(extensions)? {
229 return Ok(metadata);
230 }
231
232 let extension = extensions
233 .immutable_metadata()
234 .ok_or(GroupMetadataError::MissingExtension)?;
235
236 extension.metadata().try_into()
237}
238
239fn read_group_metadata_from_dict(
246 extensions: &Extensions<GroupContext>,
247) -> Result<Option<GroupMetadata>, GroupMetadataError> {
248 use crate::app_data::component_id::ComponentId;
249 use crate::inbox_id::InboxId as DictInboxId;
250 use crate::tls_set::TlsSet;
251 use tls_codec::Deserialize;
252
253 let Some(ext) = extensions.app_data_dictionary() else {
254 return Ok(None);
255 };
256 let dict = ext.dictionary();
257
258 if !dict.contains(&ComponentId::COMPONENT_REGISTRY.as_u16()) {
262 return Ok(None);
263 }
264
265 let Some(ct_bytes) = dict.get(&ComponentId::CONVERSATION_TYPE.as_u16()) else {
269 return Err(GroupMetadataError::Conversion(
270 xmtp_proto::ConversionError::Missing {
271 item: "CONVERSATION_TYPE",
272 r#type: "AppData dictionary entry",
273 },
274 ));
275 };
276 let Some(creator_bytes) = dict.get(&ComponentId::CREATOR_INBOX_ID.as_u16()) else {
277 return Err(GroupMetadataError::Conversion(
278 xmtp_proto::ConversionError::Missing {
279 item: "CREATOR_INBOX_ID",
280 r#type: "AppData dictionary entry",
281 },
282 ));
283 };
284
285 let ct_arr: [u8; 4] = ct_bytes
287 .try_into()
288 .map_err(|_| GroupMetadataError::InvalidConversationType)?;
289 let conversation_type_i32 = i32::from_be_bytes(ct_arr);
290 let conversation_type: ConversationType = conversation_type_i32.try_into()?;
291
292 let creator_inbox_id = DictInboxId::tls_deserialize_exact(creator_bytes)
295 .map_err(|e| {
296 GroupMetadataError::Conversion(xmtp_proto::ConversionError::InvalidValue {
297 item: "CREATOR_INBOX_ID",
298 expected: "versioned InboxId TLS encoding",
299 got: format!("{e}"),
300 })
301 })?
302 .to_hex();
303
304 let dm_members = match dict.get(&ComponentId::DM_MEMBERS.as_u16()) {
306 Some(b) => {
307 let set = TlsSet::<DictInboxId>::tls_deserialize_exact(b)
308 .map_err(|_| GroupMetadataError::InvalidDmMembers)?;
309 let ids: Vec<DictInboxId> = set.iter().copied().collect();
310 if ids.len() != 2 {
311 return Err(GroupMetadataError::InvalidDmMembers);
312 }
313 Some(DmMembers {
314 member_one_inbox_id: ids[0].to_hex(),
315 member_two_inbox_id: ids[1].to_hex(),
316 })
317 }
318 None => None,
319 };
320
321 let oneshot_message = match dict.get(&ComponentId::ONESHOT_MESSAGE.as_u16()) {
323 Some(b) => Some(OneshotMessage::decode(b)?),
324 None => None,
325 };
326
327 Ok(Some(GroupMetadata {
328 conversation_type,
329 creator_inbox_id,
330 dm_members,
331 oneshot_message,
332 }))
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[xmtp_common::test]
340 fn test_dm_members_sort() {
341 let members = DmMembers {
342 member_one_inbox_id: "thats_me".to_string(),
343 member_two_inbox_id: "some_wise_guy".to_string(),
344 };
345
346 let members2 = DmMembers {
347 member_one_inbox_id: "some_wise_guy".to_string(),
348 member_two_inbox_id: "thats_me".to_string(),
349 };
350
351 assert_eq!(members.to_string(), members2.to_string());
352 }
353}