1use super::DeviceSyncError;
2use crate::groups::deletion::{
3 ArchivedSelfDeletions, DiscardEvents, Source, apply_deletion, apply_stored_records,
4 import_record,
5};
6use crate::{
7 context::XmtpSharedContext, groups::MlsGroup, state_tx::state_write,
8 subscriptions::internal::InternalEvent, worker::NeedsDbReconnect,
9};
10use futures::{Stream, StreamExt};
11use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
12pub use xmtp_archive::*;
13use xmtp_configuration::LEGACY_GROUP_ID_BYTES;
14use xmtp_db::{
15 StorageError,
16 TransactionOutcome::Continue,
17 consent_record::{ConsentKey, ConsentWrite, Owed, parse_consent_save, write_consent},
18 group::ConversationType,
19 group_message::StoredGroupMessage,
20 message_deletion::StoredMessageDeletion,
21 prelude::*,
22};
23use xmtp_proto::types::GroupId;
24use xmtp_proto::xmtp::device_sync::{
25 BackupElement, backup_element::Element, consent_backup::ConsentSave,
26};
27
28use xmtp_events::{ArchiveRestored, ClientEvent, EventWriter};
29#[derive(Default)]
30struct ImportContext {
31 changed: bool,
32 deletions_stored: bool,
36 consent: Vec<ConsentSave>,
40 dm_groups: HashSet<GroupId>,
42 internal: HashMap<GroupId, bool>,
45 self_deletions: ArchivedSelfDeletions,
48}
49
50impl ImportContext {
51 fn is_internal(&mut self, db: &impl DbQuery, group_id: GroupId) -> Result<bool, StorageError> {
52 if let Some(internal) = self.internal.get(&group_id) {
53 return Ok(*internal);
54 }
55 let internal = db
56 .find_group(&group_id)?
57 .is_some_and(|group| group.conversation_type.is_virtual());
58 self.internal.insert(group_id, internal);
59 Ok(internal)
60 }
61}
62
63#[cfg(test)]
68pub(crate) async fn insert_importer(
69 importer: &mut ArchiveImporter,
70 context: &impl XmtpSharedContext,
71) -> Result<(), DeviceSyncError> {
72 insert_elements(importer, context, ArchivedSelfDeletions::default()).await
73}
74
75pub async fn insert_archive<F, Fut>(
79 first: &mut ArchiveImporter,
80 reopen: F,
81 context: &impl XmtpSharedContext,
82) -> Result<(), DeviceSyncError>
83where
84 F: FnOnce() -> Fut,
85 Fut: std::future::Future<Output = Result<ArchiveImporter, ArchiveError>>,
86{
87 let self_deletions = collect_self_deletions(first).await?;
88 let mut importer = reopen().await?;
89 insert_elements(&mut importer, context, self_deletions).await
90}
91
92async fn collect_self_deletions<S, E>(elements: &mut S) -> Result<ArchivedSelfDeletions, E>
96where
97 S: Stream<Item = Result<BackupElement, E>> + Unpin,
98{
99 let mut found = ArchivedSelfDeletions::default();
100 while let Some(element) = elements.next().await {
101 if let Some(Element::GroupMessage(save)) = element?.element
102 && let Ok(message) = StoredGroupMessage::try_from(save)
103 {
104 found.note(&message);
105 }
106 }
107 Ok(found)
108}
109
110#[cfg(any(test, feature = "test-utils"))]
112#[doc(hidden)]
113pub async fn insert_elements_for_test<S, E>(
114 elements: &mut S,
115 context: &impl XmtpSharedContext,
116) -> Result<(), DeviceSyncError>
117where
118 S: Stream<Item = Result<BackupElement, E>> + Unpin,
119 DeviceSyncError: From<E>,
120{
121 insert_elements(elements, context, ArchivedSelfDeletions::default()).await
122}
123
124async fn insert_elements<S, E>(
126 elements: &mut S,
127 context: &impl XmtpSharedContext,
128 self_deletions: ArchivedSelfDeletions,
129) -> Result<(), DeviceSyncError>
130where
131 S: Stream<Item = Result<BackupElement, E>> + Unpin,
132 DeviceSyncError: From<E>,
133{
134 let mut import = ImportContext {
135 self_deletions,
136 ..Default::default()
137 };
138 let mut first_error = None;
139 while let Some(element) = elements.next().await {
140 let element = match element {
141 Ok(element) => element,
142 Err(e) => {
144 first_error.get_or_insert(DeviceSyncError::from(e));
145 break;
146 }
147 };
148 if let Err(e) = insert(element, context, &mut import) {
151 if e.needs_db_reconnect() {
152 return Err(e);
153 }
154 first_error.get_or_insert(e);
155 }
156 }
157 let consent = state_write(context.mls_storage(), |tx| {
159 let storage = tx.storage();
160 let db = storage.db();
161 let changed = import_consent(
162 &db,
163 context.inbox_id(),
164 &import,
165 xmtp_common::time::now_ns(),
166 )?;
167 Ok::<_, StorageError>(Continue(changed))
168 });
169 match consent {
170 Ok(changed) => import.changed |= changed.into_continued(),
171 Err(e) if e.db_needs_connection() => return Err(e.into()),
172 Err(e) => {
173 first_error.get_or_insert(e.into());
174 }
175 }
176 let result = first_error.map_or(Ok(()), Err);
177 if import.changed {
178 context.task_channels().mark_notification_changed();
179 context.events().emit(
180 Some(ClientEvent::ArchiveRestored(ArchiveRestored {
181 complete: result.is_ok(),
182 })),
183 Some(InternalEvent::OutboxChanged),
185 );
186 } else if import.deletions_stored {
187 context.events().emit(
188 Some(ClientEvent::ArchiveRestored(ArchiveRestored {
189 complete: result.is_ok(),
190 })),
191 None,
192 );
193 }
194 result
195}
196
197fn insert(
198 element: BackupElement,
199 context: &impl XmtpSharedContext,
200 import: &mut ImportContext,
201) -> Result<(), DeviceSyncError> {
202 let Some(element) = element.element else {
203 return Ok(());
204 };
205
206 match element {
207 Element::Consent(save) => {
209 if parse_consent_save(&save).is_some() {
210 import.consent.push(save);
211 }
212 }
213 Element::Group(save)
217 if ConversationType::try_from(save.conversation_type())
218 .is_ok_and(|kind| kind.is_virtual()) => {}
219 Element::Group(save) => {
220 if save.dm_id.is_some()
223 && save.id.len() == LEGACY_GROUP_ID_BYTES
224 && let Ok(id) = GroupId::try_from(save.id.as_slice())
225 {
226 import.dm_groups.insert(id);
227 }
228 import.changed |= MlsGroup::restore_from_archive(context, &save)?;
229 }
230 Element::MessageDeletion(save) => {
232 let record: StoredMessageDeletion = save.try_into()?;
233 if !import.is_internal(&context.db(), record.group_id)? {
234 import.deletions_stored |= state_write(context.mls_storage(), |tx| {
235 let stored = import_record(&tx.storage().db(), &DiscardEvents, &record)?;
236 Ok::<_, StorageError>(Continue(stored))
237 })?
238 .into_continued();
239 }
240 }
241 Element::GroupMessage(message) => {
243 let message: StoredGroupMessage = message.try_into()?;
247 if import.is_internal(&context.db(), message.group_id)? {
248 return Ok(());
249 }
250 import.self_deletions.note(&message);
251 let self_deletions = &import.self_deletions;
252 let (inserted, deletions) = state_write(context.mls_storage(), |tx| {
253 let storage = tx.storage();
254 let db = storage.db();
255 let inserted = message.store_or_ignore_changed(&db)?;
256 let mut deletions = inserted && self_deletions.apply_to_inserted(&db, &message)?;
258 deletions |= apply_deletion(&db, &DiscardEvents, &message, Source::Archive)?;
262 if let Some(row) = db.get_group_message(&message.id)? {
263 deletions |= apply_stored_records(&db, &DiscardEvents, &row)?;
264 }
265 Ok::<_, StorageError>(Continue((inserted, deletions)))
266 })?
267 .into_continued();
268 import.changed |= inserted;
269 import.deletions_stored |= deletions;
270 }
271 _ => {}
272 }
273
274 Ok(())
275}
276
277fn import_consent(
285 db: &impl DbQuery,
286 own_inbox: &str,
287 import: &ImportContext,
288 now_ns: i64,
289) -> Result<bool, StorageError> {
290 let mut carried = BTreeSet::new();
292 for id in &import.dm_groups {
293 if let Some((peer, _)) = ConsentKey::group(id.as_ref()).pre8_dm(db, own_inbox)? {
294 carried.insert(peer);
295 }
296 }
297 let mut plain: BTreeMap<ConsentKey, Vec<ConsentSave>> = BTreeMap::new();
298 let mut pre8: BTreeMap<ConsentKey, Vec<ConsentSave>> = BTreeMap::new();
299 for save in &import.consent {
300 let Some((key, _)) = parse_consent_save(save) else {
302 continue;
303 };
304 let key = key.pre8_dm(db, own_inbox)?.map_or(key, |(peer, _)| peer);
306 let rows = if carried.contains(&key) {
307 pre8.entry(key)
308 } else {
309 plain.entry(key)
310 };
311 rows.or_default().push(save.clone());
312 }
313 for (peer, rows) in pre8 {
314 let combined = pre8::pre8_peer_consent(&peer.entity, &rows);
315 plain.insert(peer, combined.into_iter().collect());
316 }
317 let mut changed = false;
318 for (key, saves) in plain {
319 for save in saves {
320 let Some((_, value)) = parse_consent_save(&save) else {
321 continue;
322 };
323 let write = ConsentWrite::from_archive(value, now_ns);
324 changed |= write_consent(db, &key, write, Owed::Mark)?.is_some();
325 }
326 }
327 Ok(changed)
328}
329
330#[cfg(test)]
331mod tests {
332 #![allow(unused)]
333 use super::*;
334 use crate::groups::send_message_opts::SendMessageOpts;
335 use crate::prelude::*;
336 use crate::tester;
337 use crate::utils::{LocalTester, Tester};
338 use crate::worker::device_sync::{ArchiveOptions, BackupElementSelection};
339 use crate::{builder::ClientBuilder, utils::test::wait_for_min_intents};
340 use diesel::prelude::*;
341 use futures::AsyncReadExt;
342 use futures::io::{BufReader, Cursor};
343 use std::{path::Path, sync::Arc};
344 use xmtp_archive::exporter;
345 use xmtp_cryptography::utils::generate_local_wallet;
346 use xmtp_db::group_message::MsgQueryArgs;
347 use xmtp_db::{ConnectionExt, group::GroupMembershipState};
348 use xmtp_db::{
349 consent_record::StoredConsentRecord,
350 group::StoredGroup,
351 group_message::StoredGroupMessage,
352 schema::{consent_records, group_messages, groups},
353 };
354 use xmtp_mls_group_state::{
355 group::{DMMetadataOptions, GroupMetadataOptions},
356 group_mutable_metadata::MessageDisappearingSettings,
357 };
358 use xmtp_proto::types::GroupId;
359
360 struct WriteDuringExport<F: FnMut()> {
363 archive: Vec<u8>,
364 on_first: Option<F>,
365 }
366
367 impl<F: FnMut()> std::io::Write for WriteDuringExport<F> {
368 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
369 if self.archive.len() >= 2 + xmtp_archive::NONCE_SIZE {
370 self.on_first.take().into_iter().for_each(|mut f| f());
371 }
372 self.archive.write(buf)
373 }
374
375 fn flush(&mut self) -> std::io::Result<()> {
376 Ok(())
377 }
378 }
379
380 #[xmtp_common::test(unwrap_try = true)]
386 async fn archive_export_fails_on_undecodable_group_metadata() {
387 use openmls::{
388 component::ComponentData,
389 group::MlsGroup,
390 messages::proposals::{AppDataUpdateProposal, Proposal},
391 };
392 use openmls_traits::OpenMlsProvider;
393 use xmtp_archive::{ArchiveError, UnreadableGroup};
394 use xmtp_mls_group_state::app_data::component_id::ComponentId;
395
396 tester!(alix, disable_workers);
397 let conversation = alix.create_group(None, None)?;
398 let provider = alix.context.mls_provider();
399 let signer = &alix.context.identity().installation_keys;
400 let mut group = MlsGroup::load(
401 alix.context.mls_storage(),
402 &conversation.group_id.to_openmls(),
403 )??;
404 let malformed = vec![0xff; 3];
405 let mut stage = crate::groups::mls_ext::commit_without_group_info(
406 &mut group,
407 provider.storage(),
408 |builder| {
409 builder.add_proposal(Proposal::AppDataUpdate(Box::new(
410 AppDataUpdateProposal::update(
411 ComponentId::ADMIN_LIST.as_u16(),
412 malformed.clone(),
413 ),
414 )))
415 },
416 )?;
417 let mut updater = stage.app_data_dictionary_updater();
418 updater.set(ComponentData::from_parts(
419 ComponentId::ADMIN_LIST.as_u16(),
420 malformed.into(),
421 ));
422 stage.with_app_data_dictionary_updates(updater.changes());
423 stage
424 .build(provider.rand(), provider.crypto(), signer, |_| true)?
425 .stage_commit(&provider)?;
426 group.merge_pending_commit(&provider)?;
427
428 let opts = ArchiveOptions {
429 start_ns: None,
430 end_ns: None,
431 elements: vec![BackupElementSelection::Messages],
432 exclude_disappearing_messages: false,
433 };
434 let key = xmtp_common::rand_vec::<32>();
435 let failure = exporter::export(opts, alix.db(), &key, Vec::new());
436 assert!(
437 matches!(
438 failure,
439 Err(ArchiveError::UnreadableGroup {
440 group_id,
441 source: UnreadableGroup::MutableMetadata(_),
442 }) if group_id == conversation.group_id
443 ),
444 "export dropped an undecodable metadata value: {failure:?}"
445 );
446 }
447
448 #[xmtp_common::test(unwrap_try = true)]
454 async fn archive_export_holds_no_group_keys() {
455 use openmls::{
456 component::ComponentData,
457 group::MlsGroup,
458 messages::proposals::{AppDataUpdateProposal, Proposal},
459 };
460 use openmls_traits::OpenMlsProvider;
461 use prost::Message;
462 use xmtp_mls_group_state::app_data::{
463 component_id::ComponentId, group_root::read_group_root,
464 };
465
466 tester!(alix, disable_workers);
467 let conversation = alix.create_group(None, None)?;
468 conversation
469 .send_message(b"archived", Default::default())
470 .await?;
471 let root = conversation
472 .with_group_snapshot(|mls_group| Ok(read_group_root(mls_group.extensions())))???;
473 let provider = alix.context.mls_provider();
474 let signer = &alix.context.identity().installation_keys;
475 let mut group = MlsGroup::load(
476 alix.context.mls_storage(),
477 &conversation.group_id.to_openmls(),
478 )??;
479 let commit_log_signer = xmtp_common::rand_vec::<32>();
480 let mut stage = crate::groups::mls_ext::commit_without_group_info(
481 &mut group,
482 provider.storage(),
483 |builder| {
484 builder.add_proposal(Proposal::AppDataUpdate(Box::new(
485 AppDataUpdateProposal::update(
486 ComponentId::COMMIT_LOG_SIGNER.as_u16(),
487 commit_log_signer.clone(),
488 ),
489 )))
490 },
491 )?;
492 let mut updater = stage.app_data_dictionary_updater();
493 updater.set(ComponentData::from_parts(
494 ComponentId::COMMIT_LOG_SIGNER.as_u16(),
495 commit_log_signer.clone().into(),
496 ));
497 stage.with_app_data_dictionary_updates(updater.changes());
498 stage
499 .build(provider.rand(), provider.crypto(), signer, |_| true)?
500 .stage_commit(&provider)?;
501 group.merge_pending_commit(&provider)?;
502
503 let opts = ArchiveOptions {
504 start_ns: None,
505 end_ns: None,
506 elements: vec![
507 BackupElementSelection::Messages,
508 BackupElementSelection::Consent,
509 ],
510 exclude_disappearing_messages: false,
511 };
512 let key = xmtp_common::rand_vec::<32>();
513 let mut archive = Vec::new();
514 exporter::export(opts, alix.db(), &key, &mut archive)?;
515 let reader = Box::pin(BufReader::new(Cursor::new(archive)));
516 let elements: Vec<_> = ArchiveImporter::load(reader, &key)
517 .await?
518 .collect::<Vec<_>>()
519 .await
520 .into_iter()
521 .collect::<Result<_, _>>()?;
522
523 let mut saved = None;
524 let mut plaintext = Vec::new();
525 for element in &elements {
526 plaintext.extend(element.encode_to_vec());
527 if let Some(Element::Group(group)) = &element.element
528 && group.id == conversation.group_id.to_vec()
529 {
530 saved = Some(group.clone());
531 }
532 }
533 let attributes = saved?.mutable_metadata?.attributes;
534 assert!(
535 attributes.contains_key("group_name"),
536 "the archive lost the group's attributes: {attributes:?}"
537 );
538 assert!(!attributes.contains_key("_commit_log_signer"));
539 let contains = |needle: &[u8]| plaintext.windows(needle.len()).any(|w| w == needle);
540 for (secret, encoded) in crate::utils::test::group_secret_encodings(&root) {
541 assert!(!contains(&encoded), "the archive holds the {secret}");
542 }
543 assert!(
544 !contains(&commit_log_signer),
545 "the archive holds the signer"
546 );
547 assert!(
548 !contains(hex::encode(&commit_log_signer).as_bytes()),
549 "the archive holds the signer as hex"
550 );
551 assert!(!contains(b"_commit_log_signer"));
552 }
553
554 #[xmtp_common::test(unwrap_try = true)]
564 async fn archive_snapshot_is_complete() {
565 use xmtp_db::consent_record::{ConsentState, ConsentType};
566 use xmtp_proto::xmtp::device_sync::{
567 backup_element::Element, consent_backup::ConsentStateSave,
568 };
569
570 tester!(alix, persistent_db, disable_workers);
571 tester!(bo, disable_workers);
572 let empty_group = alix.create_group(None, None)?;
573 let empty_dm = alix.find_or_create_dm(bo.inbox_id(), None).await?;
574 let outside = alix.create_group(None, None)?;
575 let outside_message = outside.send_message_optimistic(b"before", Default::default())?;
576 let old = alix.create_group(None, None)?;
577 let start_ns = xmtp_common::time::now_ns();
578 xmtp_common::time::sleep(std::time::Duration::from_millis(2)).await;
581 let in_window = old.send_message_optimistic(&[7; 512 * 1024], Default::default())?;
583 let consent = |entity: &str, state| {
584 StoredConsentRecord::new(ConsentType::InboxId, state, entity.to_string())
585 };
586 consent("carol", ConsentState::Allowed).merge_into(&alix.db())?;
587
588 let opts = ArchiveOptions {
589 elements: vec![
590 BackupElementSelection::Messages,
591 BackupElementSelection::Consent,
592 ],
593 start_ns: Some(start_ns),
594 end_ns: None,
595 exclude_disappearing_messages: false,
596 };
597 let key = vec![7; 32];
598 let writer = alix.db();
599 let mut sink = WriteDuringExport {
600 archive: vec![],
601 on_first: Some(|| {
602 let written = consent("carol", ConsentState::Denied)
603 .merge_into(&writer)
604 .and_then(|_| consent("dave", ConsentState::Allowed).merge_into(&writer));
605 #[cfg(not(target_arch = "wasm32"))]
609 written.unwrap();
610 #[cfg(target_arch = "wasm32")]
611 assert!(written.is_err(), "wasm wrote during the export");
612 }),
613 };
614 exporter::export(opts, alix.db(), &key, &mut sink)?;
615 assert!(
616 sink.on_first.is_none(),
617 "the export never wrote mid-snapshot"
618 );
619 let archive = sink.archive;
620
621 let reader = Box::pin(BufReader::new(Cursor::new(archive.clone())));
622 let consent: Vec<_> = ArchiveImporter::load(reader, &key)
623 .await?
624 .filter_map(|e| async move {
625 match e.ok()?.element? {
626 Element::Consent(c) if c.entity == "carol" || c.entity == "dave" => Some(c),
627 _ => None,
628 }
629 })
630 .collect()
631 .await;
632 assert!(
633 matches!(
634 consent.as_slice(),
635 [c] if c.entity == "carol" && c.state == ConsentStateSave::Allowed as i32
636 ),
637 "the export saw writes committed after its snapshot: {consent:?}"
638 );
639
640 tester!(alix2, from: alix, disable_workers);
641 let events = alix2.context.events().subscribe(
642 xmtp_events::EventFilter::new([
643 xmtp_events::EventKind::ArchiveRestored,
644 xmtp_events::EventKind::ConversationJoined,
645 xmtp_events::EventKind::MessageReceived,
646 xmtp_events::EventKind::ConsentChanged,
647 ]),
648 Some(64),
649 );
650 let reader = Box::pin(BufReader::new(Cursor::new(archive)));
651 let mut importer = ArchiveImporter::load(reader, &key).await?;
652 insert_importer(&mut importer, &alix2.context).await?;
653
654 let db = alix2.db();
655 for group in [&empty_group, &empty_dm, &outside, &old] {
656 assert_eq!(
657 db.find_group(&group.group_id)??.membership_state,
658 GroupMembershipState::Restored
659 );
660 }
661 assert!(db.get_group_message(&in_window)?.is_some());
662 assert!(db.get_group_message(&outside_message)?.is_none());
663 assert!(
664 matches!(events.drain().as_slice(), [xmtp_events::EventEnvelope { client: Some(ClientEvent::ArchiveRestored(restored)), .. }] if restored.complete)
665 );
666 }
667
668 #[xmtp_common::test(unwrap_try = true)]
670 async fn partial_import_reports_incomplete_after_a_stored_change() {
671 tester!(alix, disable_workers);
672 let events = alix.context.events().subscribe(
673 xmtp_events::EventFilter::new([xmtp_events::EventKind::ArchiveRestored]),
674 Some(4),
675 );
676 let record = StoredConsentRecord::new(
677 xmtp_db::consent_record::ConsentType::InboxId,
678 xmtp_db::consent_record::ConsentState::Allowed,
679 "failed-import".into(),
680 );
681 let element = BackupElement {
682 element: Some(Element::Consent(record.into())),
683 };
684 let mut elements =
685 futures::stream::iter([Ok(element), Err(std::io::Error::other("archive cut"))]);
686 assert!(
687 insert_elements(&mut elements, &alix.context, Default::default())
688 .await
689 .is_err()
690 );
691 assert!(
692 alix.db()
693 .consent_row(&xmtp_db::consent_record::ConsentKey::inbox("failed-import"))?
694 .is_some()
695 );
696 assert!(matches!(
697 events.drain().as_slice(),
698 [xmtp_events::EventEnvelope {
699 client: Some(ClientEvent::ArchiveRestored(restored)), ..
700 }] if !restored.complete
701 ));
702 }
703
704 #[xmtp_common::test(unwrap_try = true)]
706 async fn failed_import_suppresses_events_for_all_stored_elements() {
707 tester!(alix, disable_workers);
708 tester!(bo, disable_workers);
709 let group = alix.create_group(None, None)?;
710 let message_id = group.send_message_optimistic(b"archive message", Default::default())?;
711 StoredConsentRecord::new(
712 xmtp_db::consent_record::ConsentType::InboxId,
713 xmtp_db::consent_record::ConsentState::Denied,
714 bo.inbox_id().into(),
715 )
716 .merge_into(&alix.db())?;
717 let key = vec![9; 32];
718 let mut archive = vec![];
719 exporter::export(
720 ArchiveOptions {
721 elements: vec![
722 BackupElementSelection::Messages,
723 BackupElementSelection::Consent,
724 ],
725 start_ns: None,
726 end_ns: None,
727 exclude_disappearing_messages: false,
728 },
729 alix.db(),
730 &key,
731 &mut archive,
732 )?;
733 let importer =
734 ArchiveImporter::load(Box::pin(BufReader::new(Cursor::new(archive))), &key).await?;
735 let mut elements = importer
736 .map(|item| item.map_err(DeviceSyncError::from))
737 .chain(futures::stream::iter([Err(DeviceSyncError::from(
738 std::io::Error::other("archive cut"),
739 ))]));
740 tester!(restored, from: alix, disable_workers);
741 let events = restored.context.events().subscribe(
742 xmtp_events::EventFilter::new([
743 xmtp_events::EventKind::ArchiveRestored,
744 xmtp_events::EventKind::ConversationJoined,
745 xmtp_events::EventKind::MessageReceived,
746 xmtp_events::EventKind::ConsentChanged,
747 ]),
748 Some(64),
749 );
750 assert!(
751 insert_elements(&mut elements, &restored.context, Default::default())
752 .await
753 .is_err()
754 );
755 assert!(restored.db().find_group(&group.group_id)?.is_some());
756 assert!(restored.db().get_group_message(&message_id)?.is_some());
757 assert!(
758 restored
759 .db()
760 .consent_row(&xmtp_db::consent_record::ConsentKey::inbox(bo.inbox_id()))?
761 .is_some()
762 );
763 assert!(
764 matches!(events.drain().as_slice(), [xmtp_events::EventEnvelope { client: Some(ClientEvent::ArchiveRestored(restored)), .. }] if !restored.complete)
765 );
766 }
767 #[xmtp_common::test(unwrap_try = true)]
771 async fn archived_consent_keeps_the_latest_record() {
772 tester!(alix, disable_workers);
773 let soon = xmtp_common::time::now_ns() + xmtp_common::NS_IN_HOUR;
774 let records = [
775 (xmtp_db::consent_record::ConsentState::Denied, i64::MAX),
776 (xmtp_db::consent_record::ConsentState::Allowed, 10),
777 (xmtp_db::consent_record::ConsentState::Allowed, soon),
778 (xmtp_db::consent_record::ConsentState::Denied, 20),
779 ];
780 let mut elements = futures::stream::iter(records.map(|(state, consented_at_ns)| {
781 let record = StoredConsentRecord {
782 entity_type: xmtp_db::consent_record::ConsentType::InboxId,
783 state,
784 entity: "archived-inbox".into(),
785 consented_at_ns,
786 owed_seq: 0,
787 };
788 Ok::<_, std::io::Error>(BackupElement {
789 element: Some(Element::Consent(record.into())),
790 })
791 }));
792 insert_elements(&mut elements, &alix.context, Default::default()).await?;
793 let limit = xmtp_common::time::now_ns() + xmtp_common::NS_IN_DAY;
794 let stored = alix
795 .db()
796 .consent_row(&xmtp_db::consent_record::ConsentKey::inbox(
797 "archived-inbox",
798 ))?
799 .unwrap();
800 assert_eq!(stored.state, xmtp_db::consent_record::ConsentState::Denied);
801 assert!(stored.consented_at_ns > soon && stored.consented_at_ns <= limit);
802 }
803
804 #[xmtp_common::test(unwrap_try = true)]
808 async fn archived_consent_time_move_reports_a_restore() {
809 tester!(alix, disable_workers);
810 let events = alix.context.events().subscribe(
811 xmtp_events::EventFilter::new([xmtp_events::EventKind::ArchiveRestored]),
812 Some(4),
813 );
814 let record = |consented_at_ns| StoredConsentRecord {
815 entity_type: xmtp_db::consent_record::ConsentType::InboxId,
816 state: xmtp_db::consent_record::ConsentState::Allowed,
817 entity: "time-move".into(),
818 consented_at_ns,
819 owed_seq: 0,
820 };
821 assert!(record(10).merge_into(&alix.db())?.is_some());
822 let mut elements = futures::stream::iter([Ok::<_, std::io::Error>(BackupElement {
823 element: Some(Element::Consent(record(30).into())),
824 })]);
825 insert_elements(&mut elements, &alix.context, Default::default()).await?;
826 let stored = alix
827 .db()
828 .consent_row(&xmtp_db::consent_record::ConsentKey::inbox("time-move"))?
829 .unwrap();
830 assert_eq!(stored.consented_at_ns, 30);
831 assert!(matches!(
832 events.drain().as_slice(),
833 [xmtp_events::EventEnvelope {
834 client: Some(ClientEvent::ArchiveRestored(restored)), ..
835 }] if restored.complete
836 ));
837 }
838
839 #[xmtp_common::test(unwrap_try = true)]
840 async fn archive_timestamp_keeps_a_message_received_during_import() {
841 tester!(alix, disable_workers);
842 let group = alix.create_group(None, None)?;
843 let save = xmtp_proto::xmtp::device_sync::group_backup::GroupSave {
844 id: group.group_id.to_vec(),
845 last_message_ns: Some(0),
846 ..Default::default()
847 };
848
849 group.send_message_optimistic(b"message during import", Default::default())?;
850 let current = alix.db().find_group(&group.group_id)??.last_message_ns;
851 assert!(current > Some(0));
852 MlsGroup::restore_from_archive(&alix.context, &save)?;
853 assert_eq!(
854 alix.db().find_group(&group.group_id)??.last_message_ns,
855 current
856 );
857 }
858
859 #[xmtp_common::test(unwrap_try = true)]
860 async fn test_archive_timestamps() {
861 tester!(alix, disable_workers);
862 tester!(alix2, from: alix);
863 tester!(bo, disable_workers);
864
865 let alix_group = alix
866 .create_group_with_members(&[bo.inbox_id()], None, None)
867 .await?;
868 alix_group.send_message(b"hi", Default::default()).await?;
869
870 alix2.sync_welcomes().await?;
871 bo.sync_welcomes().await?;
872
873 let alix2_group = alix2.group(&alix_group.group_id)?;
874 let bo_group = bo.group(&alix_group.group_id)?;
875
876 alix2_group.sync().await?;
877 bo_group.sync().await?;
878
879 alix_group
881 .send_message(b"Hello again", Default::default())
882 .await?;
883
884 let key = vec![7; 32];
885 let opts = ArchiveOptions {
886 start_ns: None,
887 end_ns: None,
888 elements: vec![
889 BackupElementSelection::Messages,
890 BackupElementSelection::Consent,
891 ],
892 exclude_disappearing_messages: false,
893 };
894 let export = {
895 let mut file = vec![];
896 exporter::export(opts, alix.db(), &key, &mut file)?;
897 file
898 };
899
900 tester!(alix3, from: alix);
901
902 let reader = Box::pin(BufReader::new(Cursor::new(export.clone())));
905 let mut importer = ArchiveImporter::load(reader, &key).await?;
906 insert_importer(&mut importer, &alix2.context).await?;
907
908 let reader = Box::pin(BufReader::new(Cursor::new(export)));
909 let mut importer = ArchiveImporter::load(reader, &key).await?;
910 insert_importer(&mut importer, &alix3.context).await?;
911
912 let alix_timestamp = alix
913 .db()
914 .find_group(&alix_group.group_id)??
915 .last_message_ns?;
916 let alix2_timestamp = alix2
917 .db()
918 .find_group(&alix_group.group_id)??
919 .last_message_ns?;
920 let alix3_timestamp = alix3
921 .db()
922 .find_group(&alix_group.group_id)??
923 .last_message_ns?;
924
925 assert_eq!(alix2_timestamp, alix_timestamp);
927 assert_eq!(alix3_timestamp, alix_timestamp);
929 }
930
931 #[xmtp_common::test(unwrap_try = true)]
932 async fn test_dm_archive() {
933 tester!(alix, disable_workers);
934 tester!(bo, disable_workers);
935
936 let alix_bo_dm = alix.find_or_create_dm(bo.inbox_id(), None).await?;
937 let archived_message_id = alix_bo_dm
938 .send_message(b"old group", Default::default())
939 .await?;
940
941 let timestamp = alix
942 .db()
943 .find_group(&alix_bo_dm.group_id)??
944 .last_message_ns?;
945
946 let key = vec![7; 32];
947 let opts = ArchiveOptions {
948 start_ns: None,
949 end_ns: None,
950 elements: vec![
951 BackupElementSelection::Messages,
952 BackupElementSelection::Consent,
953 ],
954 exclude_disappearing_messages: false,
955 };
956 let export = {
957 let mut file = vec![];
958
959 exporter::export(opts, alix.db(), &key, &mut file)?;
960 file
961 };
962
963 tester!(alix2, from: alix);
964 let reader = Box::pin(BufReader::new(Cursor::new(export)));
965 let mut importer = ArchiveImporter::load(reader, &key).await?;
966 insert_importer(&mut importer, &alix2.context).await?;
967
968 let restored = alix2.group(&alix_bo_dm.group_id)?;
971 let restored_topic = xmtp_db::incoming_envelope::StreamTopic::group(alix_bo_dm.group_id);
972 let before_join = alix2.db().topic_progress(&restored_topic)?;
973 crate::mls_store::MlsStore::new(alix2.context.clone())
974 .receive_topics_once(
975 &[xmtp_proto::types::Topic::new_group_message(
976 alix_bo_dm.group_id,
977 )],
978 alix2
979 .context
980 .incoming_runtime()
981 .policy()
982 .incoming_limits(xmtp_db::incoming_envelope::NetworkEntityKind::Group),
983 )
984 .await?;
985 let received_before_join = alix2.db().topic_progress(&restored_topic)?;
986 assert!(received_before_join.received > before_join.processed);
987 assert!(matches!(
988 restored.process_pending_group_head(None)?,
989 crate::groups::mls_sync::GroupHeadOutcome::Inactive
990 ));
991 assert_eq!(
992 alix2.db().topic_progress(&restored_topic)?.processed,
993 before_join.processed
994 );
995 assert!(
996 !alix2
997 .db()
998 .pending_states_through(&restored_topic, received_before_join.received)?
999 .is_empty()
1000 );
1001
1002 let alix2_bo_dm = alix2.find_or_create_dm(bo.inbox_id(), None).await?;
1003 assert_ne!(alix_bo_dm.group_id, alix2_bo_dm.group_id);
1004 let mut msgs = alix2_bo_dm.find_messages(&MsgQueryArgs::default())?;
1005 assert_eq!(msgs.len(), 2);
1006 assert!(
1007 msgs.iter()
1008 .any(|m| m.decrypted_message_bytes == b"old group")
1009 );
1010
1011 let timestamp2 = alix2
1014 .db()
1015 .find_group(&alix_bo_dm.group_id)??
1016 .last_message_ns?;
1017 assert_eq!(timestamp, timestamp2);
1018
1019 let live_message_id = alix2_bo_dm
1020 .send_message(b"hi bo", Default::default())
1021 .await?;
1022
1023 bo.sync_all_welcomes_and_groups(None).await?;
1024 let bo_alix2_dm = bo.group(&alix2_bo_dm.group_id)?;
1025 assert_eq!(bo_alix2_dm.test_last_message_bytes().await??, b"hi bo");
1026
1027 alix2.sync_all_welcomes_and_groups(None).await?;
1029 let rejoined_original = alix2.group(&alix_bo_dm.group_id)?;
1030 assert!(rejoined_original.is_active()?);
1031 let stitched = alix2_bo_dm.find_messages(&MsgQueryArgs::default())?;
1032 assert_eq!(stitched.len(), 4);
1033 let application_ids: Vec<_> = stitched
1034 .iter()
1035 .filter(|message| message.kind == xmtp_db::group_message::GroupMessageKind::Application)
1036 .map(|message| message.id.clone())
1037 .collect();
1038 assert_eq!(application_ids, vec![archived_message_id, live_message_id]);
1039 assert_eq!(alix2.find_groups(Default::default())?.len(), 1);
1040 let bo_original = bo.group(&alix_bo_dm.group_id)?;
1041 rejoined_original.test_can_talk_with(&bo_original).await?;
1042 bo_original.test_can_talk_with(&rejoined_original).await?;
1043 }
1044
1045 #[xmtp_common::test(unwrap_try = true)]
1047 async fn foreign_archive_preserves_dm_pair_and_history() {
1048 use crate::groups::GroupError;
1049 use xmtp_db::{
1050 consent_record::{ConsentState, ConsentType},
1051 group::{DmIdExt, QueryGroup},
1052 readd_status::ReaddStatus,
1053 };
1054 use xmtp_proto::api::HasStats;
1055
1056 tester!(alix, disable_workers);
1057 tester!(bo, disable_workers);
1058 tester!(charlie, disable_workers);
1059 tester!(dana, disable_workers);
1060 let source = alix.find_or_create_dm(bo.inbox_id(), None).await?;
1061 let alix_id = source
1062 .send_message(b"from alix", Default::default())
1063 .await?;
1064 bo.sync_welcomes().await?;
1065 let bo_group = bo.group(&source.group_id)?;
1066 let bo_id = bo_group
1067 .send_message(b"from bo", Default::default())
1068 .await?;
1069 source.sync().await?;
1070 let archived_dm_id = alix
1071 .db()
1072 .find_group(&source.group_id)?
1073 .expect("source DM")
1074 .dm_id
1075 .expect("DM pair");
1076 assert!(!archived_dm_id.contains(charlie.inbox_id()));
1077
1078 let key = vec![0x38; 32];
1079 let opts = ArchiveOptions {
1080 start_ns: None,
1081 end_ns: None,
1082 elements: vec![BackupElementSelection::Messages],
1083 exclude_disappearing_messages: false,
1084 };
1085 let mut export = vec![];
1086 exporter::export(opts.clone(), alix.db(), &key, &mut export)?;
1087 let reader = Box::pin(BufReader::new(Cursor::new(export)));
1088 let mut importer = ArchiveImporter::load(reader, &key).await?;
1089 insert_importer(&mut importer, &charlie.context).await?;
1090
1091 let stored = charlie
1092 .db()
1093 .find_group(&source.group_id)?
1094 .expect("Restored DM");
1095 assert_eq!(stored.membership_state, GroupMembershipState::Restored);
1096 assert_eq!(stored.dm_id.as_deref(), Some(archived_dm_id.as_str()));
1097 assert_eq!(archived_dm_id.other_inbox_id(charlie.inbox_id()), None);
1098 let restored = charlie.group(&source.group_id)?;
1099 assert_eq!(
1100 restored
1101 .metadata()
1102 .await?
1103 .dm_members
1104 .expect("stub pair")
1105 .to_string(),
1106 archived_dm_id
1107 );
1108 assert!(!restored.is_active()?);
1109 let stats = charlie.context.api().api_client.mls_stats();
1110 stats.clear();
1111 assert!(matches!(
1112 restored.send_message(b"inactive", Default::default()).await,
1113 Err(GroupError::GroupInactive)
1114 ));
1115 assert!(matches!(
1116 restored.sync().await,
1117 Err(GroupError::GroupInactive)
1118 ));
1119 let Err(sync_summary) = restored.sync_with_conn().await else {
1120 panic!("Restored sync_with_conn must reject the request");
1121 };
1122 assert!(matches!(
1123 sync_summary.other.as_deref(),
1124 Some(GroupError::GroupInactive)
1125 ));
1126 assert!(matches!(
1127 restored.receive().await,
1128 Err(GroupError::GroupInactive)
1129 ));
1130 assert!(matches!(
1131 charlie.client.sync_all_groups(vec![restored.clone()]).await,
1132 Err(GroupError::GroupInactive)
1133 ));
1134 assert_eq!(stats.publish.get_count(), 0);
1135 let joined = charlie.create_group(None, None)?;
1136 joined.add_members(&[dana.inbox_id()]).await?;
1137 dana.sync_welcomes().await?;
1138 let dana_group = dana.group(&joined.group_id)?;
1139 let incoming_id = dana_group
1140 .send_message(b"joined group", Default::default())
1141 .await?;
1142 let Err(GroupError::Sync(summary)) = charlie
1143 .client
1144 .sync_all_groups(vec![restored.clone(), joined])
1145 .await
1146 else {
1147 panic!("mixed explicit sync must report the inactive group");
1148 };
1149 assert!(matches!(
1150 summary.other.as_deref(),
1151 Some(GroupError::GroupInactive)
1152 ));
1153 assert!(charlie.db().get_group_message(&incoming_id)?.is_some());
1154 let sweep = charlie.client.sync_all_welcomes_and_groups(None).await?;
1155 assert_eq!(sweep.num_eligible, 1);
1156 assert_eq!(sweep.num_synced, 1);
1157 assert!(!restored.is_active()?);
1158 let db = charlie.db();
1159 StoredConsentRecord::new(
1161 ConsentType::ConversationId,
1162 ConsentState::Allowed,
1163 hex::encode(source.group_id),
1164 )
1165 .merge_into(&db)?;
1166 let excludes_publish = db
1167 .get_conversation_ids_for_remote_log_publish()?
1168 .iter()
1169 .all(|group| group.id != source.group_id);
1170 let excludes_download = db
1171 .get_conversation_ids_for_remote_log_download()?
1172 .iter()
1173 .all(|group| group.id != source.group_id);
1174 let excludes_fork_check = db
1175 .get_conversation_ids_for_fork_check()?
1176 .iter()
1177 .all(|id| id.as_slice() != source.group_id.as_ref());
1178 db.set_group_commit_log_forked_status(&source.group_id, Some(true))?;
1179 let excludes_requesting_readds = db
1180 .get_conversation_ids_for_requesting_readds()?
1181 .iter()
1182 .all(|group| group.group_id != source.group_id);
1183 ReaddStatus {
1184 group_id: source.group_id,
1185 installation_id: vec![0x42; 32],
1186 requested_at_sequence_id: Some(1),
1187 responded_at_sequence_id: None,
1188 }
1189 .store(&db)?;
1190 let excludes_responding_readds = db
1191 .get_conversation_ids_for_responding_readds()?
1192 .iter()
1193 .all(|group| group.group_id != source.group_id);
1194 assert_eq!(
1195 [
1196 excludes_publish,
1197 excludes_download,
1198 excludes_fork_check,
1199 excludes_requesting_readds,
1200 excludes_responding_readds,
1201 ],
1202 [true; 5],
1203 "Restored DM must be absent from every recovery selector"
1204 );
1205 for (id, sender, bytes) in [
1206 (&alix_id, alix.inbox_id(), b"from alix".as_slice()),
1207 (&bo_id, bo.inbox_id(), b"from bo".as_slice()),
1208 ] {
1209 let message = charlie.db().get_group_message(id)?.expect("pair history");
1210 assert_eq!(message.sender_inbox_id, sender);
1211 assert_eq!(message.decrypted_message_bytes, bytes);
1212 }
1213
1214 let mut reexport = vec![];
1215 exporter::export(opts, charlie.db(), &key, &mut reexport)?;
1216 let reader = Box::pin(BufReader::new(Cursor::new(reexport)));
1217 let mut exported = ArchiveImporter::load(reader, &key).await?;
1218 while let Some(element) = exported.next().await {
1219 match element?.element {
1220 Some(Element::Group(group)) => assert_ne!(group.id, source.group_id.as_ref()),
1221 Some(Element::GroupMessage(message)) => {
1222 assert_ne!(message.group_id, source.group_id.as_ref());
1223 }
1224 _ => {}
1225 }
1226 }
1227
1228 let reopened = crate::builder::ClientBuilder::from_client(charlie.client.clone())
1229 .with_disable_workers(true)
1230 .build()
1231 .await?;
1232 let reopened_group = reopened.group(&source.group_id)?;
1233 assert_eq!(
1234 reopened_group.dm_id.as_deref(),
1235 Some(archived_dm_id.as_str())
1236 );
1237 assert!(!reopened_group.is_active()?);
1238 assert!(reopened.db().get_group_message(&alix_id)?.is_some());
1239 assert!(reopened.db().get_group_message(&bo_id)?.is_some());
1240 }
1241
1242 #[xmtp_common::test(unwrap_try = true)]
1244 async fn archive_import_leaves_a_known_message_unchanged() {
1245 tester!(alix, disable_workers);
1246 let group = alix.create_group(None, None)?;
1247 let known_id = group.send_message(b"known", Default::default()).await?;
1248 let known = alix
1249 .db()
1250 .get_group_message(&known_id)?
1251 .expect("known message");
1252 let mut save: xmtp_proto::xmtp::device_sync::message_backup::GroupMessageSave =
1253 known.clone().into();
1254 save.sender_inbox_id = hex::encode([0x43; 32]);
1255 save.decrypted_message_bytes = b"ignored replacement".to_vec();
1256 let mut duplicate = futures::stream::iter([Ok::<_, std::io::Error>(BackupElement {
1257 element: Some(Element::GroupMessage(save)),
1258 })]);
1259 insert_elements(&mut duplicate, &alix.context, Default::default()).await?;
1260 assert_eq!(alix.db().get_group_message(&known_id)?, Some(known));
1261 }
1262
1263 #[xmtp_common::test(unwrap_try = true)]
1265 async fn imported_message_keeps_expiry_and_exclusion() {
1266 tester!(alix, disable_workers);
1267 tester!(bo, disable_workers);
1268 let source = alix.create_group(
1269 None,
1270 Some(GroupMetadataOptions {
1271 message_disappearing_settings: Some(MessageDisappearingSettings::new(
1272 1,
1273 xmtp_common::NS_IN_HOUR,
1274 )),
1275 ..Default::default()
1276 }),
1277 )?;
1278 let message_id = source
1279 .send_message(b"disappearing archive content", Default::default())
1280 .await?;
1281 let source_message = alix
1282 .db()
1283 .get_group_message(&message_id)?
1284 .expect("sent message");
1285 assert!(
1286 source_message
1287 .expire_at_ns
1288 .is_some_and(|deadline| deadline > xmtp_common::time::now_ns()),
1289 "the source message must still be eligible for export"
1290 );
1291
1292 source
1293 .remove_conversation_message_disappearing_settings()
1294 .await?;
1295 let permanent_id = source
1296 .send_message(b"permanent archive content", Default::default())
1297 .await?;
1298 let permanent = alix
1299 .db()
1300 .get_group_message(&permanent_id)?
1301 .expect("permanent source");
1302 assert_eq!(
1303 (permanent.expire_at_ns, permanent.expiry_unknown),
1304 (None, false)
1305 );
1306
1307 let key = [0x4a; 32];
1308 let options = ArchiveOptions {
1309 elements: vec![BackupElementSelection::Messages],
1310 start_ns: None,
1311 end_ns: None,
1312 exclude_disappearing_messages: false,
1313 };
1314 let mut first_archive = Vec::new();
1315 exporter::export(options.clone(), alix.db(), &key, &mut first_archive)?;
1316 let reader = Box::pin(BufReader::new(Cursor::new(first_archive)));
1317 let mut importer = ArchiveImporter::load(reader, &key).await?;
1318 insert_importer(&mut importer, &bo.context).await?;
1319
1320 let imported = bo
1321 .db()
1322 .get_group_message(&message_id)?
1323 .expect("imported message");
1324 assert_eq!(
1325 imported.decrypted_message_bytes,
1326 source_message.decrypted_message_bytes
1327 );
1328 assert_eq!(imported.expire_at_ns, source_message.expire_at_ns);
1329 assert!(!imported.expiry_unknown);
1330 let permanent_copy = bo
1331 .db()
1332 .get_group_message(&permanent_id)?
1333 .expect("permanent copy");
1334 assert_eq!(
1335 (permanent_copy.expire_at_ns, permanent_copy.expiry_unknown),
1336 (None, false)
1337 );
1338 assert_eq!(
1339 bo.db()
1340 .find_group(&source.group_id)?
1341 .expect("imported group")
1342 .membership_state,
1343 GroupMembershipState::Restored
1344 );
1345
1346 source.add_members(&[bo.inbox_id()]).await?;
1347 bo.sync_welcomes().await?;
1348 assert!(bo.group(&source.group_id)?.is_active()?);
1349
1350 let unknown_id = vec![0x5b; 32];
1352 let mut old =
1353 xmtp_proto::xmtp::device_sync::message_backup::GroupMessageSave::from(permanent);
1354 old.id = unknown_id.clone();
1355 old.expiry_known = None;
1356 old.expires_at_ns = source_message.expire_at_ns;
1357 let mut old_elements = futures::stream::iter([Ok::<_, std::io::Error>(BackupElement {
1358 element: Some(Element::GroupMessage(old)),
1359 })]);
1360 insert_elements(&mut old_elements, &bo.context, Default::default()).await?;
1361 let unknown = bo
1362 .db()
1363 .get_group_message(&unknown_id)?
1364 .expect("old archive row");
1365 assert_eq!((unknown.expire_at_ns, unknown.expiry_unknown), (None, true));
1366
1367 let mut excluded = options.clone();
1368 excluded.exclude_disappearing_messages = true;
1369 let mut second_archive = Vec::new();
1370 exporter::export(excluded, bo.db(), &key, &mut second_archive)?;
1371 let reader = Box::pin(BufReader::new(Cursor::new(second_archive)));
1372 let messages: Vec<_> = ArchiveImporter::load(reader, &key)
1373 .await?
1374 .collect::<Vec<_>>()
1375 .await
1376 .into_iter()
1377 .collect::<Result<_, _>>()?;
1378 let saved: Vec<_> = messages
1379 .into_iter()
1380 .filter_map(|element| match element.element {
1381 Some(Element::GroupMessage(saved)) => Some(saved),
1382 _ => None,
1383 })
1384 .collect();
1385 assert_eq!(
1386 saved.len(),
1387 1,
1388 "only known non-disappearing history is exported"
1389 );
1390 assert_eq!(saved[0].id, permanent_id);
1391 assert_eq!(
1392 (saved[0].expiry_known, saved[0].expires_at_ns),
1393 (Some(true), None)
1394 );
1395
1396 let mut default_export = Vec::new();
1397 exporter::export(options, bo.db(), &key, &mut default_export)?;
1398 let reader = Box::pin(BufReader::new(Cursor::new(default_export)));
1399 let exported = ArchiveImporter::load(reader, &key)
1400 .await?
1401 .collect::<Vec<_>>()
1402 .await
1403 .into_iter()
1404 .collect::<Result<Vec<_>, _>>()?;
1405 let saved = exported
1406 .into_iter()
1407 .find_map(|element| match element.element {
1408 Some(Element::GroupMessage(saved)) if saved.id == unknown_id => Some(saved),
1409 _ => None,
1410 })
1411 .expect("default export retains unknown history");
1412 assert_eq!(
1413 (saved.expiry_known, saved.expires_at_ns),
1414 (Some(false), None)
1415 );
1416 }
1417
1418 #[xmtp_common::test(unwrap_try = true)]
1423 async fn failed_import_keeps_archived_activity_of_restored_prefix() {
1424 use xmtp_mls_group_state::group_metadata::DmMembers;
1425 use xmtp_proto::xmtp::device_sync::{
1426 group_backup::GroupSave,
1427 message_backup::{GroupMessageKindSave, GroupMessageSave},
1428 };
1429
1430 tester!(caro, disable_workers);
1431 let pair = DmMembers {
1432 member_one_inbox_id: hex::encode([0x41; 32]),
1433 member_two_inbox_id: hex::encode([0x42; 32]),
1434 };
1435 let group_id = vec![0x65; 16];
1436 let archived_activity = 1_234_567;
1437 let group = GroupSave {
1438 id: group_id.clone(),
1439 conversation_type: 2,
1440 dm_id: Some(pair.to_string()),
1441 last_message_ns: Some(archived_activity),
1442 ..Default::default()
1443 };
1444 let malformed = GroupMessageSave {
1446 id: vec![0x66; 32],
1447 group_id: group_id.clone(),
1448 kind: GroupMessageKindSave::Unspecified as i32,
1449 sender_inbox_id: pair.member_one_inbox_id.clone(),
1450 ..Default::default()
1451 };
1452
1453 let events = caro.context.events().subscribe(
1454 xmtp_events::EventFilter::new([xmtp_events::EventKind::ArchiveRestored]),
1455 Some(4),
1456 );
1457 let mut elements = futures::stream::iter([
1458 Ok::<_, std::io::Error>(BackupElement {
1459 element: Some(Element::Group(group)),
1460 }),
1461 Ok(BackupElement {
1462 element: Some(Element::GroupMessage(malformed.clone())),
1463 }),
1464 ]);
1465 let error = insert_elements(&mut elements, &caro.context, Default::default())
1466 .await
1467 .expect_err("a malformed element fails the import");
1468 assert!(
1469 matches!(
1470 error,
1471 DeviceSyncError::ProtoConversion(xmtp_proto::ConversionError::Unspecified(
1472 "message_kind"
1473 ))
1474 ),
1475 "unexpected import error: {error:?}"
1476 );
1477
1478 let restored = caro
1479 .db()
1480 .find_group(&GroupId::try_from(group_id.as_slice())?)??;
1481 assert_eq!(restored.membership_state, GroupMembershipState::Restored);
1482 assert_eq!(restored.last_message_ns, Some(archived_activity));
1483 assert!(caro.db().get_group_message(&malformed.id)?.is_none());
1484 assert!(matches!(
1485 events.drain().as_slice(),
1486 [xmtp_events::EventEnvelope {
1487 client: Some(ClientEvent::ArchiveRestored(restored)), ..
1488 }] if !restored.complete
1489 ));
1490 }
1491
1492 #[xmtp_common::test(unwrap_try = true)]
1494 async fn malformed_archived_dm_id_fails_before_group_insert() {
1495 tester!(alix, disable_workers);
1496 let group_id = vec![0x64; 16];
1497 let save = xmtp_proto::xmtp::device_sync::group_backup::GroupSave {
1498 id: group_id.clone(),
1499 conversation_type: 2,
1500 dm_id: Some("dm:malformed".into()),
1501 ..Default::default()
1502 };
1503 let mut elements = futures::stream::iter([Ok::<_, std::io::Error>(BackupElement {
1504 element: Some(Element::Group(save)),
1505 })]);
1506 assert!(
1507 insert_elements(&mut elements, &alix.context, Default::default())
1508 .await
1509 .is_err()
1510 );
1511 assert!(
1512 alix.db()
1513 .find_group(&GroupId::try_from(group_id.as_slice())?)?
1514 .is_none()
1515 );
1516 }
1517
1518 #[rstest::rstest]
1520 #[xmtp_common::test]
1521 async fn test_buffer_export_import() {
1522 use futures::io::BufReader;
1523 use futures_util::AsyncReadExt;
1524
1525 tester!(alix);
1526 tester!(bo);
1527
1528 let alix_group = alix.create_group(None, None).unwrap();
1529 alix_group.add_members(&[bo.inbox_id()]).await.unwrap();
1530 alix_group
1531 .send_message(b"hello there", SendMessageOpts::default())
1532 .await
1533 .unwrap();
1534
1535 let opts = ArchiveOptions {
1536 start_ns: None,
1537 end_ns: None,
1538 elements: vec![
1539 BackupElementSelection::Messages,
1540 BackupElementSelection::Consent,
1541 ],
1542 exclude_disappearing_messages: false,
1543 };
1544
1545 let key = vec![7; 32];
1546
1547 let file = {
1548 let mut file = Vec::new();
1549 exporter::export(opts, alix.db(), &key, &mut file).unwrap();
1550 file
1551 };
1552
1553 let alix2_wallet = generate_local_wallet();
1554 let alix2 = ClientBuilder::new_test_client(&alix2_wallet).await;
1555 let events = alix2.context.events().subscribe(
1556 xmtp_events::EventFilter::new([xmtp_events::EventKind::ArchiveRestored]),
1557 Some(4),
1558 );
1559
1560 let messages: Vec<StoredGroupMessage> = alix2
1562 .context
1563 .db()
1564 .raw_query(|conn| {
1565 group_messages::table
1566 .select(StoredGroupMessage::as_select())
1567 .load(conn)
1568 })
1569 .unwrap();
1570 assert_eq!(messages.len(), 0);
1571
1572 let reader = BufReader::new(Cursor::new(file.clone()));
1573 let reader = Box::pin(reader);
1574 let mut importer = ArchiveImporter::load(reader, &key).await.unwrap();
1575 insert_importer(&mut importer, &alix2.context)
1576 .await
1577 .unwrap();
1578 assert!(matches!(
1579 events.drain().as_slice(),
1580 [xmtp_events::EventEnvelope {
1581 client: Some(xmtp_events::ClientEvent::ArchiveRestored(restored)), ..
1582 }] if restored.complete
1583 ));
1584
1585 let reader = Box::pin(BufReader::new(Cursor::new(file)));
1586 let mut second = ArchiveImporter::load(reader, &key).await.unwrap();
1587 insert_importer(&mut second, &alix2.context).await.unwrap();
1588 assert!(events.drain().is_empty());
1589
1590 let messages: Vec<StoredGroupMessage> = alix2
1592 .context
1593 .db()
1594 .raw_query(|conn| {
1595 group_messages::table
1596 .select(StoredGroupMessage::as_select())
1597 .load(conn)
1598 })
1599 .unwrap();
1600 assert_eq!(messages.len(), 1);
1601 }
1602
1603 #[xmtp_common::test(unwrap_try = true)]
1605 #[cfg(not(target_arch = "wasm32"))]
1606 async fn test_file_backup() {
1607 use crate::{groups::send_message_opts::SendMessageOpts, tester};
1608 use diesel::QueryDsl;
1609 use xmtp_db::group::{ConversationType, GroupQueryArgs};
1610
1611 tester!(alix, sync_worker, triggers);
1612 tester!(bo);
1613
1614 let alix_group = alix.create_group(None, None)?;
1615
1616 wait_for_min_intents(&alix.context.db(), 2).await?;
1618
1619 alix_group.add_members(&[bo.inbox_id()]).await?;
1620 alix_group.update_group_name("My group".to_string()).await?;
1621
1622 bo.sync_welcomes().await?;
1623 let bo_group = bo.group(&alix_group.group_id)?;
1624
1625 wait_for_min_intents(&alix.context.db(), 1).await?;
1627
1628 alix_group
1629 .send_message(b"hello there", SendMessageOpts::default())
1630 .await?;
1631
1632 wait_for_min_intents(&alix.context.db(), 4).await?;
1635
1636 let mut consent_records: Vec<StoredConsentRecord> = alix
1637 .context
1638 .db()
1639 .raw_query(|conn| consent_records::table.load(conn))?;
1640 assert_eq!(consent_records.len(), 1);
1641 let old_consent_record = consent_records.pop()?;
1642
1643 let mut groups: Vec<StoredGroup> = alix
1644 .context
1645 .db()
1646 .raw_query(|conn| groups::table.load(conn))?;
1647 assert_eq!(groups.len(), 2);
1648 let old_group = groups.pop()?;
1649
1650 let old_messages: Vec<StoredGroupMessage> = alix.context.db().raw_query(|conn| {
1653 group_messages::table
1654 .select(StoredGroupMessage::as_select())
1655 .filter(group_messages::group_id.eq(&alix_group.group_id))
1656 .load(conn)
1657 })?;
1658 assert!(!old_messages.is_empty());
1659
1660 let opts = ArchiveOptions {
1661 start_ns: None,
1662 end_ns: None,
1663 elements: vec![
1664 BackupElementSelection::Messages,
1665 BackupElementSelection::Consent,
1666 ],
1667 exclude_disappearing_messages: false,
1668 };
1669
1670 let key = xmtp_common::rand_vec::<32>();
1671 let path = Path::new("archive.xmtp");
1672 let _ = tokio::fs::remove_file(path).await;
1673 exporter::ArchiveExporter::export_to_file(opts, alix.db(), path, &key).await?;
1674
1675 tester!(alix2, sync_worker);
1676 alix2.device_sync_client().wait_for_sync_worker_init().await;
1677
1678 let consent_records: Vec<StoredConsentRecord> = alix2
1680 .context
1681 .db()
1682 .raw_query(|conn| consent_records::table.load(conn))?;
1683 assert_eq!(consent_records.len(), 0);
1684
1685 let mut importer = ArchiveImporter::from_file(path, &key).await?;
1686 insert_importer(&mut importer, &alix2.context)
1687 .await
1688 .unwrap();
1689
1690 let consent_records: Vec<StoredConsentRecord> = alix2
1692 .context
1693 .db()
1694 .raw_query(|conn| consent_records::table.load(conn))?;
1695 assert_eq!(consent_records.len(), 1);
1696 assert_eq!(
1698 (consent_records[0].state, consent_records[0].consented_at_ns),
1699 (old_consent_record.state, old_consent_record.consented_at_ns)
1700 );
1701
1702 let groups: Vec<StoredGroup> = alix2.context.db().raw_query(|conn| {
1703 groups::table
1704 .filter(groups::conversation_type.ne_all(ConversationType::virtual_types()))
1705 .load(conn)
1706 })?;
1707 assert_eq!(groups.len(), 1);
1708 assert_eq!(groups[0].id, old_group.id);
1710
1711 let messages: Vec<StoredGroupMessage> = alix2.context.db().raw_query(|conn| {
1712 group_messages::table
1713 .select(StoredGroupMessage::as_select())
1714 .filter(group_messages::group_id.eq(&groups[0].id))
1715 .load(conn)
1716 })?;
1717 assert_eq!(messages.len(), 1);
1719 for msg in messages {
1720 let old_msg = old_messages.iter().find(|m| msg.id == m.id)?;
1721 assert_eq!(old_msg.authority_id, msg.authority_id);
1722 assert_eq!(old_msg.decrypted_message_bytes, msg.decrypted_message_bytes);
1723 assert_eq!(old_msg.sent_at_ns, msg.sent_at_ns);
1724 assert_eq!(old_msg.sender_installation_id, msg.sender_installation_id);
1725 assert_eq!(old_msg.sender_inbox_id, msg.sender_inbox_id);
1726 assert_eq!(old_msg.group_id, msg.group_id);
1727 }
1728
1729 let alix2_group = alix2.group(&old_group.id)?;
1730 let _groups = alix2.find_groups(GroupQueryArgs::default())?;
1732 alix2_group.group_name()?;
1734 assert!(!alix2_group.is_active()?);
1735
1736 alix.group(&old_group.id)?
1738 .add_members(&[alix2.inbox_id()])
1739 .await?;
1740 alix2.sync_welcomes().await?;
1741
1742 let alix2_group = alix2.group(&old_group.id)?;
1744 assert!(alix2_group.is_active()?);
1745
1746 let msgs = alix2_group.find_messages(&MsgQueryArgs::default())?;
1748 let old_msg_exists = msgs
1749 .iter()
1750 .any(|msg| msg.decrypted_message_bytes == b"hello there");
1751 assert!(old_msg_exists);
1752
1753 alix2_group
1755 .send_message(b"this should send", SendMessageOpts::default())
1756 .await?;
1757 bo_group.sync().await?;
1758 let msgs = bo_group.find_messages(&MsgQueryArgs::default())?;
1759 let new_msg_exists = msgs
1760 .iter()
1761 .any(|msg| msg.decrypted_message_bytes == b"this should send");
1762 assert!(new_msg_exists);
1763
1764 let _ = tokio::fs::remove_file(path).await;
1766 }
1767
1768 #[xmtp_common::test(unwrap_try = true)]
1769 #[cfg(not(target_arch = "wasm32"))]
1770 async fn test_legacy_archive_import() {
1771 use std::path::PathBuf;
1772
1773 use crate::tester;
1774
1775 let key = vec![0; 32];
1776 let path = PathBuf::from("tests/assets/archive-legacy.xmtp");
1777 let mut importer = ArchiveImporter::from_file(path, &key).await?;
1778
1779 tester!(alix);
1780
1781 let result = insert_importer(&mut importer, &alix.context).await;
1782 assert!(result.is_ok());
1783 }
1784
1785 #[xmtp_common::test(unwrap_try = true)]
1790 #[cfg(not(target_arch = "wasm32"))]
1791 async fn test_import_current_legacy_archive_metadata() {
1792 use std::path::PathBuf;
1793
1794 use crate::groups::group_permissions::PolicySet;
1795 use xmtp_db::group::ConversationType;
1796
1797 let key = vec![9; 32];
1798 let path = PathBuf::from("tests/assets/archive-current-legacy-metadata.xmtp");
1799 let mut preview = ArchiveImporter::from_file(path.clone(), &key).await?;
1800 let mut archived_dm_id = None;
1801 while let Some(element) = preview.next().await {
1802 if let Some(Element::Group(group)) = element?.element {
1803 archived_dm_id = archived_dm_id.or(group.dm_id);
1804 }
1805 }
1806 let archived_dm_id = archived_dm_id.expect("legacy DM pair");
1807 let mut importer = ArchiveImporter::from_file(path, &key).await?;
1808 tester!(alix, disable_workers);
1809 let pair = crate::groups::parse_canonical_dm_id(Some(&archived_dm_id))?;
1810 assert_ne!(pair.member_one_inbox_id, alix.inbox_id());
1811 assert_ne!(pair.member_two_inbox_id, alix.inbox_id());
1812 insert_importer(&mut importer, &alix.context).await?;
1813
1814 let groups: Vec<StoredGroup> = alix
1815 .context
1816 .db()
1817 .raw_query(|conn| groups::table.load(conn))?;
1818 assert_eq!(groups.len(), 2);
1819 for record in &groups {
1820 let group = alix.group(&record.id)?;
1821 group.with_group_snapshot(|mls| {
1822 assert!(mls.extensions().app_data_dictionary().is_some());
1823 Ok(())
1824 })?;
1825 }
1826
1827 let restored_group_record = groups
1828 .iter()
1829 .find(|group| group.conversation_type == ConversationType::Group)
1830 .expect("legacy group archive entry");
1831 assert_eq!(restored_group_record.message_disappear_from_ns, Some(123));
1832 assert_eq!(restored_group_record.message_disappear_in_ns, Some(456));
1833 let restored_group = alix.group(&restored_group_record.id)?;
1834 assert_eq!(restored_group.group_name()?, "legacy archive group name");
1835 assert_eq!(
1836 restored_group.group_description()?,
1837 "legacy archive group description"
1838 );
1839 assert_eq!(
1840 restored_group.group_image_url_square()?,
1841 "https://example.com/legacy-archive.png"
1842 );
1843 assert_eq!(restored_group.app_data()?, "legacy archive app data");
1844 assert_eq!(restored_group.permissions()?.policies, PolicySet::default());
1845
1846 let restored_dm_record = groups
1847 .iter()
1848 .find(|group| group.conversation_type == ConversationType::Dm)
1849 .expect("legacy DM archive entry");
1850 assert_eq!(
1851 restored_dm_record.dm_id.as_deref(),
1852 Some(archived_dm_id.as_str())
1853 );
1854 assert_eq!(restored_dm_record.message_disappear_from_ns, Some(123));
1855 assert_eq!(restored_dm_record.message_disappear_in_ns, Some(456));
1856 let restored_dm = alix.group(&restored_dm_record.id)?;
1857 assert_eq!(restored_dm.group_name()?, "legacy archive dm name");
1858 assert_eq!(
1859 restored_dm.group_description()?,
1860 "legacy archive dm description"
1861 );
1862 assert_eq!(
1863 restored_dm.group_image_url_square()?,
1864 "https://example.com/legacy-dm-archive.png"
1865 );
1866 assert_eq!(restored_dm.app_data()?, "legacy archive dm app data");
1867 }
1868
1869 #[xmtp_common::test(unwrap_try = true)]
1871 async fn test_archive_includes_dictionary_groups() {
1872 tester!(alix, disable_workers);
1873 tester!(bo, disable_workers);
1874
1875 let alix_group = alix
1876 .create_group_with_members(&[bo.inbox_id()], None, None)
1877 .await?;
1878 alix_group.send_message(b"hi", Default::default()).await?;
1879
1880 alix_group
1884 .update_group_name("post-migration name".to_string())
1885 .await?;
1886 alix_group
1887 .update_group_description("post-migration description".to_string())
1888 .await?;
1889 alix_group
1890 .update_group_image_url_square("https://example.com/post-migration.png".to_string())
1891 .await?;
1892
1893 let second_group = alix
1895 .create_group_with_members(&[bo.inbox_id()], None, None)
1896 .await?;
1897 second_group
1898 .update_group_name("second group name".to_string())
1899 .await?;
1900
1901 let key = vec![7; 32];
1902 let opts = ArchiveOptions {
1903 start_ns: None,
1904 end_ns: None,
1905 elements: vec![
1906 BackupElementSelection::Messages,
1907 BackupElementSelection::Consent,
1908 ],
1909 exclude_disappearing_messages: false,
1910 };
1911 let export = {
1912 let mut file = vec![];
1913 exporter::export(opts, alix.db(), &key, &mut file)?;
1914 file
1915 };
1916
1917 tester!(alix2, from: alix);
1920 let reader = Box::pin(BufReader::new(Cursor::new(export)));
1921 let mut importer = ArchiveImporter::load(reader, &key).await?;
1922 insert_importer(&mut importer, &alix2.context).await?;
1923
1924 let restored = alix2.db().find_group(&alix_group.group_id)?;
1925 assert!(
1926 restored.is_some(),
1927 "dictionary group missing from restored archive"
1928 );
1929
1930 let restored_group = alix2.group(&alix_group.group_id)?;
1934 assert_eq!(restored_group.group_name()?, "post-migration name");
1935 assert_eq!(
1936 restored_group.group_description()?,
1937 "post-migration description"
1938 );
1939 assert_eq!(
1940 restored_group.group_image_url_square()?,
1941 "https://example.com/post-migration.png"
1942 );
1943
1944 let restored_second = alix2.group(&second_group.group_id)?;
1945 assert_eq!(restored_second.group_name()?, "second group name");
1946 }
1947}
1948
1949#[cfg(test)]
1950mod deletion_tests;
1951
1952#[cfg(test)]
1953mod pre8_tests;
1954
1955#[cfg(test)]
1956mod restored_tests;
1957
1958#[cfg(all(test, not(target_arch = "wasm32")))]
1959mod migration_tests;