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 mls_store::{MlsStore, MlsStoreError},
8 subscriptions::{SubscribeError, SyncWorkerEvent},
9 worker::{NeedsDbReconnect, metrics::WorkerMetrics},
10};
11use owo_colors::OwoColorize;
12use prost::Message;
13use std::{collections::HashMap, sync::Arc};
14use thiserror::Error;
15use tokio::sync::broadcast::error::RecvError;
16use tracing::instrument;
17use worker::SyncMetric;
18use xmtp_archive::ArchiveError;
19use xmtp_common::ErrorCode;
20use xmtp_common::{NS_IN_DAY, RetryableError, time::now_ns};
21use xmtp_content_types::encoded_content_to_bytes;
22use xmtp_db::tasks::NewTask;
23use xmtp_db::{NotFound, StorageError, consent_record::ConsentState, group::GroupQueryArgs};
24use xmtp_db::{XmtpDb, group::ConversationType, prelude::*};
25use xmtp_id::{InboxIdRef, associations::DeserializationError};
26use xmtp_mls_common::group::GroupMetadataOptions;
27use xmtp_proto::types::{GroupId, InstallationId};
28use xmtp_proto::xmtp::{
29 device_sync::content::{
30 DeviceSyncContent as DeviceSyncContentProto, device_sync_content::Content as ContentProto,
31 },
32 mls::{
33 database::{
34 AddMissingInstallations as AddMissingInstallationsProto, Task as TaskProto,
35 task::Task as TaskKindProto,
36 },
37 message_contents::{
38 ContentTypeId, EncodedContent, PlaintextEnvelope,
39 plaintext_envelope::{Content, V1},
40 },
41 },
42};
43
44pub mod archive;
45pub mod preference_sync;
46pub mod worker;
47
48pub use xmtp_archive::archive_options::{ArchiveOptions, BackupElementSelection};
49
50#[cfg(test)]
51mod tests;
52
53#[derive(Debug, Error, ErrorCode)]
54pub enum DeviceSyncError {
55 #[error("IO error: {0}")]
59 IO(#[from] std::io::Error),
60 #[error("Serialization/Deserialization Error {0}")]
64 Serde(#[from] serde_json::Error),
65 #[error(transparent)]
66 #[error_code(inherit)]
67 ProtoConversion(#[from] xmtp_proto::ConversionError),
68 #[error("AES-GCM encryption error")]
72 AesGcm(#[from] aes_gcm::Error),
73 #[error("storage error: {0}")]
74 #[error_code(inherit)]
75 Storage(#[from] StorageError),
76 #[error("type conversion error")]
80 Conversion,
81 #[error("utf-8 error: {0}")]
85 UTF8(#[from] std::str::Utf8Error),
86 #[error("client error: {0}")]
87 #[error_code(inherit)]
88 Client(#[from] ClientError),
89 #[error("group error: {0}")]
90 #[error_code(inherit)]
91 Group(#[from] GroupError),
92 #[error("invalid history message payload")]
96 InvalidPayload,
97 #[error("unspecified device sync kind")]
101 UnspecifiedDeviceSyncKind,
102 #[error(transparent)]
103 #[error_code(inherit)]
104 Subscribe(#[from] SubscribeError),
105 #[error(transparent)]
109 Bincode(#[from] bincode::Error),
110 #[error(transparent)]
114 Archive(#[from] ArchiveError),
115 #[error(transparent)]
119 Decode(#[from] prost::DecodeError),
120 #[error(transparent)]
121 #[error_code(inherit)]
122 Deserialization(#[from] DeserializationError),
123 #[error("Missing sync group")]
127 MissingSyncGroup,
128 #[error(transparent)]
129 #[error_code(inherit)]
130 Db(#[from] xmtp_db::ConnectionError),
131 #[error("{}", _0.to_string())]
135 Sync(Box<SyncSummary>),
136 #[error(transparent)]
140 MlsStore(#[from] MlsStoreError),
141 #[error(transparent)]
145 Recv(#[from] RecvError),
146 #[error("Missing Field: {0:?} {1}")]
150 MissingField(MissingField, String),
151}
152
153#[derive(Debug)]
154pub enum MissingField {
155 Conversation(ConversationField),
156}
157#[derive(Debug)]
158pub enum ConversationField {
159 DmId,
160}
161
162impl From<SyncSummary> for DeviceSyncError {
163 fn from(value: SyncSummary) -> Self {
164 DeviceSyncError::Sync(Box::new(value))
165 }
166}
167
168impl NeedsDbReconnect for DeviceSyncError {
169 fn needs_db_reconnect(&self) -> bool {
170 match self {
171 Self::Client(s) => s.db_needs_connection(),
172 Self::Storage(s) => s.db_needs_connection(),
173 Self::Db(c) => c.db_needs_connection(),
176 Self::Group(e) => e.needs_db_reconnect(),
177 Self::MlsStore(e) => e.needs_db_reconnect(),
178 Self::Subscribe(e) => e.needs_db_reconnect(),
179 _ => false,
180 }
181 }
182}
183
184impl RetryableError for DeviceSyncError {
185 fn is_retryable(&self) -> bool {
186 !matches!(
187 self,
188 Self::MissingSyncGroup | Self::UnspecifiedDeviceSyncKind
189 )
190 }
191}
192
193impl From<NotFound> for DeviceSyncError {
194 fn from(value: NotFound) -> Self {
195 DeviceSyncError::Storage(StorageError::NotFound(value))
196 }
197}
198
199#[derive(Clone)]
200pub struct DeviceSyncClient<Context> {
201 pub(crate) context: Context,
202 pub(crate) mls_store: MlsStore<Context>,
203 pub(crate) metrics: Arc<WorkerMetrics<SyncMetric>>,
204}
205
206impl<Context: XmtpSharedContext> DeviceSyncClient<Context> {
207 pub fn new(context: Context, metrics: Arc<WorkerMetrics<SyncMetric>>) -> Self {
208 Self {
209 context: context.clone(),
210 mls_store: MlsStore::new(context),
211 metrics,
212 }
213 }
214}
215
216impl<Context> DeviceSyncClient<Context>
217where
218 Context: XmtpSharedContext,
219{
220 pub fn inbox_id(&self) -> InboxIdRef<'_> {
221 self.context.identity().inbox_id()
222 }
223
224 pub fn installation_id(&self) -> InstallationId {
225 self.context.installation_id()
226 }
227
228 pub fn db(&self) -> <Context::Db as XmtpDb>::DbQuery {
229 self.context.db()
230 }
231
232 pub async fn wait_for_sync_worker_init(&self) -> Result<(), xmtp_common::time::Expired> {
234 self.metrics.wait_for_init().await
235 }
236
237 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = self.context.inbox_id()), skip(self)))]
241 #[cfg_attr(
242 not(any(test, feature = "test-utils")),
243 tracing::instrument(level = "trace", skip(self))
244 )]
245 async fn send_device_sync_message(
246 &self,
247 content: ContentProto,
248 ) -> Result<Vec<u8>, ClientError> {
249 let content = DeviceSyncContentProto {
250 content: Some(content),
251 };
252
253 let sync_group = self.get_sync_group().await?;
254
255 let msg = format!(
256 "[{}] Sending sync message to group {:?}",
257 self.context.installation_id(),
258 xmtp_common::fmt::debug_hex(sync_group.group_id)
259 );
260 tracing::info!("{}", msg.yellow());
261
262 let mut content_bytes = vec![];
263 content
264 .encode(&mut content_bytes)
265 .map_err(|err| ClientError::Generic(err.to_string()))?;
266
267 let encoded_content = EncodedContent {
268 r#type: Some(ContentTypeId {
269 authority_id: "xmtp.org".to_string(),
270 type_id: "application/x-protobuf".to_string(),
271 version_major: 1,
272 version_minor: 0,
273 }),
274 parameters: HashMap::new(),
275 fallback: None,
276 compression: None,
277 content: content_bytes,
278 };
279 let content_bytes = encoded_content_to_bytes(encoded_content);
280
281 let message_id = sync_group.prepare_message(
282 &content_bytes,
283 send_message_opts::SendMessageOpts {
284 should_push: false,
285 idempotency_key: None,
286 },
287 |key| PlaintextEnvelope {
288 content: Some(Content::V1(V1 {
289 content: content_bytes.clone(),
290 idempotency_key: key.to_string(),
291 })),
292 },
293 )?;
294
295 sync_group.sync_until_last_intent_resolved().await?;
296
297 let _ = self
299 .context
300 .worker_events()
301 .send(SyncWorkerEvent::NewSyncGroupMsg);
302
303 Ok(message_id)
304 }
305
306 #[instrument(level = "trace", skip_all)]
307 pub async fn get_sync_group(&self) -> Result<MlsGroup<Context>, GroupError> {
308 let db = self.context.db();
309 let sync_group = match db.primary_sync_group()? {
310 Some(sync_group) => self.mls_store.group(&sync_group.id)?,
311 None => {
312 let sync_group = MlsGroup::create_and_insert(
313 self.context.clone(),
314 ConversationType::Sync,
315 PreconfiguredPolicies::default().to_policy_set(),
316 GroupMetadataOptions::default(),
317 None,
318 )?;
319 tracing::info!(
320 "[{}] Creating sync group: {}",
321 hex::encode(self.context.installation_id()),
322 hex::encode(sync_group.group_id)
323 );
324 if let Err(inline_err) = sync_group.add_missing_installations().await {
325 self.schedule_add_missing_installations_task(sync_group.group_id)
332 .map_err(Box::new)?;
333 return Err(inline_err);
334 }
335 sync_group.sync_with_conn().await?;
336
337 self.metrics.increment_metric(SyncMetric::SyncGroupCreated);
338
339 sync_group
340 }
341 };
342
343 Ok(sync_group)
344 }
345
346 #[cfg_attr(
354 any(test, feature = "test-utils"),
355 tracing::instrument(level = "info", skip_all)
356 )]
357 pub fn schedule_add_installations_to_groups(&self) -> Result<usize, DeviceSyncError> {
358 let groups = self.mls_store.find_groups(GroupQueryArgs {
359 last_activity_after_ns: Some(now_ns() - NS_IN_DAY * 90),
360 consent_states: Some(vec![ConsentState::Allowed, ConsentState::Unknown]),
361 ..Default::default()
362 })?;
363
364 for group in &groups {
365 self.schedule_add_missing_installations_task(group.group_id)?;
366 }
367 Ok(groups.len())
368 }
369
370 pub(crate) fn schedule_add_missing_installations_task(
374 &self,
375 group_id: GroupId,
376 ) -> Result<(), DeviceSyncError> {
377 let task = NewTask::builder()
378 .originating_message_sequence_id(0)
379 .build(TaskProto {
380 task: Some(TaskKindProto::AddMissingInstallations(
381 AddMissingInstallationsProto {
382 group_id: group_id.to_vec(),
383 },
384 )),
385 })?;
386 self.context.db().create_or_ignore_task(task)?;
387 self.context.task_channels().wake();
388 Ok(())
389 }
390}
391
392fn decode_supported_content(bytes: &[u8]) -> Option<ContentProto> {
398 DeviceSyncContentProto::decode(bytes).ok()?.content
399}