1use super::{GroupError, MlsGroup, PreconfiguredPolicies};
2use crate::context::XmtpSharedContext;
3use xmtp_common::snippet::Snippet;
4use xmtp_db::{
5 MlsProviderExt, XmtpMlsStorageProvider,
6 consent_record::{ConsentState, ConsentType},
7 group::ConversationType,
8 prelude::{QueryConsentRecord, QueryReaddStatus},
9};
10use xmtp_id::AsIdRef;
11use xmtp_mls_common::{group::GroupMetadataOptions, group_metadata::GroupMetadata};
12use xmtp_proto::{
13 types::{Cursor, GroupId},
14 xmtp::mls::message_contents::{OneshotMessage, oneshot_message},
15};
16
17pub struct Oneshot {}
18
19impl Oneshot {
20 pub async fn send_message<C: XmtpSharedContext, S: AsIdRef>(
34 context: C,
35 inbox_ids: impl AsRef<[S]>,
36 oneshot_message: OneshotMessage,
37 ) -> Result<(), GroupError> {
38 let group = MlsGroup::<C>::create_and_insert(
40 context.clone(),
41 ConversationType::Oneshot,
42 PreconfiguredPolicies::default().to_policy_set(),
43 GroupMetadataOptions::default(),
44 Some(oneshot_message),
45 )?;
46
47 if !inbox_ids.as_ref().is_empty() {
49 group.add_members(&inbox_ids).await?;
50 } else {
51 group.add_missing_installations().await?;
52 }
53
54 Ok(())
56 }
57
58 pub fn process_message(
59 provider: &impl MlsProviderExt,
60 _sender_inbox_id: String,
61 sender_installation_id: Vec<u8>,
62 message: OneshotMessage,
63 ) -> Result<(), GroupError> {
64 match message.message_type {
65 Some(oneshot_message::MessageType::ReaddRequest(readd_request)) => {
66 if Self::validate_readd_request(
67 provider.key_store(),
68 &readd_request.group_id,
69 &sender_installation_id,
70 )? {
71 let group_id = GroupId::try_from(readd_request.group_id.as_slice())?;
72 provider.key_store().db().update_requested_at_sequence_id(
73 &group_id,
74 &sender_installation_id,
75 readd_request.latest_commit_sequence_id as i64,
76 )?;
77 tracing::info!(
78 group_id = readd_request.group_id.snippet(),
79 sender_installation_id = sender_installation_id.snippet(),
80 latest_commit_sequence_id = readd_request.latest_commit_sequence_id,
81 "Stored incoming readd request"
82 );
83 }
84 }
85 _ => {
86 tracing::warn!(
87 "Oneshot message {:?} is not a recognized message type",
88 message.message_type
89 );
90 }
91 }
92 Ok(())
93 }
94
95 pub fn process_welcome(
96 provider: &impl MlsProviderExt,
97 cursor: Cursor,
98 sender_inbox_id: String,
99 sender_installation_id: Vec<u8>,
100 metadata: GroupMetadata,
101 ) -> Result<(), GroupError> {
102 tracing::debug!("Processing oneshot welcome");
103 if let Some(message) = metadata.oneshot_message {
104 Self::process_message(provider, sender_inbox_id, sender_installation_id, message)?;
105 } else {
106 tracing::warn!(
107 "Oneshot group welcome {} does not have oneshot message",
108 cursor
109 );
110 }
111 Ok(())
112 }
113
114 fn validate_readd_request(
115 storage: &impl XmtpMlsStorageProvider,
116 group_id: &Vec<u8>,
117 sender_installation_id: &Vec<u8>,
118 ) -> Result<bool, GroupError> {
119 let Some(record) = storage
120 .db()
121 .get_consent_record(hex::encode(group_id), ConsentType::ConversationId)?
122 else {
123 tracing::warn!(
124 group_id = group_id.snippet(),
125 "No consent record found for readd request"
126 );
127 return Ok(false);
128 };
129 if record.state != ConsentState::Allowed {
130 tracing::warn!(
131 group_id = group_id.snippet(),
132 "Group not consented for readd request"
133 );
134 return Ok(false);
135 }
136 let Ok(Some(openmls_group)) = openmls::group::MlsGroup::load(
138 storage,
139 &openmls::group::GroupId::from_slice(group_id.as_slice()),
140 ) else {
141 tracing::warn!(
142 group_id = group_id.snippet(),
143 "Unable to load OpenMLS group for readd request"
144 );
145 return Ok(false);
146 };
147 if !openmls_group
148 .members()
149 .any(|member| member.signature_key == *sender_installation_id)
150 {
151 tracing::warn!(
152 group_id = group_id.snippet(),
153 "Sender not a member of group for readd request"
154 );
155 return Ok(false);
156 }
157
158 Ok(true)
159 }
160}
161
162#[cfg(all(test, not(target_arch = "wasm32")))]
163mod tests {
164 use super::*;
165 use crate::tester;
166 use futures::stream::StreamExt;
167 use xmtp_proto::xmtp::mls::message_contents::{ReaddRequest, oneshot_message::MessageType};
168
169 #[tokio::test]
170 async fn test_receive_oneshot_message_via_syncing() {
171 use xmtp_db::prelude::QueryReaddStatus;
172
173 tester!(alix);
174 tester!(bo);
175 tester!(caro);
176
177 let a_group = alix
178 .create_group_with_members(&[bo.inbox_id(), caro.inbox_id()], None, None)
179 .await
180 .unwrap();
181 let group_id = a_group.group_id;
182 let group_id_typed: GroupId = group_id;
183 bo.sync_all_welcomes_and_groups(None).await.unwrap();
184 caro.sync_all_welcomes_and_groups(None).await.unwrap();
185 let b_group = bo.group(&group_id).unwrap();
186 b_group.update_consent_state(ConsentState::Allowed).unwrap();
187 let c_group = caro.group(&group_id).unwrap();
188 c_group.update_consent_state(ConsentState::Allowed).unwrap();
189 let latest_commit_sequence_id = 42;
190
191 let bo_initial_status = bo
193 .context
194 .db()
195 .get_readd_status(&group_id_typed, alix.context.installation_id().as_slice())
196 .expect("Failed to query readd status");
197 assert!(
198 bo_initial_status.is_none(),
199 "Bo should not have readd status for Alix initially"
200 );
201
202 let caro_initial_status = caro
203 .context
204 .db()
205 .get_readd_status(&group_id_typed, alix.context.installation_id().as_slice())
206 .expect("Failed to query readd status");
207 assert!(
208 caro_initial_status.is_none(),
209 "Caro should not have readd status for Alix initially"
210 );
211
212 let readd_request = ReaddRequest {
214 group_id: group_id.to_vec(),
215 latest_commit_sequence_id,
216 };
217 let oneshot_message = OneshotMessage {
218 message_type: Some(MessageType::ReaddRequest(readd_request.clone())),
219 };
220
221 Oneshot::send_message(
223 alix.context.clone(),
224 vec![bo.inbox_id(), caro.inbox_id()],
225 oneshot_message.clone(),
226 )
227 .await
228 .expect("Failed to send oneshot message");
229
230 bo.sync_welcomes().await.expect("Failed to sync welcomes");
232
233 let bo_status = bo
235 .context
236 .db()
237 .get_readd_status(&group_id_typed, alix.context.installation_id().as_slice())
238 .expect("Failed to query readd status")
239 .expect("Bo should have readd status for Alix after syncing");
240
241 assert_eq!(
242 bo_status.requested_at_sequence_id,
243 Some(latest_commit_sequence_id as i64),
244 "Bo should have requested_at_sequence_id set to {}",
245 latest_commit_sequence_id
246 );
247 assert_eq!(
248 bo_status.responded_at_sequence_id, None,
249 "Bo should not have responded_at_sequence_id set"
250 );
251
252 caro.sync_welcomes().await.expect("Failed to sync welcomes");
254
255 let caro_status = caro
257 .context
258 .db()
259 .get_readd_status(&group_id_typed, alix.context.installation_id().as_slice())
260 .expect("Failed to query readd status")
261 .expect("Caro should have readd status for Alix after syncing");
262
263 assert_eq!(
264 caro_status.requested_at_sequence_id,
265 Some(latest_commit_sequence_id as i64),
266 "Caro should have requested_at_sequence_id set to {}",
267 latest_commit_sequence_id
268 );
269 assert_eq!(
270 caro_status.responded_at_sequence_id, None,
271 "Caro should not have responded_at_sequence_id set"
272 );
273 }
274
275 #[tokio::test]
276 async fn test_oneshot_groups_not_in_find_groups() {
277 tester!(alix);
278 tester!(bo);
279
280 let readd_request = ReaddRequest {
282 group_id: vec![1, 2, 3, 4],
283 latest_commit_sequence_id: 0,
284 };
285 let oneshot_message = OneshotMessage {
286 message_type: Some(MessageType::ReaddRequest(readd_request)),
287 };
288
289 Oneshot::send_message(alix.context.clone(), vec![bo.inbox_id()], oneshot_message)
291 .await
292 .expect("Failed to send oneshot message");
293
294 bo.sync_welcomes().await.expect("Failed to sync welcomes");
296
297 let alix_groups = alix.find_groups(Default::default()).unwrap();
299 let bo_groups = bo.find_groups(Default::default()).unwrap();
300
301 assert_eq!(alix_groups.len(), 0, "Alix should have no groups");
303 assert_eq!(bo_groups.len(), 0, "Bo should have no groups");
304 }
305
306 #[tokio::test]
307 async fn test_oneshot_groups_not_in_stream_groups() {
308 tester!(alix);
309 tester!(bo);
310
311 let mut alix_conversations = alix
313 .stream_conversations(None, false)
314 .await
315 .expect("Failed to stream conversations");
316 let mut bo_conversations = bo
317 .stream_conversations(None, false)
318 .await
319 .expect("Failed to stream conversations");
320
321 let readd_request = ReaddRequest {
323 group_id: vec![5, 6, 7, 8],
324 latest_commit_sequence_id: 0,
325 };
326 let oneshot_message = OneshotMessage {
327 message_type: Some(MessageType::ReaddRequest(readd_request)),
328 };
329
330 Oneshot::send_message(alix.context.clone(), vec![bo.inbox_id()], oneshot_message)
332 .await
333 .expect("Failed to send oneshot message");
334
335 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
337
338 let alix_event = tokio::time::timeout(
340 std::time::Duration::from_millis(100),
341 alix_conversations.next(),
342 )
343 .await;
344 let bo_event = tokio::time::timeout(
345 std::time::Duration::from_millis(100),
346 bo_conversations.next(),
347 )
348 .await;
349
350 assert!(
352 alix_event.is_err(),
353 "Alix should not receive new conversation event"
354 );
355 assert!(
356 bo_event.is_err(),
357 "Bo should not receive new conversation event"
358 );
359 }
360
361 #[tokio::test]
362 async fn test_syncing_and_streaming_oneshot_group_simultaneously() {
363 tester!(alix);
367 tester!(bo);
368
369 let mut bo_conversations = bo
370 .stream_conversations(None, false)
371 .await
372 .expect("Failed to stream conversations");
373
374 let readd_request = ReaddRequest {
376 group_id: vec![5, 6, 7, 8],
377 latest_commit_sequence_id: 0,
378 };
379 let oneshot_message = OneshotMessage {
380 message_type: Some(MessageType::ReaddRequest(readd_request)),
381 };
382
383 Oneshot::send_message(alix.context.clone(), vec![bo.inbox_id()], oneshot_message)
385 .await
386 .expect("Failed to send oneshot message");
387
388 bo.sync_welcomes().await.expect("Failed to sync welcomes");
390
391 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
393
394 let bo_event = tokio::time::timeout(
395 std::time::Duration::from_millis(100),
396 bo_conversations.next(),
397 )
398 .await;
399
400 assert!(
401 bo_event.is_err(),
402 "Bo should not receive new conversation event"
403 );
404 }
405}