1use crate::{
2 builder::DeviceSyncMode,
3 context::XmtpSharedContext,
4 groups::{
5 ConversationListItem, GroupError, MlsGroup, group_permissions::PolicySet,
6 welcome_sync::WelcomeService,
7 },
8 identity::{Identity, IdentityError, parse_credential},
9 identity_updates::{IdentityUpdateError, IdentityUpdates, load_identity_updates},
10 mls_store::{MlsStore, MlsStoreError},
11 subscriptions::{LocalEventError, LocalEvents, SyncWorkerEvent},
12 utils::VersionInfo,
13 worker::device_sync::{
14 DeviceSyncClient, preference_sync::PreferenceUpdate, worker::SyncMetric,
15 },
16 worker::{WorkerRunner, metrics::WorkerMetrics},
17};
18pub mod notifications;
19use crate::{
20 groups::welcome_sync::GroupSyncSummary,
21 identity_updates::{batch_get_association_state_with_verifier, get_creation_signature_kind},
22 messages::{
23 decoded_message::DecodedMessage,
24 enrichment::{EnrichMessageError, enrich_messages},
25 },
26};
27use itertools::Itertools;
28use openmls::prelude::tls_codec::Error as TlsCodecError;
29use std::{collections::HashMap, sync::Arc};
30use thiserror::Error;
31use tokio::sync::broadcast;
32use xmtp_api::{ApiClientWrapper, XmtpApi};
33use xmtp_common::{ErrorCode, Event, Retry, retry_async, retryable};
34use xmtp_configuration::{CREATE_PQ_KEY_PACKAGE_EXTENSION, KEY_PACKAGE_ROTATION_INTERVAL_NS};
35use xmtp_cryptography::signature::IdentifierValidationError;
36use xmtp_db::TransactionOutcome::Continue;
37use xmtp_db::{
38 ConnectionExt, NotFound, StorageError, TransactionOutcome, XmtpDb,
39 consent_record::{ConsentState, ConsentType, StoredConsentRecord},
40 db_connection::DbConnection,
41 encrypted_store::conversation_list::ConversationListItem as DbConversationListItem,
42 group::{ConversationType, GroupMembershipState, GroupQueryArgs},
43 group_message::StoredGroupMessage,
44 identity::StoredIdentity,
45 identity_cache::StoredIdentityKind,
46};
47use xmtp_db::{group::GroupQueryOrderBy, prelude::*};
48use xmtp_id::key_package::{KeyPackageVerificationError, VerifiedKeyPackageV2};
49use xmtp_id::{
50 AsIdRef, InboxId, InboxIdRef,
51 associations::{
52 AssociationError, AssociationState, Identifier, MemberIdentifier, SignatureError,
53 builder::{SignatureRequest, SignatureRequestError},
54 },
55 scw_verifier::SmartContractSignatureVerifier,
56};
57use xmtp_macro::log_event;
58use xmtp_mls_common::{
59 group::{DMMetadataOptions, GroupMetadataOptions},
60 group_metadata::DmMembers,
61 group_mutable_metadata::MessageDisappearingSettings,
62};
63use xmtp_proto::{
64 ConversionError,
65 api::HasStats,
66 api_client::{ApiStats, IdentityStats},
67};
68use xmtp_proto::{types::InstallationId, xmtp::identity::associations::IdentifierKind};
69
70use xmtp_proto::types::GroupId;
71#[derive(Clone, Copy, Default, Debug)]
73pub enum Network {
74 Local(&'static str),
75 #[default]
76 Dev,
77 Prod,
78}
79
80#[derive(Debug, Clone)]
82pub struct VisibilityConfirmationOptions {
83 pub timeout_ms: u64,
84}
85
86const REGISTRATION_INITIAL_BACKOFF: std::time::Duration = std::time::Duration::from_millis(50);
87const REGISTRATION_MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(1);
88
89impl Default for VisibilityConfirmationOptions {
90 fn default() -> Self {
91 Self { timeout_ms: 30_000 }
92 }
93}
94
95#[derive(Debug, Error, ErrorCode)]
96pub enum ClientError {
97 #[error(transparent)]
98 #[error_code(inherit)]
99 AddressValidation(#[from] IdentifierValidationError),
100 #[error("could not publish: {0}")]
104 PublishError(String),
105 #[error("storage error: {0}")]
109 Storage(#[from] StorageError),
110 #[error("API error: {0}")]
114 Api(#[from] xmtp_api::ApiError),
115 #[error("identity error: {0}")]
119 Identity(#[from] crate::identity::IdentityError),
120 #[error("TLS Codec error: {0}")]
124 TlsError(#[from] TlsCodecError),
125 #[error("key package verification: {0}")]
129 KeyPackageVerification(#[from] KeyPackageVerificationError),
130 #[error("Stream inconsistency error: {0}")]
134 StreamInconsistency(String),
135 #[error("Association error: {0}")]
139 Association(#[from] AssociationError),
140 #[error("signature validation error: {0}")]
144 SignatureValidation(#[from] SignatureError),
145 #[error(transparent)]
149 IdentityUpdate(#[from] IdentityUpdateError),
150 #[error(transparent)]
154 SignatureRequest(#[from] SignatureRequestError),
155 #[error(transparent)]
160 Group(Box<GroupError>),
161 #[error(transparent)]
165 LocalEvent(#[from] LocalEventError),
166 #[error(transparent)]
170 Db(#[from] xmtp_db::ConnectionError),
171 #[error("generic:{0}")]
175 Generic(String),
176 #[error(transparent)]
180 MlsStore(#[from] MlsStoreError),
181 #[error(transparent)]
185 EnrichMessage(#[from] EnrichMessageError),
186 #[error(transparent)]
190 Conversion(#[from] xmtp_proto::ConversionError),
191 #[error("Registration is not visible")]
195 RegistrationNotVisible,
196 #[error("client is closed")]
201 AlreadyClosed,
202 #[error("server configuration unavailable: {0}")]
209 ConfigurationUnavailable(#[source] Box<crate::server_configuration::ConfigurationFetchError>),
210 #[error("server configuration invalid: {0}")]
216 ConfigurationInvalid(#[from] xmtp_configuration::ServerConfigurationError),
217 #[error("this database is bound to backend {stored}, but {received} answered")]
223 BackendMismatch { stored: String, received: String },
224 #[error("client version {client} is below the {minimum} this backend requires")]
229 ClientVersionTooOld { client: String, minimum: String },
230 #[error(
235 "this backend requires authentication; required scopes: [{}]",
236 required_scopes.join(", ")
237 )]
238 AuthRequired { required_scopes: Vec<String> },
239 #[error(
244 "this backend does not accept smart contract wallet signatures on {chain}; accepted: [{}]",
245 accepted.join(", ")
246 )]
247 ChainNotAccepted {
248 chain: String,
249 accepted: Vec<String>,
250 },
251}
252
253impl ClientError {
254 pub fn db_needs_connection(&self) -> bool {
255 match self {
256 Self::Storage(s) => s.db_needs_connection(),
257 Self::Db(c) => c.db_needs_connection(),
258 _ => false,
259 }
260 }
261}
262
263impl From<NotFound> for ClientError {
264 fn from(value: NotFound) -> Self {
265 ClientError::Storage(StorageError::NotFound(value))
266 }
267}
268
269impl From<GroupError> for ClientError {
270 fn from(err: GroupError) -> ClientError {
271 ClientError::Group(Box::new(err))
272 }
273}
274
275impl xmtp_common::RetryableError for ClientError {
276 fn is_retryable(&self) -> bool {
277 match self {
278 ClientError::Group(group_error) => retryable!(group_error),
279 ClientError::Api(api_error) => retryable!(api_error),
280 ClientError::Storage(storage_error) => retryable!(storage_error),
281 ClientError::Db(db) => retryable!(db),
282 ClientError::SignatureValidation(e) => retryable!(e),
286 ClientError::ConfigurationUnavailable(e) => retryable!(e),
290 ClientError::Generic(err) => err.contains("database is locked"),
291 _ => false,
292 }
293 }
294}
295
296impl From<String> for ClientError {
297 fn from(value: String) -> Self {
298 Self::Generic(value)
299 }
300}
301
302impl From<&str> for ClientError {
303 fn from(value: &str) -> Self {
304 Self::Generic(value.to_string())
305 }
306}
307
308pub struct Client<Context> {
310 pub context: Context,
311 pub installation_id: InstallationId,
312 pub(crate) local_events: broadcast::Sender<LocalEvents>,
313 pub(crate) workers: Arc<WorkerRunner>,
314}
315
316impl<Context> Drop for Client<Context> {
317 fn drop(&mut self) {
318 log_event!(Event::ClientDropped, self.installation_id);
319 }
320}
321
322#[derive(Clone)]
323pub struct DeviceSync {
324 pub(crate) mode: DeviceSyncMode,
325}
326
327impl<Context: Clone> Clone for Client<Context> {
329 fn clone(&self) -> Self {
330 Self {
331 context: self.context.clone(),
332 installation_id: self.installation_id,
333 local_events: self.local_events.clone(),
334 workers: self.workers.clone(),
335 }
336 }
337}
338
339impl<Context> Client<Context>
340where
341 Context: XmtpSharedContext,
342{
343 pub fn identity_updates(&self) -> IdentityUpdates<&Context> {
344 IdentityUpdates::new(&self.context)
345 }
346
347 pub fn mls_store(&self) -> MlsStore<Context> {
348 MlsStore::new(self.context.clone())
349 }
350
351 pub fn scw_verifier(&self) -> Arc<Box<dyn SmartContractSignatureVerifier>> {
352 self.context.scw_verifier()
353 }
354
355 pub fn version_info(&self) -> &VersionInfo {
356 self.context.version_info()
357 }
358}
359
360impl<Context> Client<Context>
361where
362 Context: XmtpSharedContext,
363 Context::ApiClient: HasStats,
364{
365 pub fn api_stats(&self) -> ApiStats {
366 self.context.api().api_client.mls_stats()
367 }
368
369 pub fn identity_api_stats(&self) -> IdentityStats {
370 self.context.api().api_client.identity_stats()
371 }
372
373 pub fn clear_stats(&self) {
374 self.context.api().api_client.mls_stats().clear();
375 self.context.api().api_client.identity_stats().clear();
376 }
377}
378
379pub async fn inbox_addresses_with_verifier<ApiClient: XmtpApi>(
381 api_client: &ApiClientWrapper<ApiClient>,
382 conn: &impl DbQuery,
383 inbox_ids: Vec<InboxIdRef<'_>>,
384 scw_verifier: &impl SmartContractSignatureVerifier,
385) -> Result<Vec<AssociationState>, ClientError> {
386 load_identity_updates(api_client, conn, &inbox_ids).await?;
387 let state = batch_get_association_state_with_verifier(
388 conn,
389 &inbox_ids.into_iter().map(|i| (i, None)).collect::<Vec<_>>(),
390 scw_verifier,
391 )
392 .await?;
393 Ok(state)
394}
395
396impl<Context> Client<Context>
397where
398 Context: XmtpSharedContext + 'static,
399{
400 pub fn reconnect_db(&self) -> Result<(), ClientError> {
402 if self.context.is_closed() {
403 return Err(ClientError::AlreadyClosed);
404 }
405 self.context.db().reconnect().map_err(StorageError::from)?;
406 self.workers.spawn(self.context.clone());
407 Ok(())
408 }
409
410 pub async fn close(&self) -> Result<(), ClientError> {
417 if self.context.shutdown_complete() {
422 return Ok(());
423 }
424 self.context.cancellation_token().cancel();
425 self.context.close_message_delivery()?;
426 self.workers.shutdown().await;
427 self.context
428 .db()
429 .disconnect()
430 .map_err(xmtp_db::StorageError::from)?;
431 self.context.mark_shutdown_complete();
432 log_event!(Event::ClientClosed, self.installation_id);
433 Ok(())
434 }
435
436 pub async fn wait_for_sync_worker_init(&self) {
438 self.workers.wait_for_sync_worker_init().await;
439 }
440
441 pub fn sync_metrics(&self) -> Option<Arc<WorkerMetrics<SyncMetric>>> {
442 self.workers.sync_metrics()
443 }
444}
445
446impl<Context> Client<Context>
447where
448 Context: XmtpSharedContext,
449{
450 pub fn server_configuration(&self) -> &xmtp_configuration::ServerConfiguration {
454 self.context.server_configuration().configuration()
455 }
456
457 pub async fn refresh_server_configuration(
464 &self,
465 ) -> Result<xmtp_configuration::ServerConfiguration, ClientError> {
466 let handle = self.context.server_configuration();
467 let db = self.context.db();
468 let fetched =
469 match crate::server_configuration::fetch_and_store(self.context.api(), &db, handle)
470 .await
471 {
472 Ok(fetched) => fetched,
473 Err(error) => {
474 if handle.latched().is_some() {
481 self.context.cancellation_token().cancel();
482 }
483 return Err(error);
484 }
485 };
486 if let Err(ClientError::ClientVersionTooOld { client, minimum }) =
487 crate::server_configuration::check_minimum_version(
488 &fetched,
489 self.context.version_info().pkg_semver().semver(),
490 )
491 {
492 tracing::error!(
493 %client,
494 %minimum,
495 "the backend now requires a newer libxmtp than this client"
496 );
497 let error = handle.latch(
499 crate::server_configuration::ConfigurationLatch::ClientVersionTooOld {
500 client,
501 minimum,
502 },
503 );
504 self.context.cancellation_token().cancel();
505 return Err(error);
506 }
507 Ok(fetched)
508 }
509
510 pub fn installation_public_key(&self) -> InstallationId {
512 self.context.installation_id()
513 }
514 pub fn inbox_id(&self) -> InboxIdRef<'_> {
516 self.context.identity().inbox_id()
517 }
518
519 pub fn db(&self) -> <Context::Db as XmtpDb>::DbQuery {
522 self.context.db()
523 }
524
525 #[cfg(any(test, feature = "test-utils"))]
528 pub fn set_name(&self, name: &str) {
529 log_event!(Event::AssociateName, self.context.installation_id(), name);
530 }
531
532 pub fn device_sync_client(&self) -> DeviceSyncClient<Context> {
533 let metrics = self.context.sync_metrics();
534 DeviceSyncClient::new(
535 self.context.clone(),
536 metrics.unwrap_or(Arc::new(WorkerMetrics::new(self.context.installation_id()))),
537 )
538 }
539
540 pub async fn find_inbox_id_from_identifier(
542 &self,
543 conn: &impl DbQuery,
544 identifier: Identifier,
545 ) -> Result<Option<String>, ClientError> {
546 let results = self
547 .find_inbox_ids_from_identifiers(conn, &[identifier])
548 .await?;
549 Ok(results.into_iter().next().flatten())
550 }
551
552 pub(crate) async fn find_inbox_ids_from_identifiers(
555 &self,
556 conn: &impl DbQuery,
557 identifiers: &[Identifier],
558 ) -> Result<Vec<Option<String>>, ClientError> {
559 let ids: Vec<(String, StoredIdentityKind)> = identifiers
560 .iter()
561 .map(|i| {
562 Ok::<_, ConversionError>((
563 i.clone().to_string(),
564 StoredIdentityKind::try_from(IdentifierKind::from(i))?,
565 ))
566 })
567 .try_collect()?;
568 let cached_inbox_ids = conn.fetch_cached_inbox_ids(&ids)?;
569 let mut new_inbox_ids: HashMap<&Identifier, Option<String>> = HashMap::new();
570
571 let missing: Vec<_> = identifiers
572 .iter()
573 .filter(|ident| !cached_inbox_ids.contains_key(&format!("{ident}")))
574 .collect();
575
576 if !missing.is_empty() {
577 let requests = missing
578 .iter()
579 .map(|identifier| (*identifier).into())
580 .collect();
581 let results = self.context.api().get_inbox_ids(requests).await?;
582 new_inbox_ids = missing.into_iter().zip(results).collect();
583 }
584
585 let inbox_ids = identifiers
586 .iter()
587 .map(|ident| {
588 let cache_key = format!("{ident}");
589 if let Some(inbox_id) = cached_inbox_ids.get(&cache_key) {
590 return Some(inbox_id.clone());
591 }
592 new_inbox_ids.get(ident).cloned().flatten()
593 })
594 .collect();
595 Ok(inbox_ids)
596 }
597
598 pub fn inbox_sequence_id(
601 &self,
602 conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
603 ) -> Result<i64, StorageError> {
604 self.context
605 .identity()
606 .sequence_id(conn)
607 .map_err(Into::into)
608 }
609
610 pub async fn inbox_state(
612 &self,
613 refresh_from_network: bool,
614 ) -> Result<AssociationState, ClientError> {
615 let conn = self.context.db();
616 let inbox_id = self.inbox_id();
617 if refresh_from_network {
618 load_identity_updates(self.context.api(), &conn, &[inbox_id]).await?;
619 }
620 let identity_service = IdentityUpdates::new(&self.context);
621 let state = identity_service
622 .get_association_state(&conn, inbox_id, None)
623 .await?;
624 Ok(state)
625 }
626
627 pub async fn inbox_addresses(
629 &self,
630 refresh_from_network: bool,
631 inbox_ids: Vec<InboxIdRef<'_>>,
632 ) -> Result<Vec<AssociationState>, ClientError> {
633 let conn = self.context.db();
634 if refresh_from_network {
635 load_identity_updates(self.context.api(), &conn, &inbox_ids).await?;
636 }
637 let identity_service = IdentityUpdates::new(&self.context);
638 let state = identity_service
639 .batch_get_association_state(
640 &conn,
641 &inbox_ids.into_iter().map(|i| (i, None)).collect::<Vec<_>>(),
642 )
643 .await?;
644 Ok(state)
645 }
646
647 pub async fn fetch_inbox_updates_count(
651 &self,
652 refresh_from_network: bool,
653 inbox_ids: Vec<InboxIdRef<'_>>,
654 ) -> Result<HashMap<InboxId, u32>, ClientError> {
655 let conn = self.context.db();
656 if refresh_from_network {
657 load_identity_updates(self.context.api(), &conn, &inbox_ids).await?;
658 }
659 let inbox_id_strs = inbox_ids.to_vec();
660 let counts = conn.count_inbox_updates(&inbox_id_strs)?;
661 Ok(counts.into_iter().map(|(k, v)| (k, v as u32)).collect())
662 }
663
664 pub async fn fetch_own_inbox_updates_count(
668 &self,
669 refresh_from_network: bool,
670 ) -> Result<u32, ClientError> {
671 let inbox_id = self.inbox_id();
672 Ok(self
673 .fetch_inbox_updates_count(refresh_from_network, vec![inbox_id])
674 .await?
675 .get(inbox_id)
676 .copied()
677 .unwrap_or(0))
678 }
679
680 pub async fn inbox_creation_signature_kind(
690 &self,
691 inbox_id: InboxIdRef<'_>,
692 refresh_from_network: bool,
693 ) -> Result<Option<xmtp_id::associations::SignatureKind>, ClientError> {
694 let conn = self.context.db();
695
696 if refresh_from_network {
698 load_identity_updates(self.context.api(), &conn, &[inbox_id]).await?;
699 }
700
701 let verifier = self.context.scw_verifier();
702
703 let signature_kind = get_creation_signature_kind(&conn, verifier, inbox_id).await?;
704
705 Ok(signature_kind)
706 }
707
708 pub async fn set_consent_states(
711 &self,
712 records: &[StoredConsentRecord],
713 ) -> Result<(), ClientError> {
714 let conn = self.context.db();
715 let changed_records = conn.insert_or_replace_consent_records(records)?;
716
717 if !changed_records.is_empty() {
718 self.context.task_channels().wake_notifications();
719 let updates: Vec<_> = changed_records
720 .into_iter()
721 .map(PreferenceUpdate::Consent)
722 .collect();
723
724 let _ = self
726 .local_events
727 .send(LocalEvents::PreferencesChanged(updates.clone()));
728 let _ = self
729 .context
730 .worker_events()
731 .send(SyncWorkerEvent::SyncPreferences(updates));
732 }
733
734 Ok(())
735 }
736
737 pub async fn get_consent_state(
739 &self,
740 entity_type: ConsentType,
741 entity: String,
742 ) -> Result<ConsentState, ClientError> {
743 let conn = self.context.db();
744 let record = conn.get_consent_record(entity, entity_type)?;
745
746 match record {
747 Some(rec) => Ok(rec.state),
748 None => Ok(ConsentState::Unknown),
749 }
750 }
751
752 pub fn release_db_connection(&self) -> Result<(), ClientError> {
754 self.context
755 .db()
756 .disconnect()
757 .map_err(xmtp_db::StorageError::from)?;
758 Ok(())
759 }
760
761 pub fn identity(&self) -> &Identity {
763 self.context.identity()
764 }
765
766 fn ensure_identity_ready(&self) -> Result<(), ClientError> {
769 self.context.server_configuration().check()?;
773 if !self.identity().is_ready() {
774 tracing::warn!(
775 inbox_id = %self.inbox_id(),
776 "Operation attempted before register_identity() was called"
777 );
778 return Err(IdentityError::UninitializedIdentity.into());
779 }
780 Ok(())
781 }
782
783 pub fn create_group(
786 &self,
787 permissions_policy_set: Option<PolicySet>,
788 opts: Option<GroupMetadataOptions>,
789 ) -> Result<MlsGroup<Context>, ClientError> {
790 self.ensure_identity_ready()?;
791
792 let group: MlsGroup<Context> = MlsGroup::create_and_insert(
793 self.context.clone(),
794 ConversationType::Group,
795 permissions_policy_set.unwrap_or_default(),
796 opts.unwrap_or_default(),
797 None,
798 )?;
799
800 log_event!(
801 Event::CreatedGroup,
802 self.context.installation_id(),
803 group_id = group.group_id
804 );
805
806 let _ = self
808 .local_events
809 .send(LocalEvents::NewGroup(group.group_id));
810
811 Ok(group)
812 }
813
814 pub async fn create_group_with_identifiers(
816 &self,
817 account_identifiers: &[Identifier],
818 permissions_policy_set: Option<PolicySet>,
819 opts: Option<GroupMetadataOptions>,
820 ) -> Result<MlsGroup<Context>, ClientError> {
821 let group = self.create_group(permissions_policy_set, opts)?;
822
823 group.add_members_by_identity(account_identifiers).await?;
824
825 Ok(group)
826 }
827
828 #[tracing::instrument(level = "debug", skip_all, fields(size = inbox_ids.len()))]
829 pub async fn create_group_with_members(
830 &self,
831 inbox_ids: &[impl AsIdRef],
832 permissions_policy_set: Option<PolicySet>,
833 opts: Option<GroupMetadataOptions>,
834 ) -> Result<MlsGroup<Context>, ClientError> {
835 tracing::info!("creating group");
836 let group = self.create_group(permissions_policy_set, opts)?;
837
838 group.add_members(inbox_ids).await?;
839
840 Ok(group)
841 }
842
843 #[tracing::instrument(level = "debug", skip_all)]
845 async fn create_dm_by_inbox_id(
846 &self,
847 target_inbox_id: InboxId,
848 opts: Option<DMMetadataOptions>,
849 ) -> Result<MlsGroup<Context>, ClientError> {
850 let group: MlsGroup<Context> = MlsGroup::create_dm_and_insert(
851 &self.context,
852 GroupMembershipState::Allowed,
853 target_inbox_id.clone(),
854 opts.unwrap_or_default(),
855 None,
856 )?;
857
858 log_event!(
859 Event::CreatedDM,
860 self.context.installation_id(),
861 group_id = group.group_id,
862 target_inbox = target_inbox_id
863 );
864 let _ = self
866 .local_events
867 .send(LocalEvents::NewGroup(group.group_id));
868
869 group.add_members(&[target_inbox_id]).await?;
870
871 Ok(group)
872 }
873
874 pub async fn find_or_create_dm_by_identity(
876 &self,
877 target_identity: Identifier,
878 opts: Option<DMMetadataOptions>,
879 ) -> Result<MlsGroup<Context>, ClientError> {
880 self.ensure_identity_ready()?;
881 tracing::info!("finding or creating dm with address: {target_identity}");
882 let inbox_id = match self
883 .find_inbox_id_from_identifier(&self.context.db(), target_identity.clone())
884 .await?
885 {
886 Some(id) => id,
887 None => {
888 return Err(NotFound::InboxIdForAddress(target_identity.to_string()).into());
889 }
890 };
891
892 self.find_or_create_dm(inbox_id, opts).await
893 }
894
895 pub async fn find_or_create_dm(
897 &self,
898 inbox_id: impl AsIdRef,
899 opts: Option<DMMetadataOptions>,
900 ) -> Result<MlsGroup<Context>, ClientError> {
901 self.ensure_identity_ready()?;
902 let inbox_id = inbox_id.as_ref();
903 tracing::info!("finding or creating dm with inbox_id: {}", inbox_id);
904 let db = self.context.db();
905 let group = db.find_active_dm_group(&DmMembers {
906 member_one_inbox_id: self.inbox_id(),
907 member_two_inbox_id: inbox_id,
908 })?;
909
910 if let Some(group) = group {
911 return Ok(MlsGroup::new(
912 self.context.clone(),
913 group.id,
914 group.dm_id,
915 group.conversation_type,
916 group.created_at_ns,
917 ));
918 }
919 self.create_dm_by_inbox_id(inbox_id.to_string(), opts).await
920 }
921
922 pub fn group(&self, group_id: &GroupId) -> Result<MlsGroup<Context>, ClientError> {
927 MlsStore::new(self.context.clone())
928 .group(group_id)
929 .map_err(Into::into)
930 }
931
932 pub fn stitched_group(&self, group_id: &GroupId) -> Result<MlsGroup<Context>, ClientError> {
937 let conn = self.context.db();
938 let stored_group = conn.fetch_stitched(group_id)?;
939 stored_group
940 .map(|g| {
941 MlsGroup::new(
942 self.context.clone(),
943 g.id,
944 g.dm_id,
945 g.conversation_type,
946 g.created_at_ns,
947 )
948 })
949 .ok_or(NotFound::GroupById(*group_id))
950 .map_err(Into::into)
951 }
952
953 pub fn find_duplicate_dms_for_group(
955 &self,
956 group_id: &GroupId,
957 ) -> Result<Vec<MlsGroup<Context>>, ClientError> {
958 let (group, _) = MlsGroup::new_cached(self.context.clone(), group_id)?;
959 group.find_duplicate_dms()
960 }
961
962 pub fn group_disappearing_settings(
967 &self,
968 group_id: &GroupId,
969 ) -> Result<Option<MessageDisappearingSettings>, ClientError> {
970 let (group, _) = MlsGroup::new_cached(self.context.clone(), group_id)?;
971 Ok(group.disappearing_settings()?)
972 }
973
974 pub fn dm_group_from_target_inbox(
980 &self,
981 target_inbox_id: String,
982 ) -> Result<MlsGroup<Context>, ClientError> {
983 let conn = self.context.db();
984
985 let group = conn
986 .find_active_dm_group(&DmMembers {
987 member_one_inbox_id: self.inbox_id(),
988 member_two_inbox_id: &target_inbox_id,
989 })?
990 .ok_or(NotFound::DmByInbox(target_inbox_id))?;
991 Ok(MlsGroup::new(
992 self.context.clone(),
993 group.id,
994 group.dm_id,
995 group.conversation_type,
996 group.created_at_ns,
997 ))
998 }
999
1000 pub fn message(&self, message_id: Vec<u8>) -> Result<StoredGroupMessage, ClientError> {
1003 let conn = &mut self.context.db();
1004 let message = conn.get_group_message(&message_id)?;
1005 Ok(message.ok_or(NotFound::MessageById(message_id))?)
1006 }
1007
1008 #[xmtp_common::mls_span]
1011 pub fn message_v2(&self, message_id: Vec<u8>) -> Result<DecodedMessage, ClientError> {
1012 let conn = self.context.db();
1013 let message = conn
1014 .get_group_message(&message_id)?
1015 .ok_or_else(|| NotFound::MessageById(message_id.clone()))?;
1016
1017 let group_id = message.group_id;
1018
1019 let enriched = enrich_messages(conn, &group_id, vec![message])?;
1020
1021 enriched
1023 .into_iter()
1024 .next()
1025 .ok_or_else(|| ClientError::Generic("Failed to decode message".to_string()))
1027 }
1028
1029 pub fn delete_message(&self, message_id: Vec<u8>) -> Result<usize, ClientError> {
1033 let conn = self.context.db();
1034
1035 let msg = conn.get_group_message(&message_id)?;
1037
1038 let num_deleted = conn.delete_message_by_id(&message_id)?;
1039 if num_deleted > 0
1041 && let Some(message) = msg
1042 {
1043 let _ =
1044 self.context
1045 .local_events()
1046 .send(crate::subscriptions::LocalEvents::MsgsDeleted(vec![
1047 message,
1048 ]));
1049 }
1050
1051 Ok(num_deleted)
1052 }
1053
1054 pub fn find_groups(&self, args: GroupQueryArgs) -> Result<Vec<MlsGroup<Context>>, ClientError> {
1062 MlsStore::new(self.context.clone())
1063 .find_groups(args)
1064 .map_err(Into::into)
1065 }
1066
1067 pub fn list_conversations(
1068 &self,
1069 args: GroupQueryArgs,
1070 ) -> Result<Vec<ConversationListItem<Context>>, ClientError> {
1071 let mut args = args.clone();
1072 if args.order_by.is_none() {
1074 args.order_by = Some(GroupQueryOrderBy::LastActivity);
1075 }
1076 Ok(self
1077 .context
1078 .db()
1079 .fetch_conversation_list(args)?
1080 .into_iter()
1081 .map(|conversation_item: DbConversationListItem| {
1082 let message = conversation_item.message_id.and_then(|message_id| {
1083 let msg: Option<StoredGroupMessage> = Some(StoredGroupMessage {
1085 id: message_id,
1086 group_id: conversation_item.id,
1087 decrypted_message_bytes: conversation_item.decrypted_message_bytes?,
1088 sent_at_ns: conversation_item.sent_at_ns?,
1089 sender_installation_id: conversation_item.sender_installation_id?,
1090 sender_inbox_id: conversation_item.sender_inbox_id?,
1091 kind: conversation_item.kind?,
1092 delivery_status: conversation_item.delivery_status?,
1093 content_type: conversation_item.content_type?,
1094 version_major: conversation_item.version_major?,
1095 version_minor: conversation_item.version_minor?,
1096 authority_id: conversation_item.authority_id?,
1097 reference_id: None, sequence_id: conversation_item.sequence_id?,
1099 envelope_hash: None,
1100 expiry_ns: None,
1101 expire_at_ns: None, inserted_at_ns: 0, should_push: true, idempotency_key: conversation_item.sent_at_ns.unwrap_or_default().to_string(),
1107 });
1108 if msg.is_none() {
1109 tracing::warn!("tried listing message, but message had missing fields so it was skipped");
1110 }
1111 msg
1112 });
1113
1114 ConversationListItem {
1115 group: MlsGroup::new(
1116 self.context.clone(),
1117 conversation_item.id,
1118 conversation_item.dm_id,
1119 conversation_item.conversation_type,
1120 conversation_item.created_at_ns,
1121 ),
1122 last_message: message,
1123 is_commit_log_forked: conversation_item.is_commit_log_forked,
1124 }
1125 })
1126 .collect())
1127 }
1128
1129 #[xmtp_common::mls_span]
1132 pub async fn register_identity(
1133 &self,
1134 signature_request: SignatureRequest,
1135 ) -> Result<(), ClientError> {
1136 tracing::info!("registering identity");
1137 self.context.server_configuration().check()?;
1139
1140 let stored_identity: Option<StoredIdentity> = self.context.db().fetch(&())?;
1142 if stored_identity.is_some() {
1143 tracing::info!("Identity already registered, skipping");
1144 self.identity().set_ready();
1145 return Ok(());
1146 }
1147
1148 let (kp_bytes, history_id) = self.identity().generate_and_store_key_package(
1150 self.context.mls_storage(),
1151 CREATE_PQ_KEY_PACKAGE_EXTENSION,
1152 )?;
1153
1154 let identity_update = signature_request
1156 .build_identity_update()
1157 .map_err(IdentityUpdateError::from)?;
1158 identity_update
1159 .to_verified(&self.context.scw_verifier())
1160 .await?;
1161
1162 let key_package_meta = self.context.api().upload_key_package(kp_bytes).await?;
1164 let key_package_cursor = key_package_meta
1165 .cursor
1166 .filter(|cursor| cursor.sequence_id > 0)
1167 .ok_or(xmtp_api::ApiError::InvalidResponse(
1168 "key package publish cursor",
1169 ))?;
1170
1171 let registration_cursor = crate::identity_updates::publish_with_conflict_retry(
1173 self.context.api(),
1174 &self.context.db(),
1175 identity_update,
1176 &self.context.scw_verifier(),
1177 )
1178 .await?;
1179
1180 let inbox_id = self.inbox_id().to_string();
1182 retry_async!(
1183 Retry::default(),
1184 (async {
1185 load_identity_updates(self.context.api(), &self.context.db(), &[inbox_id.as_str()])
1186 .await
1187 })
1188 )?;
1189
1190 crate::state_tx::state_write(self.context.mls_storage(), |tx| {
1192 let storage = tx.storage();
1193 storage.db().record_key_package_publication(
1194 history_id,
1195 xmtp_proto::types::Cursor(key_package_cursor.sequence_id),
1196 )?;
1197 storage
1198 .db()
1199 .reset_key_package_rotation_queue(KEY_PACKAGE_ROTATION_INTERVAL_NS)?;
1200 Ok::<_, StorageError>(Continue(()))
1201 })
1202 .map(TransactionOutcome::into_continued)?;
1203
1204 let mut stored_identity = StoredIdentity::try_from(self.identity())?;
1206 stored_identity.registration_cursor_sequence_id = Some(registration_cursor.0 as i64);
1207 stored_identity.store(&self.context.db())?;
1208 self.identity().set_ready();
1209 Ok(())
1210 }
1211
1212 pub async fn wait_for_registration_visible(
1215 &self,
1216 options: VisibilityConfirmationOptions,
1217 ) -> Result<(), ClientError> {
1218 use xmtp_common::time::{Duration, sleep, timeout};
1219 if !self.identity().is_ready() {
1220 return Err(ClientError::RegistrationNotVisible);
1221 }
1222 let stored: Option<StoredIdentity> = self.context.db().fetch(&())?;
1223 let sequence_id = stored
1224 .and_then(|identity| identity.registration_cursor_sequence_id)
1225 .and_then(|sequence_id| u64::try_from(sequence_id).ok())
1226 .filter(|sequence_id| *sequence_id != 0)
1227 .ok_or(ClientError::RegistrationNotVisible)?;
1228 timeout(Duration::from_millis(options.timeout_ms), async {
1229 let mut delay = REGISTRATION_INITIAL_BACKOFF;
1230 let inbox = hex::decode(self.inbox_id())
1231 .map_err(|_| xmtp_api::ApiError::InvalidRequest("registration inbox id"))?;
1232 let topic = xmtp_proto::types::Topic::new_identity_update(inbox);
1233 xmtp_proto::types::Topic::parse(&topic)?;
1234 loop {
1235 let heads = self
1236 .context
1237 .api()
1238 .newest_topic_cursors(vec![topic.clone()])
1239 .await?;
1240 let head = heads
1241 .get(&topic)
1242 .ok_or(xmtp_api::ApiError::InvalidResponse(
1243 "registration identity head",
1244 ))?;
1245 if head.0 >= sequence_id {
1246 return Ok(());
1247 }
1248 sleep(delay).await;
1249 delay = (delay * 2).min(REGISTRATION_MAX_BACKOFF);
1250 }
1251 })
1252 .await
1253 .map_err(|_| ClientError::RegistrationNotVisible)?
1254 }
1255
1256 pub fn queue_key_rotation(&self) -> Result<(), ClientError> {
1258 crate::worker::key_package_maintenance::queue_key_rotation(&self.context)?;
1259 Ok(())
1260 }
1261
1262 pub async fn rotate_and_upload_key_package(&self) -> Result<(), ClientError> {
1265 self.ensure_identity_ready()?;
1269 self.identity()
1270 .rotate_and_upload_key_package(
1271 self.context.api(),
1272 self.context.mls_storage(),
1273 CREATE_PQ_KEY_PACKAGE_EXTENSION,
1274 )
1275 .await?;
1276 crate::worker::key_package_maintenance::nudge_deletion(&self.context)?;
1279
1280 Ok(())
1281 }
1282
1283 #[tracing::instrument(skip_all)]
1285 pub async fn get_key_packages_for_installation_ids(
1286 &self,
1287 installation_ids: Vec<Vec<u8>>,
1288 ) -> Result<
1289 HashMap<Vec<u8>, Result<VerifiedKeyPackageV2, KeyPackageVerificationError>>,
1290 ClientError,
1291 > {
1292 MlsStore::new(self.context.clone())
1293 .get_key_packages_for_installation_ids(installation_ids)
1294 .await
1295 .map_err(Into::into)
1296 }
1297
1298 #[tracing::instrument(skip_all)]
1301 pub async fn sync_welcomes(&self) -> Result<Vec<MlsGroup<Context>>, GroupError> {
1302 self.ensure_identity_ready()?;
1303 WelcomeService::new(self.context.clone())
1304 .sync_welcomes()
1305 .await
1306 }
1307
1308 #[tracing::instrument(err, skip_all, fields(operation = "sync_all_groups"))]
1311 pub async fn sync_all_groups(
1312 &self,
1313 groups: Vec<MlsGroup<Context>>,
1314 ) -> Result<GroupSyncSummary, GroupError> {
1315 self.ensure_identity_ready()?;
1316 WelcomeService::new(self.context.clone())
1317 .sync_all_groups(groups)
1318 .await
1319 }
1320
1321 #[xmtp_common::mls_span]
1324 pub async fn sync_all_welcomes_and_groups(
1325 &self,
1326 consent_states: Option<Vec<ConsentState>>,
1327 ) -> Result<GroupSyncSummary, GroupError> {
1328 self.ensure_identity_ready()?;
1329 WelcomeService::new(self.context.clone())
1330 .sync_all_welcomes_and_groups(consent_states)
1331 .await
1332 }
1333
1334 pub async fn unstick_paused_groups(&self) -> Result<usize, GroupError> {
1344 self.ensure_identity_ready()?;
1345 WelcomeService::new(self.context.clone())
1346 .unstick_paused_groups()
1347 .await
1348 }
1349
1350 pub async fn sync_all_welcomes_and_device_sync_groups(
1351 &self,
1352 ) -> Result<GroupSyncSummary, ClientError> {
1353 self.sync_welcomes().await?;
1354 self.sync_all_device_sync_groups().await
1355 }
1356
1357 pub async fn sync_all_device_sync_groups(&self) -> Result<GroupSyncSummary, ClientError> {
1358 let groups = self
1359 .context
1360 .db()
1361 .all_sync_groups()?
1362 .into_iter()
1363 .map(|g| {
1364 MlsGroup::new(
1365 self.context.clone(),
1366 g.id,
1367 g.dm_id,
1368 g.conversation_type,
1369 g.created_at_ns,
1370 )
1371 })
1372 .collect();
1373
1374 Ok(self.sync_all_groups(groups).await?)
1375 }
1376
1377 pub async fn validate_credential_against_network(
1384 &self,
1385 conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
1386 credential: &[u8],
1387 installation_pub_key: Vec<u8>,
1388 ) -> Result<InboxId, ClientError> {
1389 let inbox_id = parse_credential(credential)?;
1390 let association_state = IdentityUpdates::new(&self.context)
1391 .get_latest_association_state(conn, &inbox_id)
1392 .await?;
1393 let ident = MemberIdentifier::installation(installation_pub_key);
1394
1395 match association_state.get(&ident) {
1396 Some(_) => Ok(inbox_id),
1397 None => Err(IdentityError::InstallationIdNotFound(inbox_id).into()),
1398 }
1399 }
1400
1401 pub async fn can_message(
1409 &self,
1410 account_identifiers: &[Identifier],
1411 ) -> Result<HashMap<Identifier, bool>, ClientError> {
1412 self.context.server_configuration().check()?;
1416 let requests = account_identifiers.iter().map(Into::into).collect();
1417
1418 let results = self.context.api().get_inbox_ids(requests).await?;
1419 Ok(account_identifiers
1420 .iter()
1421 .cloned()
1422 .zip(results.into_iter().map(|inbox_id| inbox_id.is_some()))
1423 .collect())
1424 }
1425}
1426
1427#[cfg(test)]
1428pub(crate) mod tests;