1use crate::{
2 StorageError, XmtpApi,
3 attachments::AttachmentRuntime,
4 client::{Client, ClientError, DeviceSync},
5 context::{CoreContext, XmtpMlsLocalContext, XmtpSharedContext},
6 groups::change_callbacks::UnstableChangeCallbacks,
7 identity::{Identity, IdentityStrategy},
8 identity_updates::load_identity_updates,
9 mutex_registry::MutexRegistry,
10 server_configuration::ServerConfigurationHandle,
11 utils::VersionInfo,
12 worker::{WorkerRunner, tasks::TaskWorker},
13 worker::{device_sync::worker::SyncWorker, disappearing_messages::DisappearingMessagesWorker},
14};
15use futures::FutureExt;
16use prost::Message as _;
17use std::sync::Arc;
18use std::sync::atomic::AtomicBool;
19use thiserror::Error;
20use tokio_util::sync::CancellationToken;
21use tracing::debug;
22use xmtp_api::ApiClientWrapper;
23use xmtp_api_backend::TrackedStatsClient;
24use xmtp_common::snippet::Snippet;
25use xmtp_common::{ErrorCode, Event, Retry};
26use xmtp_cryptography::signature::IdentifierValidationError;
27use xmtp_db::{XmtpDb, sql_key_store::SqlKeyStore};
28use xmtp_db::{XmtpMlsStorageProvider, prelude::*};
29use xmtp_id::scw_verifier::SmartContractSignatureVerifier;
30use xmtp_macro::log_event;
31use xmtp_proto::api_client::XmtpBackendClient;
32use xmtp_proto::xmtp::mls::database::{
33 ProcessPendingSelfRemove, Task as TaskProto, task::Task as TaskKind,
34};
35
36pub use crate::core::config::{DeviceSyncMode, ForkRecoveryOpts, ForkRecoveryPolicy};
37
38type ContextParts<Api, S, Db> = Arc<XmtpMlsLocalContext<Api, Db, S>>;
39#[cfg(not(target_arch = "wasm32"))]
41type LocationStoreOpener<Db> = Box<
42 dyn FnOnce(
43 crate::storage_location::ResolvedPaths,
44 ) -> xmtp_common::BoxDynFuture<'static, Result<Db, ClientBuilderError>>
45 + Send
46 + Sync,
47>;
48#[cfg(target_arch = "wasm32")]
49type LocationStoreOpener<Db> = Box<
50 dyn FnOnce(
51 crate::storage_location::ResolvedPaths,
52 ) -> xmtp_common::BoxDynFuture<'static, Result<Db, ClientBuilderError>>,
53>;
54
55#[cfg(not(target_arch = "wasm32"))]
56fn open_location_store(
57 paths: crate::storage_location::ResolvedPaths,
58 key: xmtp_db::EncryptionKey,
59) -> xmtp_common::BoxDynFuture<'static, Result<xmtp_db::DefaultStore, ClientBuilderError>> {
60 Box::pin(async move {
61 let parent = paths
62 .db_path
63 .parent()
64 .ok_or(crate::storage_location::StorageLocationError::InboxId)?;
65 xmtp_attachments::create_private_directory(parent)
66 .await
67 .map_err(crate::storage_location::StorageLocationError::from)?;
68 let db = xmtp_db::NativeDb::builder()
69 .persistent(paths.db_path.to_string_lossy().into_owned())
70 .key(key)
71 .build()?;
72 Ok(xmtp_db::EncryptedMessageStore::new(db)?)
73 })
74}
75
76fn admit_configuration<ApiClient: XmtpBackendClient>(
79 api_client: &mut ApiClientWrapper<ApiClient>,
80 configuration: &xmtp_configuration::ServerConfiguration,
81 client_version: &semver::Version,
82) -> Result<(), ClientError> {
83 crate::server_configuration::check_minimum_version(configuration, client_version)?;
84 if configuration.auth.enabled && !api_client.has_credential_source() {
86 return Err(ClientError::AuthRequired {
87 required_scopes: configuration.auth.required_scopes.clone(),
88 });
89 }
90 let snapshot = Arc::new(configuration.clone());
91 api_client
92 .api_client
93 .set_limits(Arc::new(snapshot.limits.clone()));
94 api_client.set_configuration(snapshot);
95 Ok(())
96}
97
98#[derive(Error, Debug, ErrorCode)]
99pub enum ClientBuilderError {
100 #[error(transparent)]
103 #[error_code("StorageLocation")]
104 StorageLocation(#[from] crate::storage_location::StorageLocationError),
105 #[error(transparent)]
108 #[error_code("Attachment")]
109 Attachment(#[from] crate::attachments::AttachmentClientError),
110 #[error(transparent)]
111 #[error_code(inherit)]
112 AddressValidation(#[from] IdentifierValidationError),
113 #[error("Missing parameter: {parameter}")]
117 MissingParameter { parameter: &'static str },
118 #[error(transparent)]
122 ClientError(#[from] crate::client::ClientError),
123 #[error("Storage Error")]
127 StorageError(#[from] StorageError),
128 #[error(transparent)]
132 Identity(#[from] crate::identity::IdentityError),
133 #[error(transparent)]
137 WrappedApiError(#[from] xmtp_api::ApiError),
138 #[error(transparent)]
142 GroupError(#[from] Box<crate::groups::GroupError>),
143 #[error(transparent)]
147 DeviceSync(#[from] Box<crate::worker::device_sync::DeviceSyncError>),
148 #[error("Offline build failed, builder tried to access the network")]
152 OfflineBuildFailed,
153}
154
155impl From<crate::worker::device_sync::DeviceSyncError> for ClientBuilderError {
156 fn from(value: crate::worker::device_sync::DeviceSyncError) -> Self {
157 ClientBuilderError::DeviceSync(Box::new(value))
158 }
159}
160
161impl From<crate::groups::GroupError> for ClientBuilderError {
162 fn from(value: crate::groups::GroupError) -> Self {
163 ClientBuilderError::GroupError(Box::new(value))
164 }
165}
166
167pub struct ClientBuilder<ApiClient, S, Db = xmtp_db::DefaultStore> {
168 pub(crate) deployment_recorder: Option<crate::storage_location::DeploymentRecorder>,
169 pub(crate) data_location: Option<crate::storage_location::StorageLocation>,
170 pub(crate) location_store_opener: Option<LocationStoreOpener<Db>>,
171 pub(crate) mls_storage_factory: Option<fn(&Db) -> S>,
172 pub(crate) storage_location_selected: bool,
173 pub(crate) storage_location_conflict: bool,
174 pub(crate) attachments_dir: Option<std::path::PathBuf>,
175 pub(crate) attachment_options: xmtp_attachments::AttachmentOptions,
176 pub(crate) api_client: Option<ApiClient>,
177 pub(crate) identity: Option<Identity>,
178 pub(crate) store: Option<Db>,
179 pub(crate) identity_strategy: IdentityStrategy,
180 pub(crate) scw_verifier: Option<Box<dyn SmartContractSignatureVerifier>>,
181 pub(crate) custom_scw_verifier: bool,
183 pub(crate) device_sync_worker_mode: DeviceSyncMode,
184 pub(crate) fork_recovery_opts: Option<ForkRecoveryOpts>,
185 pub(crate) change_callbacks: UnstableChangeCallbacks,
187 pub(crate) stream_policy: crate::subscriptions::policy::StreamPolicy,
188 pub(crate) incoming_factory:
189 Option<Arc<dyn crate::subscriptions::incoming::SubscriptionFactory>>,
190 pub(crate) version_info: VersionInfo,
191 pub(crate) allow_offline: bool,
192 pub(crate) disable_commit_log_worker: bool,
193 pub(crate) mls_storage: Option<S>,
194 pub(crate) disable_workers: bool,
195 pub(crate) worker_config: crate::worker::WorkerConfig,
196 pub(crate) config_provider: Option<Arc<dyn xmtp_configuration::ConfigProvider>>,
200 #[cfg(any(test, feature = "test-utils"))]
201 application_components_for_test:
202 Option<Vec<xmtp_configuration::ApplicationComponentDefinition>>,
203}
204
205impl Client<()> {
206 pub fn builder(strategy: IdentityStrategy) -> ClientBuilder<(), ()> {
208 ClientBuilder::<(), ()>::new(strategy)
209 }
210}
211
212impl<ApiClient, S, Db> ClientBuilder<ApiClient, S, Db> {
213 #[cfg(any(test, feature = "test-hooks"))]
215 pub fn stream_policy(mut self, settings: crate::subscriptions::policy::StreamPolicy) -> Self {
216 self.stream_policy = settings;
217 self
218 }
219
220 #[tracing::instrument(level = "trace", skip_all)]
221 pub fn new(identity_strategy: IdentityStrategy) -> Self {
222 Self {
223 deployment_recorder: None,
224 data_location: None,
225 location_store_opener: None,
226 mls_storage_factory: None,
227 storage_location_selected: false,
228 storage_location_conflict: false,
229 attachments_dir: None,
230 attachment_options: xmtp_attachments::AttachmentOptions::default(),
231 identity_strategy,
232 api_client: None,
233 identity: None,
234 store: None,
235 scw_verifier: None,
236 custom_scw_verifier: false,
237 device_sync_worker_mode: DeviceSyncMode::Enabled,
238 fork_recovery_opts: None,
239 change_callbacks: UnstableChangeCallbacks::default(),
240 stream_policy: crate::subscriptions::policy::StreamPolicy::default(),
241 incoming_factory: None,
242 version_info: VersionInfo::default(),
243 allow_offline: false,
244 disable_commit_log_worker: false,
245 mls_storage: None,
246 disable_workers: false,
247 worker_config: crate::worker::WorkerConfig::default(),
248 config_provider: None,
249 #[cfg(any(test, feature = "test-utils"))]
250 application_components_for_test: None,
251 }
252 }
253}
254
255#[cfg(any(test, feature = "test-utils"))]
256impl<ApiClient, S, Db> ClientBuilder<ApiClient, S, Db>
257where
258 ApiClient: Clone,
259 Db: Clone,
260 S: Clone,
261{
262 pub fn from_client(
263 client: Client<ContextParts<ApiClient, S, Db>>,
264 ) -> ClientBuilder<ApiClient, S, Db> {
265 let cloned_api: ApiClient = client.context.api_client.api_client.raw_for_test().clone();
266 ClientBuilder {
267 deployment_recorder: None,
268 data_location: None,
269 location_store_opener: None,
270 mls_storage_factory: None,
271 storage_location_selected: false,
272 storage_location_conflict: false,
273 attachments_dir: client.context.features.attachments.dir.clone(),
274 attachment_options: client.context.features.attachments.options.clone(),
275 api_client: Some(cloned_api),
276 identity: Some(client.context.identity.clone()),
277 store: Some(client.context.store.clone()),
278 identity_strategy: IdentityStrategy::CachedOnly,
279 scw_verifier: Some(Box::new(client.context.scw_verifier.clone())),
280 custom_scw_verifier: client.context.server_configuration.custom_verifier(),
281 device_sync_worker_mode: client.context.device_sync.mode,
282 fork_recovery_opts: Some(client.context.fork_recovery_opts.clone()),
283 change_callbacks: client.context.change_callbacks.clone(),
284 stream_policy: client.context.features.incoming.policy().clone(),
285 incoming_factory: client.context.features.incoming.original_factory.clone(),
286 version_info: client.context.version_info.clone(),
287 allow_offline: false,
288 disable_commit_log_worker: false,
289 mls_storage: Some(client.context.mls_storage.clone()),
290 disable_workers: false,
291 worker_config: client.context.worker_config.clone(),
292 config_provider: None,
293 #[cfg(any(test, feature = "test-utils"))]
294 application_components_for_test: None,
295 }
296 }
297}
298
299fn backfill_pending_self_remove_tasks<C>(context: &C) -> Result<(), StorageError>
307where
308 C: XmtpSharedContext,
309{
310 let db = context.db();
311 for raw_id in db.get_groups_have_pending_leave_request()? {
312 let Ok(group_id) = xmtp_proto::types::GroupId::try_from(raw_id.as_slice()) else {
313 continue;
314 };
315 let now = xmtp_common::time::now_ns();
316 let proto = TaskProto {
317 task: Some(TaskKind::ProcessPendingSelfRemove(
318 ProcessPendingSelfRemove {
319 group_id: group_id.to_vec(),
320 },
321 )),
322 };
323 let task = xmtp_db::tasks::NewTask::builder()
324 .originating_message_sequence_id(0)
325 .created_at_ns(now)
326 .next_attempt_at_ns(now)
327 .build(proto)?;
328 db.upsert_pending_self_remove_task(&group_id, task)?;
332 }
333 Ok(())
334}
335
336impl<ApiClient, S, Db> ClientBuilder<ApiClient, S, Db> {
339 pub fn build_offline(self) -> Result<Client<ContextParts<ApiClient, S, Db>>, ClientBuilderError>
342 where
343 ApiClient: XmtpApi + 'static,
344 Db: xmtp_db::XmtpDb + 'static,
345 S: XmtpMlsStorageProvider + 'static,
346 {
347 self.build()
348 .now_or_never()
349 .ok_or(ClientBuilderError::OfflineBuildFailed)
350 .flatten()
351 }
352
353 #[tracing::instrument(err, skip_all, fields(operation = "mls.build_client"))]
363 pub async fn build(self) -> Result<Client<ContextParts<ApiClient, S, Db>>, ClientBuilderError>
364 where
365 ApiClient: XmtpApi + 'static,
366 Db: xmtp_db::XmtpDb + 'static,
367 S: XmtpMlsStorageProvider + 'static,
368 {
369 let ClientBuilder {
370 mut deployment_recorder,
371 data_location,
372 location_store_opener,
373 mls_storage_factory,
374 storage_location_conflict,
375 mut attachments_dir,
376 attachment_options,
377 mut api_client,
378 identity,
379 mut store,
380 mut identity_strategy,
381 mut scw_verifier,
382 custom_scw_verifier,
383
384 device_sync_worker_mode,
385 fork_recovery_opts,
386 change_callbacks,
387 stream_policy,
388 incoming_factory,
389 version_info,
390 allow_offline,
391 disable_commit_log_worker,
392 mut mls_storage,
393 disable_workers,
394 worker_config,
395 config_provider,
396 #[cfg(any(test, feature = "test-utils"))]
397 application_components_for_test,
398 ..
399 } = self;
400
401 if storage_location_conflict {
402 return Err(crate::storage_location::StorageLocationError::ConflictingStore.into());
403 }
404
405 let api_client = api_client
406 .take()
407 .ok_or(ClientBuilderError::MissingParameter {
408 parameter: "api_client",
409 })?;
410
411 let mut scw_verifier = scw_verifier
412 .take()
413 .ok_or(ClientBuilderError::MissingParameter {
414 parameter: "scw_verifier",
415 })?;
416
417 let mut api_client = ApiClientWrapper::new(api_client, Retry::default());
418 let config_provider = config_provider.map(ServerConfigurationHandle::new);
419 let mut data_dir_opened_identifier = None;
420 if let Some(location) = data_location {
421 use crate::storage_location::StorageLocationError;
422 let data_dir = matches!(
423 location,
424 crate::storage_location::StorageLocation::DataDir(_)
425 );
426 let look_up_inbox = data_dir && identity_strategy.inbox_id().is_none();
429 if look_up_inbox
430 && (allow_offline
431 || !matches!(
432 identity_strategy,
433 IdentityStrategy::CreateForIdentifier { .. }
434 ))
435 {
436 return Err(StorageLocationError::InboxId.into());
437 }
438 let backend_url = api_client.backend_url().unwrap_or_default().to_owned();
439 if data_dir && backend_url.trim_end_matches('/').is_empty() {
440 return Err(StorageLocationError::BackendUrl.into());
441 }
442 let recorder = location.recorder(&backend_url);
443 let recorded = match &recorder {
444 Some(recorder) => recorder.lookup().await?,
445 None => None,
446 };
447 let mut fetched = None;
448 let opened_identifier = match recorded {
453 Some(identifier) if !look_up_inbox => Some(identifier),
454 _ if !data_dir => None,
455 recorded => {
456 if allow_offline {
457 return Err(StorageLocationError::OfflineMissingDeployment.into());
458 }
459 let response = api_client.get_configuration().await.map_err(|error| {
460 ClientBuilderError::ClientError(ClientError::ConfigurationUnavailable(
461 Box::new(crate::server_configuration::ConfigurationFetchError::Api(
462 error,
463 )),
464 ))
465 })?;
466 let configuration = crate::server_configuration::validated(&response)
467 .map_err(ClientError::from)?;
468 let identifier = configuration.identifier.clone();
469 if let Some(recorded) = recorded
470 && recorded != identifier
471 {
472 return Err(ClientError::BackendMismatch {
473 stored: recorded,
474 received: identifier,
475 }
476 .into());
477 }
478 if look_up_inbox {
479 let admission = config_provider
480 .as_ref()
481 .map(ServerConfigurationHandle::configuration)
482 .unwrap_or(&configuration);
483 if admission.identifier != identifier {
484 return Err(StorageLocationError::DeploymentMismatch.into());
485 }
486 admit_configuration(
487 &mut api_client,
488 admission,
489 version_info.pkg_semver().semver(),
490 )?;
491 }
492 fetched = Some((identifier.clone(), response));
493 Some(identifier)
494 }
495 };
496 if look_up_inbox {
499 identity_strategy = identity_strategy.with_inbox(&api_client).await?;
500 }
501 let paths = location.resolve_identifier(
502 identity_strategy.inbox_id().unwrap_or_default(),
503 opened_identifier.as_deref().unwrap_or_default(),
504 )?;
505 let opener = location_store_opener.ok_or(StorageLocationError::ConflictingStore)?;
506 let opened = opener(paths.clone()).await?;
507 if let Some((identifier, response)) = fetched {
508 if opened.db().server_configuration()?.is_none() {
511 opened.db().store_server_configuration(
512 &identifier,
513 backend_url.trim_end_matches('/'),
514 &response.encode_to_vec(),
515 xmtp_common::time::now_ns(),
516 )?;
517 }
518 if let Some(recorder) = &recorder {
519 recorder.record(&identifier).await?;
520 }
521 }
522 store = Some(opened);
523 attachments_dir = Some(paths.attachments_dir);
524 if data_dir {
525 data_dir_opened_identifier = opened_identifier.clone();
526 }
527 deployment_recorder = recorder.map(|recorder| match opened_identifier {
528 Some(identifier) => recorder.for_opened_identifier(identifier),
529 None => recorder,
530 });
531 }
532 let store = store
533 .take()
534 .ok_or(ClientBuilderError::MissingParameter { parameter: "store" })?;
535 let mls_storage = mls_storage
536 .take()
537 .or_else(|| mls_storage_factory.map(|factory| factory(&store)))
538 .ok_or(ClientBuilderError::MissingParameter {
539 parameter: "mls_storage",
540 })?;
541
542 let conn = store.db();
543
544 let has_config_provider = config_provider.is_some();
550 let server_configuration = match config_provider {
551 Some(provider) => provider,
552 None => crate::server_configuration::resolve(&api_client, &conn, allow_offline)
553 .await
554 .map_err(ClientError::from)?,
555 };
556
557 #[cfg(any(test, feature = "test-utils"))]
558 let server_configuration = match application_components_for_test {
559 Some(components) => server_configuration.with_application_components(components),
560 None => server_configuration,
561 };
562
563 if has_config_provider && let Some(opened_identifier) = data_dir_opened_identifier {
564 let provider_identifier = &server_configuration.configuration().identifier;
565 let stored_identifier = conn.server_configuration()?.map(|row| row.identifier);
566 if provider_identifier != &opened_identifier
567 || stored_identifier.as_deref().is_some_and(|identifier| {
568 !identifier.is_empty() && identifier != provider_identifier
569 })
570 {
571 return Err(
572 crate::storage_location::StorageLocationError::DeploymentMismatch.into(),
573 );
574 }
575 }
576
577 let server_configuration = server_configuration.with_chain_restriction(custom_scw_verifier);
578 if server_configuration.requires_preflight() {
579 api_client.api_client.require_preflight();
580 }
581 if let Some(recorder) = deployment_recorder {
582 if !has_config_provider && !server_configuration.configuration().identifier.is_empty() {
583 recorder
584 .record(&server_configuration.configuration().identifier)
585 .await?;
586 }
587 server_configuration.set_deployment_recorder(recorder);
588 }
589
590 admit_configuration(
591 &mut api_client,
592 server_configuration.configuration(),
593 version_info.pkg_semver().semver(),
594 )?;
595 let incoming_factory = incoming_factory
598 .map(|factory| factory.bind_limits(api_client.limits()).unwrap_or(factory));
599 if !custom_scw_verifier {
602 scw_verifier = Box::new(api_client.clone());
603 }
604
605 let identifier_check = match &identity_strategy {
606 IdentityStrategy::CreateIfNotFound {
607 identifier, nonce, ..
608 }
609 | IdentityStrategy::CreateForIdentifier { identifier, nonce } => {
610 Some((identifier.clone(), *nonce))
611 }
612 _ => None,
613 };
614 let mut identity = if let Some(identity) = identity {
615 identity
616 } else {
617 identity_strategy
618 .initialize_identity(&api_client, &mls_storage, &scw_verifier)
619 .await?
620 };
621
622 if let Some(request) = identity.signature_request.as_mut() {
626 server_configuration.restrict(request);
627 }
628
629 debug!(
630 inbox_id = identity.inbox_id(),
631 installation_id = identity.installation_keys.public_bytes().snippet(),
632 "Initialized identity"
633 );
634 if !allow_offline {
635 load_identity_updates(
637 &api_client,
638 &conn,
639 vec![identity.inbox_id.as_str()].as_slice(),
640 )
641 .await
642 .map_err(ClientError::from)?;
643 }
644 if let Some((identifier, nonce)) = identifier_check {
650 let inbox_id = identity.inbox_id.as_str();
651 let membership = if allow_offline {
652 crate::identity_updates::stored_identifier_membership(&conn, inbox_id, &identifier)
653 .await
654 .map_err(ClientError::from)?
655 } else {
656 crate::identity_updates::identifier_membership(
657 &conn,
658 inbox_id,
659 &identifier,
660 &scw_verifier,
661 )
662 .await
663 .map_err(ClientError::from)?
664 };
665 if !crate::identity_updates::identifier_opens_inbox(
666 membership,
667 &identifier,
668 nonce,
669 &identity.inbox_id,
670 ) {
671 return Err(crate::identity::IdentityError::IdentifierNotInInbox {
672 inbox_id: identity.inbox_id.clone(),
673 }
674 .into());
675 }
676 }
677
678 let own_inbox_stored_through = conn
681 .get_latest_sequence_id(&[identity.inbox_id.as_str()])
682 .map_err(StorageError::from)?
683 .get(identity.inbox_id.as_str())
684 .copied();
685
686 let mut worker_config = worker_config;
691 if disable_workers {
692 for kind in [
695 crate::worker::WorkerKind::DeviceSync,
696 crate::worker::WorkerKind::DisappearingMessages,
697 crate::worker::WorkerKind::CommitLog,
698 crate::worker::WorkerKind::TaskRunner,
699 crate::worker::WorkerKind::ConfigurationRefresh,
700 crate::worker::WorkerKind::AttachmentCleanup,
701 ] {
702 worker_config.enabled.insert(kind, false);
703 }
704 }
705 if matches!(device_sync_worker_mode, DeviceSyncMode::Disabled) {
706 worker_config
707 .enabled
708 .entry(crate::worker::WorkerKind::DeviceSync)
709 .or_insert(false);
710 }
711 if disable_commit_log_worker {
712 worker_config
713 .enabled
714 .entry(crate::worker::WorkerKind::CommitLog)
715 .or_insert(false);
716 }
717
718 let events = xmtp_events::EventBus::new();
719 let events_for_lifetime = events.clone();
720 let public_event_writer: Arc<dyn xmtp_events::EventWriter<()>> =
721 Arc::new(xmtp_events::PublicBusWriter::new(&events));
722 server_configuration.set_event_writer(public_event_writer.clone());
723 api_client
724 .api_client
725 .register_client_event_writer(&public_event_writer);
726 let mut workers = WorkerRunner::new();
727 let incoming_runtime = crate::subscriptions::incoming::IncomingRuntime::new(
728 stream_policy.clone(),
729 incoming_factory,
730 )
731 .with_preflight(api_client.api_client.clone());
732 let attachments =
733 Arc::new(AttachmentRuntime::new(attachments_dir, attachment_options).await?);
734 let context = Arc::new(XmtpMlsLocalContext {
735 identity,
736 mls_storage,
737 store,
738 api_client,
739 version_info,
740 server_configuration,
741 scw_verifier: Arc::new(scw_verifier),
742 mutexes: MutexRegistry::new(),
743 hooks: Default::default(),
744 events,
745 public_event_writer,
746 registration_event_pending: Arc::new(AtomicBool::new(false)),
747 device_sync: DeviceSync {
748 mode: device_sync_worker_mode,
749 },
750 fork_recovery_opts: fork_recovery_opts.unwrap_or_default(),
751 change_callbacks,
752 stream_policy,
753 worker_config,
754
755 worker_metrics: workers.sync_metrics_slot().clone(),
756 task_channels: workers.task_channels().clone(),
757 cancellation_token: CancellationToken::new(),
758 shutdown_complete: Arc::new(AtomicBool::new(false)),
759 foreground_calls: Default::default(),
760 delivery_owner: Default::default(),
761 retired_delivery_owner: Default::default(),
762 features: crate::features::MlsFeatures {
763 attachments,
764 incoming: Arc::new(incoming_runtime),
765 identity_resolutions: Arc::new(
766 crate::identity_updates::IdentityResolutionRegistry::starting_after(
767 own_inbox_stored_through,
768 ),
769 ),
770 },
771 });
772
773 crate::core::preflight::bind_preflight(&context);
774
775 if let Err(error) = context.features.attachments.sweep(&context).await {
777 tracing::warn!(%error, "attachment cleanup failed during client build");
778 }
779 if let Err(error) = context
780 .features
781 .attachments
782 .ensure_reconciled(&context)
783 .await
784 {
785 tracing::warn!(%error, "attachment reconciliation failed during client build");
786 }
787 if !disable_workers {
788 use crate::worker::WorkerKind;
789 let enabled = |k| context.worker_config().worker_enabled(k);
791
792 if enabled(WorkerKind::DeviceSync) && context.device_sync_worker_enabled() {
795 workers.register_new_worker::<SyncWorker<ContextParts<ApiClient, S, Db>>, _>(
796 context.clone(),
797 );
798 }
799 if enabled(WorkerKind::DisappearingMessages) {
800 workers
801 .register_new_worker::<DisappearingMessagesWorker<ContextParts<ApiClient, S, Db>>, _>(
802 context.clone(),
803 );
804 }
805 if enabled(WorkerKind::CommitLog)
809 && context
810 .server_configuration()
811 .configuration()
812 .mls
813 .commit_log_enabled()
814 && !disable_commit_log_worker
815 {
816 workers.register_new_worker::<
817 crate::groups::commit_log::CommitLogWorker<ContextParts<ApiClient, S, Db>>,
818 _,
819 >(context.clone());
820 }
821 if enabled(WorkerKind::ConfigurationRefresh) && !has_config_provider {
824 workers.register_new_worker::<crate::core::config_refresh::ConfigurationWorker<
825 ContextParts<ApiClient, S, Db>,
826 >, _>(context.clone());
827 }
828 if enabled(WorkerKind::TaskRunner) {
829 workers.register_new_worker::<TaskWorker<
830 ContextParts<ApiClient, S, Db>,
831 crate::task_handlers::MlsTaskHandlers,
832 >, _>(context.clone());
833 crate::worker::key_package_maintenance::seed_and_reconcile_kp_tasks(&context)?;
841 if let Err(e) = backfill_pending_self_remove_tasks(&context) {
850 tracing::warn!(
851 "pending-self-remove backfill failed (will rely on next sync): {e}"
852 );
853 }
854 }
855 if enabled(WorkerKind::AttachmentCleanup)
856 && context.features.attachments.store.is_some()
857 {
858 workers.register_new_worker::<crate::attachments::cleanup::AttachmentCleanup<
859 ContextParts<ApiClient, S, Db>,
860 >, _>(context.clone());
861 }
862 }
863
864 workers.register_new_worker::<
867 crate::worker::hmac_epoch::HmacEpochWorker<ContextParts<ApiClient, S, Db>>,
868 _,
869 >(context.clone());
870
871 let workers = Arc::new(workers);
872
873 workers.spawn(context.clone());
874
875 log_event!(
876 Event::ClientCreated,
877 context.installation_id(),
878 inbox_id = context.inbox_id(),
879 device_sync_enabled = context.device_sync_worker_enabled(),
880 disabled_workers = disable_workers,
881 );
882
883 let installation_id = context.installation_id();
884 let client = Client {
885 context,
886 installation_id,
887 workers,
888 app_lifetime: Arc::new(crate::client::AppLifetime {
889 events: events_for_lifetime,
890 }),
891 };
892
893 Ok(client)
894 }
895
896 pub fn identity(self, identity: Identity) -> Self {
897 Self {
898 identity: Some(identity),
899 ..self
900 }
901 }
902
903 pub fn attachments_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
904 self.storage_location_conflict |= self.storage_location_selected;
905 self.attachments_dir = Some(path.into());
906 self
907 }
908
909 pub fn attachment_options(mut self, options: xmtp_attachments::AttachmentOptions) -> Self {
910 self.attachment_options = options;
911 self
912 }
913
914 #[cfg(not(target_arch = "wasm32"))]
919 pub async fn data_location(
920 self,
921 location: crate::storage_location::StorageLocation,
922 key: xmtp_db::EncryptionKey,
923 ) -> Result<ClientBuilder<ApiClient, S, xmtp_db::DefaultStore>, ClientBuilderError> {
924 self.data_location_with(location, move |paths| open_location_store(paths, key))
925 }
926
927 pub fn data_location_with<NewDb>(
931 self,
932 location: crate::storage_location::StorageLocation,
933 opener: impl FnOnce(
934 crate::storage_location::ResolvedPaths,
935 )
936 -> xmtp_common::BoxDynFuture<'static, Result<NewDb, ClientBuilderError>>
937 + xmtp_common::MaybeSend
938 + xmtp_common::MaybeSync
939 + 'static,
940 ) -> Result<ClientBuilder<ApiClient, S, NewDb>, ClientBuilderError> {
941 location.validate()?;
942 let conflict = self.storage_location_conflict
943 || self.store.is_some()
944 || self.mls_storage.is_some()
945 || self.mls_storage_factory.is_some()
946 || self.storage_location_selected
947 || self.attachments_dir.is_some();
948 Ok(ClientBuilder {
949 deployment_recorder: None,
950 data_location: Some(location),
951 location_store_opener: Some(Box::new(opener)),
952 mls_storage_factory: None,
953 storage_location_selected: true,
954 storage_location_conflict: conflict,
955 attachments_dir: self.attachments_dir,
956 attachment_options: self.attachment_options,
957 api_client: self.api_client,
958 identity: self.identity,
959 store: None,
960 identity_strategy: self.identity_strategy,
961 scw_verifier: self.scw_verifier,
962 custom_scw_verifier: self.custom_scw_verifier,
963 device_sync_worker_mode: self.device_sync_worker_mode,
964 fork_recovery_opts: self.fork_recovery_opts,
965 change_callbacks: self.change_callbacks,
966 stream_policy: self.stream_policy,
967 incoming_factory: self.incoming_factory,
968 version_info: self.version_info,
969 allow_offline: self.allow_offline,
970 disable_commit_log_worker: self.disable_commit_log_worker,
971 mls_storage: self.mls_storage,
972 disable_workers: self.disable_workers,
973 worker_config: self.worker_config,
974 config_provider: self.config_provider,
975 #[cfg(any(test, feature = "test-utils"))]
976 application_components_for_test: self.application_components_for_test,
977 })
978 }
979
980 pub fn unstable_change_callbacks(self, change_callbacks: UnstableChangeCallbacks) -> Self {
989 Self {
990 change_callbacks,
991 ..self
992 }
993 }
994
995 pub fn store<NewDb>(self, db: NewDb) -> ClientBuilder<ApiClient, S, NewDb> {
996 ClientBuilder {
997 deployment_recorder: self.deployment_recorder,
998 data_location: self.data_location,
999 location_store_opener: None,
1000 mls_storage_factory: None,
1001 storage_location_selected: self.storage_location_selected,
1002 storage_location_conflict: self.storage_location_conflict
1003 || self.storage_location_selected,
1004 store: Some(db),
1005 api_client: self.api_client,
1006 identity: self.identity,
1007 identity_strategy: self.identity_strategy,
1008 scw_verifier: self.scw_verifier,
1009 custom_scw_verifier: self.custom_scw_verifier,
1010 device_sync_worker_mode: self.device_sync_worker_mode,
1011 fork_recovery_opts: self.fork_recovery_opts,
1012 change_callbacks: self.change_callbacks,
1013 stream_policy: self.stream_policy,
1014 incoming_factory: self.incoming_factory,
1015 version_info: self.version_info,
1016 allow_offline: self.allow_offline,
1017 disable_commit_log_worker: self.disable_commit_log_worker,
1018 mls_storage: self.mls_storage,
1019 disable_workers: self.disable_workers,
1020 worker_config: self.worker_config,
1021 config_provider: self.config_provider,
1022 #[cfg(any(test, feature = "test-utils"))]
1023 application_components_for_test: self.application_components_for_test,
1024 attachments_dir: self.attachments_dir,
1025 attachment_options: self.attachment_options,
1026 }
1027 }
1028
1029 pub fn default_mls_store(
1031 self,
1032 ) -> Result<
1033 ClientBuilder<ApiClient, SqlKeyStore<<Db as XmtpDb>::DbQuery>, Db>,
1034 ClientBuilderError,
1035 >
1036 where
1037 Db: XmtpDb,
1038 {
1039 Ok(ClientBuilder {
1040 deployment_recorder: self.deployment_recorder,
1041 data_location: self.data_location,
1042 location_store_opener: self.location_store_opener,
1043 mls_storage_factory: Some(|store: &Db| SqlKeyStore::new(store.db())),
1044 storage_location_selected: self.storage_location_selected,
1045 storage_location_conflict: self.storage_location_conflict,
1046 api_client: self.api_client,
1047 identity: self.identity,
1048 identity_strategy: self.identity_strategy,
1049 scw_verifier: self.scw_verifier,
1050 custom_scw_verifier: self.custom_scw_verifier,
1051 device_sync_worker_mode: self.device_sync_worker_mode,
1052 fork_recovery_opts: self.fork_recovery_opts,
1053 change_callbacks: self.change_callbacks,
1054 stream_policy: self.stream_policy,
1055 incoming_factory: self.incoming_factory,
1056 version_info: self.version_info,
1057 allow_offline: self.allow_offline,
1058 disable_commit_log_worker: self.disable_commit_log_worker,
1059 mls_storage: self
1060 .store
1061 .as_ref()
1062 .map(|store| SqlKeyStore::new(store.db())),
1063 store: self.store,
1064 disable_workers: self.disable_workers,
1065 worker_config: self.worker_config,
1066 config_provider: self.config_provider,
1067 #[cfg(any(test, feature = "test-utils"))]
1068 application_components_for_test: self.application_components_for_test,
1069 attachments_dir: self.attachments_dir,
1070 attachment_options: self.attachment_options,
1071 })
1072 }
1073
1074 pub fn mls_storage<NewS>(self, mls_storage: NewS) -> ClientBuilder<ApiClient, NewS, Db> {
1075 let storage_location_conflict =
1076 self.storage_location_conflict || self.storage_location_selected;
1077 ClientBuilder {
1078 deployment_recorder: self.deployment_recorder,
1079 data_location: self.data_location,
1080 location_store_opener: self.location_store_opener,
1081 mls_storage_factory: None,
1082 storage_location_selected: self.storage_location_selected,
1083 storage_location_conflict,
1084 store: self.store,
1085 api_client: self.api_client,
1086 identity: self.identity,
1087 identity_strategy: self.identity_strategy,
1088 scw_verifier: self.scw_verifier,
1089 custom_scw_verifier: self.custom_scw_verifier,
1090 device_sync_worker_mode: self.device_sync_worker_mode,
1091 fork_recovery_opts: self.fork_recovery_opts,
1092 change_callbacks: self.change_callbacks,
1093 stream_policy: self.stream_policy,
1094 incoming_factory: self.incoming_factory,
1095 version_info: self.version_info,
1096 allow_offline: self.allow_offline,
1097 disable_commit_log_worker: self.disable_commit_log_worker,
1098 mls_storage: Some(mls_storage),
1099 disable_workers: self.disable_workers,
1100 worker_config: self.worker_config,
1101 config_provider: self.config_provider,
1102 #[cfg(any(test, feature = "test-utils"))]
1103 application_components_for_test: self.application_components_for_test,
1104 attachments_dir: self.attachments_dir,
1105 attachment_options: self.attachment_options,
1106 }
1107 }
1108
1109 pub fn with_disable_workers(mut self, disable_workers: bool) -> Self {
1110 self.disable_workers = disable_workers;
1111 self
1112 }
1113
1114 pub fn with_device_sync_worker_mode(self, mode: Option<DeviceSyncMode>) -> Self {
1115 Self {
1116 device_sync_worker_mode: mode.unwrap_or(DeviceSyncMode::Enabled),
1117 ..self
1118 }
1119 }
1120
1121 pub fn device_sync_worker_mode(self, mode: DeviceSyncMode) -> Self {
1122 Self {
1123 device_sync_worker_mode: mode,
1124 ..self
1125 }
1126 }
1127
1128 pub fn fork_recovery_opts(self, opts: ForkRecoveryOpts) -> Self {
1129 Self {
1130 fork_recovery_opts: Some(opts),
1131 ..self
1132 }
1133 }
1134
1135 pub fn worker_config(mut self, cfg: crate::worker::WorkerConfig) -> Self {
1138 self.worker_config = cfg;
1139 self
1140 }
1141
1142 #[cfg(any(test, feature = "test-utils"))]
1145 pub fn application_components_for_test(
1146 mut self,
1147 components: Vec<xmtp_configuration::ApplicationComponentDefinition>,
1148 ) -> Self {
1149 self.application_components_for_test = Some(components);
1150 self
1151 }
1152
1153 pub fn config_provider(
1159 mut self,
1160 provider: Arc<dyn xmtp_configuration::ConfigProvider>,
1161 ) -> Self {
1162 self.config_provider = Some(provider);
1163 self
1164 }
1165
1166 pub fn api_client<A>(self, api_client: A) -> ClientBuilder<A, S, Db> {
1169 ClientBuilder {
1170 deployment_recorder: self.deployment_recorder,
1171 data_location: self.data_location,
1172 location_store_opener: self.location_store_opener,
1173 mls_storage_factory: self.mls_storage_factory,
1174 storage_location_selected: self.storage_location_selected,
1175 storage_location_conflict: self.storage_location_conflict,
1176 api_client: Some(api_client),
1177 identity: self.identity,
1178 identity_strategy: self.identity_strategy,
1179 scw_verifier: self.scw_verifier,
1180 custom_scw_verifier: self.custom_scw_verifier,
1181 store: self.store,
1182 device_sync_worker_mode: self.device_sync_worker_mode,
1183 fork_recovery_opts: self.fork_recovery_opts,
1184 change_callbacks: self.change_callbacks,
1185 stream_policy: self.stream_policy,
1186 incoming_factory: None,
1187 version_info: self.version_info,
1188 allow_offline: self.allow_offline,
1189 disable_commit_log_worker: self.disable_commit_log_worker,
1190 mls_storage: self.mls_storage,
1191 disable_workers: self.disable_workers,
1192 worker_config: self.worker_config,
1193 config_provider: self.config_provider,
1194 #[cfg(any(test, feature = "test-utils"))]
1195 application_components_for_test: self.application_components_for_test,
1196 attachments_dir: self.attachments_dir,
1197 attachment_options: self.attachment_options,
1198 }
1199 }
1200
1201 xmtp_common::if_native! {
1202 pub fn api_client_with_streams<A>(self, api_client: A) -> ClientBuilder<A, S, Db>
1205 where
1206 A: xmtp_proto::api_client::XmtpMlsBidiStreams
1207 + crate::subscriptions::router_callbacks::ApiClientIdentity
1208 + Clone
1209 + Send
1210 + Sync
1211 + 'static,
1212 A::SubscribeStream: 'static,
1213 {
1214 let factory = crate::subscriptions::incoming::streaming_factory(api_client.clone());
1215 let mut builder = self.api_client(api_client);
1216 builder.incoming_factory = Some(factory);
1217 builder
1218 }
1219 }
1220
1221 xmtp_common::if_wasm! {
1222 pub fn api_client_with_streams<A>(self, api_client: A) -> ClientBuilder<A, S, Db>
1225 where
1226 A: xmtp_proto::api_client::XmtpMlsStreams + Clone + 'static,
1227 {
1228 let factory = crate::subscriptions::incoming::streaming_factory(api_client.clone());
1229 let mut builder = self.api_client(api_client);
1230 builder.incoming_factory = Some(factory);
1231 builder
1232 }
1233 }
1234
1235 pub fn maybe_version(
1236 mut self,
1237 version: Option<VersionInfo>,
1238 ) -> ClientBuilder<ApiClient, S, Db> {
1239 if let Some(v) = version {
1240 self.version_info = v;
1241 }
1242 self
1243 }
1244
1245 pub fn version(self, version_info: VersionInfo) -> ClientBuilder<ApiClient, S, Db> {
1246 Self {
1247 version_info,
1248 ..self
1249 }
1250 }
1251
1252 pub fn with_allow_offline(
1256 self,
1257 allow_offline: Option<bool>,
1258 ) -> ClientBuilder<ApiClient, S, Db> {
1259 Self {
1260 allow_offline: allow_offline.unwrap_or(false),
1261 ..self
1262 }
1263 }
1264
1265 #[cfg(any(test, feature = "test-utils"))]
1268 pub fn with_commit_log_worker(self, enabled: bool) -> Self {
1269 Self {
1270 disable_commit_log_worker: !enabled,
1271 ..self
1272 }
1273 }
1274
1275 #[cfg(any(test, feature = "test-utils"))]
1276 pub fn enable_sqlite_triggers(self) -> Self
1277 where
1278 Db: XmtpDb,
1279 {
1280 let db = self.store.as_ref().expect("unwrapping in test env").conn();
1281 let db = xmtp_db::DbConnection::new(db);
1282 db.register_triggers();
1283 db.disable_memory_security();
1284 self
1285 }
1286
1287 pub fn enable_api_stats(
1288 self,
1289 ) -> Result<ClientBuilder<TrackedStatsClient<ApiClient>, S, Db>, ClientBuilderError> {
1290 if self.api_client.is_none() {
1291 return Err(ClientBuilderError::MissingParameter {
1292 parameter: "api_client",
1293 });
1294 }
1295
1296 Ok(ClientBuilder {
1297 deployment_recorder: self.deployment_recorder,
1298 data_location: self.data_location,
1299 location_store_opener: self.location_store_opener,
1300 mls_storage_factory: self.mls_storage_factory,
1301 storage_location_selected: self.storage_location_selected,
1302 storage_location_conflict: self.storage_location_conflict,
1303 api_client: Some(TrackedStatsClient::new(
1304 self.api_client.expect("checked for none"),
1305 )),
1306 identity: self.identity,
1307 identity_strategy: self.identity_strategy,
1308 scw_verifier: self.scw_verifier,
1309 custom_scw_verifier: self.custom_scw_verifier,
1310 store: self.store,
1311
1312 device_sync_worker_mode: self.device_sync_worker_mode,
1313 fork_recovery_opts: self.fork_recovery_opts,
1314 change_callbacks: self.change_callbacks,
1315 stream_policy: self.stream_policy,
1316 incoming_factory: self.incoming_factory,
1317 version_info: self.version_info,
1318 allow_offline: self.allow_offline,
1319 disable_commit_log_worker: self.disable_commit_log_worker,
1320 mls_storage: self.mls_storage,
1321 disable_workers: self.disable_workers,
1322 worker_config: self.worker_config,
1323 config_provider: self.config_provider,
1324 #[cfg(any(test, feature = "test-utils"))]
1325 application_components_for_test: self.application_components_for_test,
1326 attachments_dir: self.attachments_dir,
1327 attachment_options: self.attachment_options,
1328 })
1329 }
1330
1331 pub fn with_scw_verifier(
1332 self,
1333 verifier: impl SmartContractSignatureVerifier + 'static,
1334 ) -> ClientBuilder<ApiClient, S, Db> {
1335 ClientBuilder {
1336 deployment_recorder: self.deployment_recorder,
1337 data_location: self.data_location,
1338 location_store_opener: self.location_store_opener,
1339 mls_storage_factory: self.mls_storage_factory,
1340 storage_location_selected: self.storage_location_selected,
1341 storage_location_conflict: self.storage_location_conflict,
1342 api_client: self.api_client,
1343 identity: self.identity,
1344 identity_strategy: self.identity_strategy,
1345 scw_verifier: Some(Box::new(verifier)),
1346 custom_scw_verifier: true,
1347 store: self.store,
1348
1349 device_sync_worker_mode: self.device_sync_worker_mode,
1350 fork_recovery_opts: self.fork_recovery_opts,
1351 change_callbacks: self.change_callbacks,
1352 stream_policy: self.stream_policy,
1353 incoming_factory: self.incoming_factory,
1354 version_info: self.version_info,
1355 allow_offline: self.allow_offline,
1356 disable_commit_log_worker: self.disable_commit_log_worker,
1357 mls_storage: self.mls_storage,
1358 disable_workers: self.disable_workers,
1359 worker_config: self.worker_config,
1360 config_provider: self.config_provider,
1361 #[cfg(any(test, feature = "test-utils"))]
1362 application_components_for_test: self.application_components_for_test,
1363 attachments_dir: self.attachments_dir,
1364 attachment_options: self.attachment_options,
1365 }
1366 }
1367
1368 pub fn with_remote_verifier(self) -> Result<ClientBuilder<ApiClient, S, Db>, ClientBuilderError>
1371 where
1372 ApiClient: Clone + XmtpApi + 'static,
1373 {
1374 let api = self
1375 .api_client
1376 .clone()
1377 .ok_or(ClientBuilderError::MissingParameter {
1378 parameter: "api_client",
1379 })?;
1380
1381 Ok(ClientBuilder {
1382 deployment_recorder: self.deployment_recorder,
1383 data_location: self.data_location,
1384 location_store_opener: self.location_store_opener,
1385 mls_storage_factory: self.mls_storage_factory,
1386 storage_location_selected: self.storage_location_selected,
1387 storage_location_conflict: self.storage_location_conflict,
1388 api_client: self.api_client,
1389 identity: self.identity,
1390 identity_strategy: self.identity_strategy,
1391 scw_verifier: Some(Box::new(ApiClientWrapper::new(api, Retry::default()))
1392 as Box<dyn SmartContractSignatureVerifier>),
1393 custom_scw_verifier: false,
1396 store: self.store,
1397 device_sync_worker_mode: self.device_sync_worker_mode,
1398 fork_recovery_opts: self.fork_recovery_opts,
1399 change_callbacks: self.change_callbacks,
1400 stream_policy: self.stream_policy,
1401 incoming_factory: self.incoming_factory,
1402 version_info: self.version_info,
1403 allow_offline: self.allow_offline,
1404 disable_commit_log_worker: self.disable_commit_log_worker,
1405 mls_storage: self.mls_storage,
1406 disable_workers: self.disable_workers,
1407 worker_config: self.worker_config,
1408 config_provider: self.config_provider,
1409 #[cfg(any(test, feature = "test-utils"))]
1410 application_components_for_test: self.application_components_for_test,
1411 attachments_dir: self.attachments_dir,
1412 attachment_options: self.attachment_options,
1413 })
1414 }
1415}
1416
1417#[cfg(test)]
1418mod worker_registration_tests {
1419 use crate::tester;
1420 use crate::worker::{WorkerConfig, WorkerKind};
1421
1422 #[xmtp_common::test(unwrap_try = true)]
1423 #[cfg_attr(target_arch = "wasm32", ignore)]
1424 async fn disabled_worker_is_not_registered() {
1425 let mut cfg = WorkerConfig::default();
1426 cfg.enabled.insert(WorkerKind::DisappearingMessages, false);
1427 tester!(alix, worker_config: cfg);
1428
1429 let kinds = alix.client.workers.registered_kinds();
1430 assert!(
1431 !kinds.contains(&WorkerKind::DisappearingMessages),
1432 "disabled worker must not be registered, got {kinds:?}"
1433 );
1434 assert!(
1435 kinds.contains(&WorkerKind::TaskRunner),
1436 "un-disabled worker must still be registered, got {kinds:?}"
1437 );
1438 let subscriptions = alix.client.workers.subscribed_kinds();
1439 assert!(!subscriptions.contains(&WorkerKind::DisappearingMessages));
1440 assert!(subscriptions.contains(&WorkerKind::TaskRunner));
1441 }
1442
1443 #[xmtp_common::test(unwrap_try = true)]
1444 #[cfg_attr(target_arch = "wasm32", ignore)]
1445 async fn disabled_optional_workers_keep_the_epoch_observer() {
1446 tester!(alix, disable_workers);
1447 assert_eq!(
1448 alix.client.workers.registered_kinds(),
1449 vec![WorkerKind::HmacEpoch]
1450 );
1451 assert!(alix.client.workers.is_running());
1452 }
1453}