xmtp_mls/worker/device_sync/
mod.rs1use crate::{
2 client::ClientError,
3 context::XmtpSharedContext,
4 groups::{
5 GroupError, MlsGroup, PreconfiguredPolicies, send_message_opts, summary::SyncSummary,
6 },
7 identity::parse_credential,
8 mls_store::{MlsStore, MlsStoreError},
9 subscriptions::{SubscribeError, internal::InternalEvent},
10 worker::{NeedsDbReconnect, metrics::WorkerMetrics},
11};
12use openmls::prelude::{BasicCredential, Member};
13use owo_colors::OwoColorize;
14use prost::Message;
15use std::{collections::HashMap, sync::Arc};
16use thiserror::Error;
17use tokio::sync::broadcast::error::RecvError;
18use tracing::instrument;
19use worker::SyncMetric;
20use xmtp_archive::ArchiveError;
21use xmtp_common::ErrorCode;
22use xmtp_common::{NS_IN_DAY, RetryableError, time::now_ns};
23use xmtp_content_types::encoded_content_to_bytes;
24use xmtp_db::tasks::NewTask;
25use xmtp_db::{NotFound, StorageError, consent_record::ConsentState, group::GroupQueryArgs};
26use xmtp_db::{XmtpDb, group::ConversationType, prelude::*};
27use xmtp_id::{InboxIdRef, associations::DeserializationError};
28use xmtp_mls_common::group::GroupMetadataOptions;
29use xmtp_proto::types::{GroupId, InstallationId};
30use xmtp_proto::xmtp::{
31 device_sync::content::{
32 DeviceSyncContent as DeviceSyncContentProto, device_sync_content::Content as ContentProto,
33 },
34 mls::{
35 database::{
36 AddMissingInstallations as AddMissingInstallationsProto, Task as TaskProto,
37 task::Task as TaskKindProto,
38 },
39 message_contents::{ContentTypeId, EncodedContent},
40 },
41};
42
43pub mod archive;
44pub mod preference_sync;
45pub mod worker;
46
47pub use xmtp_archive::archive_options::{ArchiveOptions, BackupElementSelection};
48
49#[cfg(test)]
50mod tests;
51
52fn sync_message_bytes(content: ContentProto) -> Vec<u8> {
54 let content = DeviceSyncContentProto {
55 content: Some(content),
56 };
57 encoded_content_to_bytes(EncodedContent {
58 r#type: Some(ContentTypeId {
59 authority_id: "xmtp.org".to_string(),
60 type_id: "application/x-protobuf".to_string(),
61 version_major: 1,
62 version_minor: 0,
63 }),
64 parameters: HashMap::new(),
65 fallback: None,
66 compression: None,
67 content: content.encode_to_vec(),
68 })
69}
70
71pub(crate) fn is_own_sync_group(
75 own_inbox_id: &str,
76 added_by_inbox_id: &str,
77 members: impl IntoIterator<Item = Member>,
78) -> bool {
79 added_by_inbox_id == own_inbox_id
80 && members.into_iter().all(|member| {
81 BasicCredential::try_from(member.credential)
82 .ok()
83 .and_then(|credential| parse_credential(credential.identity()).ok())
84 .is_some_and(|inbox_id| inbox_id == own_inbox_id)
85 })
86}
87
88#[derive(Debug, Error, ErrorCode)]
89pub enum DeviceSyncError {
90 #[error("IO error: {0}")]
94 IO(#[from] std::io::Error),
95 #[error("Serialization/Deserialization Error {0}")]
99 Serde(#[from] serde_json::Error),
100 #[error(transparent)]
101 #[error_code(inherit)]
102 ProtoConversion(#[from] xmtp_proto::ConversionError),
103 #[error("AES-GCM encryption error")]
107 AesGcm(#[from] aes_gcm::Error),
108 #[error("storage error: {0}")]
109 #[error_code(inherit)]
110 Storage(#[from] StorageError),
111 #[error("type conversion error")]
115 Conversion,
116 #[error("utf-8 error: {0}")]
120 UTF8(#[from] std::str::Utf8Error),
121 #[error("client error: {0}")]
122 #[error_code(inherit)]
123 Client(#[from] ClientError),
124 #[error("group error: {0}")]
125 #[error_code(inherit)]
126 Group(#[from] GroupError),
127 #[error("invalid history message payload")]
131 InvalidPayload,
132 #[error("unspecified device sync kind")]
136 UnspecifiedDeviceSyncKind,
137 #[error(transparent)]
138 #[error_code(inherit)]
139 Subscribe(#[from] SubscribeError),
140 #[error(transparent)]
144 Bincode(#[from] bincode::Error),
145 #[error(transparent)]
149 Archive(#[from] ArchiveError),
150 #[error(transparent)]
154 Decode(#[from] prost::DecodeError),
155 #[error(transparent)]
156 #[error_code(inherit)]
157 Deserialization(#[from] DeserializationError),
158 #[error("Missing sync group")]
162 MissingSyncGroup,
163 #[error(transparent)]
164 #[error_code(inherit)]
165 Db(#[from] xmtp_db::ConnectionError),
166 #[error("{}", _0.to_string())]
170 Sync(#[source] Box<SyncSummary>),
171 #[error(transparent)]
175 MlsStore(#[from] MlsStoreError),
176 #[error(transparent)]
180 Recv(#[from] RecvError),
181 #[error("Missing Field: {0:?} {1}")]
185 MissingField(MissingField, String),
186}
187
188#[derive(Debug)]
189pub enum MissingField {
190 Conversation(ConversationField),
191}
192#[derive(Debug)]
193pub enum ConversationField {
194 DmId,
195}
196
197impl From<SyncSummary> for DeviceSyncError {
198 fn from(value: SyncSummary) -> Self {
199 DeviceSyncError::Sync(Box::new(value))
200 }
201}
202
203impl NeedsDbReconnect for DeviceSyncError {
204 fn needs_db_reconnect(&self) -> bool {
205 match self {
206 Self::Client(s) => s.db_needs_connection(),
207 Self::Storage(s) => s.db_needs_connection(),
208 Self::Db(c) => c.db_needs_connection(),
211 Self::Group(e) => e.needs_db_reconnect(),
212 Self::MlsStore(e) => e.needs_db_reconnect(),
213 Self::Subscribe(e) => e.needs_db_reconnect(),
214 _ => false,
215 }
216 }
217}
218
219impl RetryableError for DeviceSyncError {
220 fn is_retryable(&self) -> bool {
221 !matches!(
222 self,
223 Self::MissingSyncGroup | Self::UnspecifiedDeviceSyncKind
224 )
225 }
226}
227
228impl From<NotFound> for DeviceSyncError {
229 fn from(value: NotFound) -> Self {
230 DeviceSyncError::Storage(StorageError::NotFound(value))
231 }
232}
233
234#[derive(Clone)]
235pub struct DeviceSyncClient<Context> {
236 pub(crate) context: Context,
237 pub(crate) mls_store: MlsStore<Context>,
238 pub(crate) metrics: Arc<WorkerMetrics<SyncMetric>>,
239}
240
241impl<Context: XmtpSharedContext> DeviceSyncClient<Context> {
242 pub fn new(context: Context, metrics: Arc<WorkerMetrics<SyncMetric>>) -> Self {
243 Self {
244 context: context.clone(),
245 mls_store: MlsStore::new(context),
246 metrics,
247 }
248 }
249}
250
251impl<Context> DeviceSyncClient<Context>
252where
253 Context: XmtpSharedContext,
254{
255 pub fn inbox_id(&self) -> InboxIdRef<'_> {
256 self.context.identity().inbox_id()
257 }
258
259 pub fn installation_id(&self) -> InstallationId {
260 self.context.installation_id()
261 }
262
263 pub fn db(&self) -> <Context::Db as XmtpDb>::DbQuery {
264 self.context.db()
265 }
266
267 pub async fn wait_for_sync_worker_init(&self) -> Result<(), xmtp_common::time::Expired> {
269 self.metrics.wait_for_init().await
270 }
271
272 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = self.context.inbox_id()), skip(self, content)))]
276 #[cfg_attr(
277 not(any(test, feature = "test-utils")),
278 tracing::instrument(level = "trace", skip(self, content))
279 )]
280 async fn send_device_sync_message(
282 &self,
283 content: ContentProto,
284 ) -> Result<Vec<u8>, ClientError> {
285 let sync_group = self.get_sync_group().await?;
286
287 let msg = format!(
288 "Sending sync message to group {:?}",
289 xmtp_common::fmt::debug_hex(sync_group.group_id)
290 );
291 tracing::info!("{}", msg.yellow());
292
293 let content_bytes = sync_message_bytes(content);
294
295 let message_id = sync_group.prepare_message(
296 &content_bytes,
297 send_message_opts::SendMessageOpts {
298 should_push: false,
299 idempotency_key: None,
300 },
301 )?;
302
303 sync_group.sync_until_last_intent_resolved().await?;
304
305 xmtp_events::EventWriter::emit(
306 self.context.events(),
307 None,
308 Some(InternalEvent::SyncMessagePublished),
309 );
310
311 Ok(message_id)
312 }
313
314 pub(crate) fn primary_sync_group(&self) -> Result<Option<MlsGroup<Context>>, GroupError> {
318 let own_inbox_id = self.context.inbox_id();
319 for stored in self.context.db().all_sync_groups()? {
320 let group = self.mls_store.group(&stored.id)?;
321 let own = group.with_group_snapshot(|mls_group| {
322 Ok(is_own_sync_group(
323 own_inbox_id,
324 &stored.added_by_inbox_id,
325 mls_group.members(),
326 ))
327 })?;
328 if own {
329 return Ok(Some(group));
330 }
331 tracing::debug!(
332 group_id = %stored.id,
333 "skipping a sync group that names another inbox"
334 );
335 }
336 Ok(None)
337 }
338
339 #[instrument(level = "trace", skip_all)]
341 pub async fn get_sync_group(&self) -> Result<MlsGroup<Context>, GroupError> {
342 let sync_group = match self.primary_sync_group()? {
343 Some(sync_group) => sync_group,
344 None => {
345 let sync_group = MlsGroup::create_and_insert(
346 self.context.clone(),
347 ConversationType::Sync,
348 PreconfiguredPolicies::default().to_policy_set(),
349 GroupMetadataOptions::default(),
350 None,
351 )?;
352 tracing::info!(
353 "Creating sync group: {}",
354 xmtp_common::fmt::debug_hex(sync_group.group_id)
355 );
356 if let Err(inline_err) = sync_group.add_missing_installations().await {
357 self.schedule_add_missing_installations_task(sync_group.group_id)
364 .map_err(Box::new)?;
365 return Err(inline_err);
366 }
367 sync_group.sync_with_conn().await?;
368
369 self.metrics.increment_metric(SyncMetric::SyncGroupCreated);
370
371 sync_group
372 }
373 };
374
375 Ok(sync_group)
376 }
377
378 #[cfg_attr(
386 any(test, feature = "test-utils"),
387 tracing::instrument(level = "info", skip_all)
388 )]
389 pub fn schedule_add_installations_to_groups(&self) -> Result<usize, DeviceSyncError> {
391 let groups = self.mls_store.find_groups(GroupQueryArgs {
392 last_activity_after_ns: Some(now_ns() - NS_IN_DAY * 90),
393 consent_states: Some(vec![ConsentState::Allowed, ConsentState::Unknown]),
394 ..Default::default()
395 })?;
396
397 for group in &groups {
398 self.schedule_add_missing_installations_task(group.group_id)?;
399 }
400 Ok(groups.len())
401 }
402
403 pub(crate) fn schedule_add_missing_installations_task(
407 &self,
408 group_id: GroupId,
409 ) -> Result<(), DeviceSyncError> {
410 let task = NewTask::builder()
411 .originating_message_sequence_id(0)
412 .build(TaskProto {
413 task: Some(TaskKindProto::AddMissingInstallations(
414 AddMissingInstallationsProto {
415 group_id: group_id.to_vec(),
416 },
417 )),
418 })?;
419 self.context.db().create_or_ignore_task(task)?;
420 xmtp_events::EventWriter::emit(
421 self.context.events(),
422 None,
423 Some(InternalEvent::TaskScheduled),
424 );
425 Ok(())
426 }
427}
428
429fn decode_supported_content(bytes: &[u8]) -> Option<ContentProto> {
435 DeviceSyncContentProto::decode(bytes).ok()?.content
436}