1use super::{
4 Result,
5 incoming::{IncomingCoordinator, IncomingLease, IncomingScope, stream_api::GroupSetGate},
6 internal::InternalEvent,
7 local_delivery::LocalDeliveryError,
8};
9use crate::{context::XmtpSharedContext, groups::MlsGroup};
10use futures::Stream;
11use std::{
12 collections::{HashMap, VecDeque},
13 pin::Pin,
14 sync::{Arc, Weak},
15 task::{Context, Poll},
16};
17use xmtp_common::{BoxDynStream, time::sleep};
18use xmtp_db::{
19 consent_record::ConsentState,
20 group::{ConversationType, GroupQueryArgs, StoredGroup},
21 prelude::*,
22};
23use xmtp_events::EventFilter;
24use xmtp_proto::{
25 api_client::XmtpMlsStreams,
26 types::{GroupId, Topic},
27};
28
29const ALL_CONSENT_STATES: [ConsentState; 3] = [
30 ConsentState::Allowed,
31 ConsentState::Unknown,
32 ConsentState::Denied,
33];
34
35#[derive(Default)]
37struct KnownConversations {
38 welcomes: HashMap<GroupId, Option<i64>>,
40 dms: HashMap<String, GroupId>,
42}
43
44impl KnownConversations {
45 fn from_groups(groups: Vec<StoredGroup>) -> Self {
47 let mut known = Self::default();
48 for group in groups {
49 known.welcomes.insert(group.id, group.sequence_id);
50 if let Some(dm_id) = group.dm_id {
51 known.dms.entry(dm_id).or_insert(group.id);
52 }
53 }
54 known
55 }
56
57 fn observe(&mut self, group: &StoredGroup, include_duplicate_dms: bool) -> bool {
59 if self.welcomes.insert(group.id, group.sequence_id) == Some(group.sequence_id)
60 || ConversationType::virtual_types().contains(&group.conversation_type)
61 {
62 return false;
63 }
64 if !include_duplicate_dms && let Some(dm_id) = &group.dm_id {
65 return *self.dms.entry(dm_id.clone()).or_insert(group.id) == group.id;
66 }
67 true
68 }
69}
70
71fn changed_groups<C: XmtpSharedContext>(
79 context: &C,
80 query: &GroupQueryArgs,
81 gate: &mut GroupSetGate,
82) -> std::result::Result<Vec<StoredGroup>, xmtp_db::ConnectionError> {
83 let db = context.db();
84 let generation = db.group_set_generation()?;
85 if !gate.changed(generation) {
86 return Ok(Vec::new());
87 }
88 let groups = if query.consent_states.as_ref().is_some_and(Vec::is_empty) {
89 Vec::new()
90 } else {
91 #[cfg(any(test, feature = "test-hooks"))]
92 test_hooks::count_groups_read();
93 db.find_groups(query)?
94 };
95 gate.record(generation);
96 Ok(groups)
97}
98
99pub struct StreamConversations<C: XmtpSharedContext> {
101 inner: BoxDynStream<'static, Result<MlsGroup<C>>>,
102 lease: Weak<IncomingLease>,
103}
104
105impl<C: XmtpSharedContext + 'static> StreamConversations<C> {
106 pub async fn new(
107 context: &C,
108 conversation_type: Option<ConversationType>,
109 include_duplicate_dms: bool,
110 consent_states: Option<Vec<ConsentState>>,
111 ) -> Result<Self>
112 where
113 C::ApiClient: XmtpMlsStreams,
114 {
115 Self::new_owned(
116 context.clone(),
117 conversation_type,
118 include_duplicate_dms,
119 consent_states,
120 )
121 .await
122 }
123
124 pub async fn new_owned(
126 context: C,
127 conversation_type: Option<ConversationType>,
128 include_duplicate_dms: bool,
129 consent_states: Option<Vec<ConsentState>>,
130 ) -> Result<Self>
131 where
132 C::ApiClient: XmtpMlsStreams,
133 {
134 context.server_configuration().check()?;
135 let events = context.events().subscribe(
136 EventFilter::default().with_internal(|event| {
137 matches!(event, InternalEvent::GroupJoined { is_sync: false, .. })
138 }),
139 Some(10),
140 );
141 let known = KnownConversations::from_groups(context.db().find_groups(GroupQueryArgs {
142 include_sync_groups: true,
143 include_duplicate_dms: true,
144 consent_states: Some(ALL_CONSENT_STATES.to_vec()),
145 ..Default::default()
146 })?);
147 let coordinator = IncomingCoordinator::for_context(&context);
148 let lease = Arc::new(coordinator.acquire_stream(IncomingScope::Topics(vec![
149 Topic::new_welcome_message(context.installation_id()),
150 ])));
151 let query = GroupQueryArgs {
152 conversation_type,
153 consent_states: Some(consent_states.unwrap_or_else(|| ALL_CONSENT_STATES.to_vec())),
154 include_duplicate_dms,
155 ..Default::default()
156 };
157 let observer = Arc::downgrade(&lease);
158 let stream = futures::stream::unfold(
159 Some((
160 context,
161 events,
162 lease,
163 known,
164 VecDeque::new(),
165 query,
166 GroupSetGate::default(),
167 )),
168 |state| async move {
169 let (context, events, lease, mut known, mut ready, query, mut gate) = state?;
170 loop {
171 if let Err(error) = context.server_configuration().check() {
172 lease.close();
173 return Some((Err(error.into()), None));
174 }
175 if context.is_closed() {
176 return None;
177 }
178 if let Err(error) = lease.check_recovery() {
179 lease.close();
180 return Some((Err(LocalDeliveryError::from(error).into()), None));
181 }
182 if let Some(group) = ready.pop_front() {
183 return Some((
184 Ok(group),
185 Some((context, events, lease, known, ready, query, gate)),
186 ));
187 }
188 #[cfg(any(test, feature = "test-hooks"))]
189 test_hooks::count_pass();
190 match changed_groups(&context, &query, &mut gate) {
191 Ok(groups) => {
192 for group in groups {
193 if known.observe(&group, query.include_duplicate_dms) {
194 ready.push_back(MlsGroup::new(
195 context.clone(),
196 group.id,
197 group.dm_id,
198 group.conversation_type,
199 group.created_at_ns,
200 ));
201 }
202 }
203 }
204 Err(error) => {
205 lease.close();
208 return Some((Err(error.into()), None));
209 }
210 }
211 if !ready.is_empty() {
212 continue;
213 }
214 tokio::select! {
215 _ = context.cancellation_token().cancelled() => {},
216 _ = lease.changed() => {},
217 _ = sleep(context.stream_policy().active_database_poll_interval) => {},
218 event = events.next() => { event?; },
219 }
220 }
221 },
222 );
223 Ok(Self {
224 inner: Box::pin(stream),
225 lease: observer,
226 })
227 }
228
229 pub fn lease(&self) -> Option<Arc<IncomingLease>> {
231 self.lease.upgrade()
232 }
233}
234
235impl<C: XmtpSharedContext> Stream for StreamConversations<C> {
236 type Item = Result<MlsGroup<C>>;
237 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
238 self.inner.as_mut().poll_next(cx)
239 }
240}
241
242#[cfg(any(test, feature = "test-hooks"))]
243mod test_hooks;
244
245#[cfg(test)]
246mod test {
247 use crate::prelude::*;
248 use crate::utils::ClientTester;
249 use std::sync::Arc;
250
251 use super::*;
252 use crate::builder::ClientBuilder;
253 use crate::groups::send_message_opts::SendMessageOpts;
254 use crate::tester;
255 use crate::utils::fixtures::{alix, bo};
256 use xmtp_db::group::GroupQueryArgs;
257
258 use futures::StreamExt;
259 use std::cell::Cell;
260 use xmtp_cryptography::utils::generate_local_wallet;
261
262 use super::test_hooks::{GROUPS_READS, PASSES};
263
264 #[xmtp_common::test(unwrap_try = true)]
268 async fn conversation_stream_rereads_groups_only_after_a_change() {
269 tester!(alix, disable_workers);
270 tester!(bo, disable_workers);
271 let passes = || PASSES.with(Cell::get);
272 let reads = || GROUPS_READS.with(Cell::get);
273 let mut stream = StreamConversations::new(&bo.context, None, false, None).await?;
274 tokio::select! {
275 _ = stream.next() => panic!("the stream yielded with no new conversation"),
276 waited = xmtp_common::wait_for_ge(|| async { passes() }, 3) => waited?,
277 }
278 assert_eq!(reads(), 1, "an idle pass reread the groups table");
279
280 let group = alix.create_group(None, None)?;
281 group.add_members(&[bo.inbox_id()]).await?;
282 assert_eq!(stream.next().await.unwrap()?.group_id, group.group_id);
283 assert_eq!(reads(), 2);
284 }
285
286 #[xmtp_common::test(unwrap_try = true)]
288 async fn a_blocked_connection_fences_queued_conversations() {
289 use crate::{server_configuration::BlockedConnection, subscriptions::SubscribeError};
290
291 tester!(alix, disable_workers);
292 let mut stream = StreamConversations::new(&alix.context, None, false, None).await?;
293 alix.create_group(None, None)?;
294 alix.create_group(None, None)?;
295 stream.next().await.unwrap()?;
296
297 alix.context.server_configuration().block_connection(
298 BlockedConnection::ClientVersionTooOld {
299 client: "1.0.0".into(),
300 minimum: "9999.0.0".into(),
301 },
302 );
303 alix.context.cancellation_token().cancel();
304 let error = stream.next().await.unwrap().unwrap_err();
305 assert!(matches!(
306 error,
307 SubscribeError::Blocked(BlockedConnection::ClientVersionTooOld { ref minimum, .. })
308 if minimum == "9999.0.0"
309 ));
310 assert!(stream.next().await.is_none());
311 }
312
313 xmtp_common::if_native! {
314 #[xmtp_common::test(unwrap_try = true)]
316 async fn storage_error_ends_notifications_once_and_releases_the_lease() {
317 use xmtp_db::ConnectionExt;
318 tester!(alix, persistent_db, disable_workers);
319 let coordinator = IncomingCoordinator::for_context(&alix.context);
320 let baseline_owners = Arc::strong_count(&coordinator);
321 let mut stream = StreamConversations::new(&alix.context, None, false, None).await?;
322 assert_eq!(Arc::strong_count(&coordinator), baseline_owners + 1);
323 alix.context.db().disconnect()?;
324 let expected = alix.context.db().find_groups(GroupQueryArgs::default()).unwrap_err();
325 let error = stream.next().await.unwrap().unwrap_err();
326 assert_eq!(error.to_string(), expected.to_string());
327 assert!(matches!(error, super::super::SubscribeError::Db(
328 xmtp_db::ConnectionError::Platform(xmtp_db::PlatformStorageError::PoolNeedsConnection)
329 )), "{error:?}");
330 assert_eq!(Arc::strong_count(&coordinator), baseline_owners);
331 assert!(stream.next().await.is_none());
332 assert!(!alix.context.is_closed());
333
334 alix.context.db().reconnect()?;
335 let mut replacement = StreamConversations::new(&alix.context, None, false, None).await?;
336 let group = alix.create_group(None, None)?;
337 assert_eq!(replacement.next().await.unwrap()?.group_id, group.group_id);
338 }
339 }
340
341 #[xmtp_common::timeout(std::time::Duration::from_secs(10))]
342 #[rstest::rstest]
343 #[case::two_conversations(2)]
344 #[case::five_conversations(5)]
345 #[xmtp_common::test]
346 #[awt]
347 async fn stream_welcomes(
348 #[future] alix: ClientTester,
349 #[future] bo: ClientTester,
350 #[case] group_size: usize,
351 ) {
352 let mut groups = vec![];
353 let mut stream = StreamConversations::new(&bo.context, None, false, None)
354 .await
355 .unwrap();
356 for _ in 0..group_size {
357 let alix_bo_group = alix.create_group(None, None).unwrap();
358 groups.push(alix_bo_group.group_id);
359 alix_bo_group.add_members(&[bo.inbox_id()]).await.unwrap();
360 }
361 while !groups.is_empty() {
362 let bo_received_groups = stream.next().await.unwrap().unwrap();
363 let index = groups
364 .iter()
365 .position(|group_id| bo_received_groups.group_id == *group_id)
366 .expect("group must be found");
367 groups.remove(index);
368 }
369
370 assert!(groups.is_empty(), "Groups must have all been received");
371 }
372
373 #[rstest::rstest]
374 #[xmtp_common::test(unwrap_try = true)]
375 async fn test_sync_groups_are_not_streamed() {
376 tester!(alix, sync_worker);
377 let stream = alix.stream_conversations(None, false).await?;
378 futures::pin_mut!(stream);
379
380 tester!(_alix2, from: alix);
381
382 let result =
383 xmtp_common::time::timeout(std::time::Duration::from_millis(100), stream.next()).await;
384 assert!(result.is_err(), "Sync group should not stream");
385 }
386
387 #[rstest::rstest]
388 #[case(ConversationType::Dm, "Unexpectedly received a Group")]
389 #[case(ConversationType::Group, "Unexpectedly received a DM")]
390 #[xmtp_common::test]
391 #[cfg_attr(target_arch = "wasm32", ignore)]
395 async fn test_dm_stream_filter(
396 #[case] conversation_type: ConversationType,
397 #[case] expected: &str,
398 ) {
399 tester!(alix);
400 tester!(bo);
401 let stream = alix
402 .stream_conversations(Some(conversation_type), false)
403 .await
404 .unwrap();
405 futures::pin_mut!(stream);
406
407 alix.find_or_create_dm(bo.inbox_id().to_string(), None)
408 .await
409 .unwrap();
410
411 let group = alix.create_group(None, None).unwrap();
412 group.add_members(&[bo.inbox_id()]).await.unwrap();
413
414 let group = stream.next().await.unwrap();
415 let metadata = group.unwrap().metadata().await.unwrap();
416
417 assert_eq!(
418 metadata.conversation_type, conversation_type,
419 "{}",
420 expected
421 );
422 let result =
424 xmtp_common::time::timeout(std::time::Duration::from_millis(100), stream.next()).await;
425 assert!(result.is_err(), "should only be one item in the stream");
426 }
427
428 #[rstest::rstest]
429 #[xmtp_common::test]
430 async fn test_dm_stream_all_conversation_types() {
431 let alix = Arc::new(ClientBuilder::new_test_client(&generate_local_wallet()).await);
432 let bo = Arc::new(ClientBuilder::new_test_client(&generate_local_wallet()).await);
433 let davon = Arc::new(ClientBuilder::new_test_client(&generate_local_wallet()).await);
434 let eri = Arc::new(ClientBuilder::new_test_client(&generate_local_wallet()).await);
435
436 let mut groups = Vec::new();
438 let stream = alix.stream_conversations(None, false).await.unwrap();
440 futures::pin_mut!(stream);
441
442 alix.find_or_create_dm(davon.inbox_id().to_string(), None)
443 .await
444 .unwrap();
445 let group = stream.next().await.unwrap();
446 assert!(group.is_ok());
447 groups.push(group.unwrap());
448
449 let dm = eri
450 .find_or_create_dm(alix.inbox_id().to_string(), None)
451 .await
452 .unwrap();
453 dm.add_members(&[alix.inbox_id()]).await.unwrap();
454 let group = stream.next().await.unwrap();
455 assert!(group.is_ok());
456 groups.push(group.unwrap());
457
458 let group = alix.create_group(None, None).unwrap();
459 group.add_members(&[bo.inbox_id()]).await.unwrap();
460 let group = stream.next().await.unwrap();
461 assert!(group.is_ok());
462 groups.push(group.unwrap());
463
464 assert_eq!(groups.len(), 3);
465 }
466
467 #[xmtp_common::timeout(std::time::Duration::from_secs(10))]
468 #[rstest::rstest]
469 #[xmtp_common::test]
470 async fn test_self_group_creation() {
471 tester!(alix);
472 tester!(bo);
473
474 let stream = alix
475 .stream_conversations(Some(ConversationType::Group), false)
476 .await
477 .unwrap();
478 futures::pin_mut!(stream);
479
480 alix.create_group(None, None).unwrap();
481 let _self_group = stream.next().await.unwrap();
482
483 let group = bo.create_group(None, None).unwrap();
484 group.add_members(&[alix.inbox_id()]).await.unwrap();
485 let _bo_group = stream.next().await.unwrap();
486
487 alix.sync_welcomes().await.unwrap();
489 let find_groups_results = alix.find_groups(GroupQueryArgs::default()).unwrap();
490 assert_eq!(2, find_groups_results.len());
491 }
492
493 #[rstest::rstest]
495 #[case::unfiltered(None, 2)]
496 #[case::allowed_only(Some(vec![ConsentState::Allowed]), 1)]
497 #[case::empty_filter(Some(Vec::new()), 0)]
498 #[xmtp_common::test(unwrap_try = true)]
499 async fn conversation_consent_filter_preserves_live_baseline(
500 #[case] consent_states: Option<Vec<ConsentState>>,
501 #[case] expected_count: usize,
502 ) {
503 use xmtp_common::time::{Duration, timeout};
504
505 tester!(alix, disable_workers);
506 let old = alix.create_group(None, None).unwrap();
507 old.update_consent_state(ConsentState::Denied).unwrap();
508 let mut stream = StreamConversations::new(
509 &alix.context,
510 Some(ConversationType::Group),
511 false,
512 consent_states,
513 )
514 .await
515 .unwrap();
516
517 old.update_consent_state(ConsentState::Allowed).unwrap();
518 let denied = alix.create_group(None, None).unwrap();
519 denied.update_consent_state(ConsentState::Denied).unwrap();
520 let allowed = alix.create_group(None, None).unwrap();
521 let expected = match expected_count {
522 2 => vec![denied.group_id, allowed.group_id],
523 1 => vec![allowed.group_id],
524 _ => Vec::new(),
525 };
526 for group_id in expected {
527 let observed = timeout(Duration::from_secs(5), stream.next())
528 .await
529 .unwrap()
530 .unwrap()
531 .unwrap();
532 assert_eq!(observed.group_id, group_id);
533 }
534 assert!(
535 timeout(Duration::from_millis(100), stream.next())
536 .await
537 .is_err()
538 );
539 }
540
541 #[xmtp_common::timeout(std::time::Duration::from_secs(5))]
542 #[rstest::rstest]
543 #[xmtp_common::test]
544 async fn test_add_remove_re_add() {
545 tester!(alix);
546 tester!(bo);
547
548 let alix_group = alix
549 .create_group_with_members(&[bo.inbox_id().to_string()], None, None)
550 .await
551 .unwrap();
552
553 alix_group.remove_members(&[bo.inbox_id()]).await.unwrap();
554 bo.sync_welcomes().await.unwrap();
555 let stream = bo
556 .stream_conversations(Some(ConversationType::Group), false)
557 .await
558 .unwrap();
559 futures::pin_mut!(stream);
560 alix_group
561 .add_members(&[bo.inbox_id().to_string()])
562 .await
563 .unwrap();
564
565 let group_result = stream.next().await.unwrap();
566 if let Err(error) = group_result {
567 panic!("Error streaming group: {:?}", error);
568 }
569 }
570
571 #[xmtp_common::timeout(std::time::Duration::from_secs(15))]
572 #[rstest::rstest]
573 #[xmtp_common::test]
574 async fn test_duplicate_dm_not_streamed() {
575 use xmtp_cryptography::utils::generate_local_wallet;
576
577 let client1 = Arc::new(ClientBuilder::new_test_client(&generate_local_wallet()).await);
578 let client2 = Arc::new(ClientBuilder::new_test_client(&generate_local_wallet()).await);
579
580 let mut stream = client1.stream_conversations(None, false).await.unwrap();
581
582 let dm1 = client1
584 .find_or_create_dm(client2.inbox_id().to_string(), None)
585 .await
586 .unwrap();
587
588 let streamed_dm1 = stream.next().await.unwrap();
589 assert!(streamed_dm1.is_ok());
590 assert_eq!(streamed_dm1.unwrap().group_id, dm1.group_id);
591
592 let dm2 = client2
596 .find_or_create_dm(client1.inbox_id().to_string(), None)
597 .await
598 .unwrap();
599
600 assert_ne!(dm1.group_id, dm2.group_id);
602
603 let wait = xmtp_configuration::ACTIVE_DATABASE_POLL_INTERVAL * 3;
607 let result = xmtp_common::time::timeout(wait, stream.next()).await;
608 assert!(result.is_err(), "Duplicate DM was unexpectedly streamed");
609 }
610
611 #[xmtp_common::timeout(std::time::Duration::from_secs(120))]
612 #[rstest::rstest]
613 #[case::five_dms(5)]
614 #[case::onehundred_dms(100)]
615 #[xmtp_common::test]
616 #[awt]
617
618 async fn test_many_concurrent_dm_invites(#[future] alix: ClientTester, #[case] dms: usize) {
619 let alix_inbox_id = Arc::new(alix.inbox_id().to_string());
620 let mut clients = vec![];
621 for _ in 0..dms {
622 let client =
623 Arc::new(ClientBuilder::new_test_client_vanilla(&generate_local_wallet()).await);
624 clients.push(client);
625 }
626
627 let stream = alix.stream_all_messages(None, None).await.unwrap();
628 for client in clients.iter().take(dms) {
629 xmtp_common::task::spawn({
630 let id = alix_inbox_id.clone();
631 let c = client.clone();
632 async move {
633 xmtp_common::time::sleep(std::time::Duration::from_millis(100)).await;
634 let dm = c.find_or_create_dm(id.as_ref(), None).await?;
635 dm.send_message(b"hi", SendMessageOpts::default()).await?;
636 Ok::<_, crate::client::ClientError>(())
637 }
638 });
639 }
640 futures::pin_mut!(stream);
641 for _ in 0..dms {
642 let _welcome = stream.next().await;
643 }
644 }
645}