1use crate::crypto::account::OlmAccountState;
107use crate::crypto::cross_signing::CrossSigningState;
108use crate::crypto::device_tracker::{DeviceTracker, StoredDevice};
109use crate::crypto::group_sessions::{GroupDecryptError, GroupSessionManager, RoomEventPlaintext};
110use crate::crypto::olm_sessions::{DecryptedToDevice, OlmDecryptError, OlmSessionManager};
111use crate::error::MessengerError;
112use crate::ids::{DeviceId, EventId, RequestId, RoomId, TxnId, UserId};
113use crate::outgoing_queue::{Jitter, Lane, OutgoingQueue, PendingRequest, ResponseOutcome};
114use crate::persist::{FlushBatch, SealedRecord};
115use crate::room::state::RoomState;
116use crate::room::timeline::{
117 interpret_content, ForwardRefusal, ItemContent, SendState, Timeline, KEY_FORWARDED, KEY_FORWARDED_FROM,
118};
119use crate::room::RoomKind;
120use crate::store::sealed::SealedRecordCodec;
121use crate::store::{CryptoStore, RecordCodec, StateStore, Store};
122use crate::wire::events::{
123 DirectContent, ForwardedRoomKeyContent, InReplyTo, MegolmEncryptedContent, Membership, RawEvent, ReceiptContent,
124 RelatesTo, RoomEncryptedContent, RoomKeyContent, RoomKeyRequestAction, RoomKeyRequestBody, RoomKeyRequestContent,
125 RoomKeyWithheldContent, StrippedStateEvent, TagContent, TagInfo, TextLikeMessageContent, ToDeviceEvent,
126 TypingContent, Unsigned,
127};
128use crate::wire::sync::{parse_sync_response, InvitedRoom, JoinedRoom, LeftRoom};
129use crate::wire::{percent_decode_segment, HttpResponseDescriptor, OutgoingRequest, OutgoingRequestKind};
130use serde::{Deserialize, Serialize};
131use std::collections::{BTreeMap, BTreeSet, VecDeque};
132use zeroize::Zeroizing;
133
134pub const SYNC_TIMEOUT_MS: u64 = 30_000;
137const LOAD_OLDER_PAGE_SIZE: u32 = 50;
140const TYPING_TIMEOUT_MS: u64 = 30_000;
143const UNKNOWN_SENDER_RETRY_CAPACITY: usize = 256;
148const TYPING_DEBOUNCE_MS: i64 = 4_000;
152const ROOM_KEY_SHARE_EVENT_TYPE: &str = "m.room.encrypted";
160
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
165pub struct Counters {
166 pub next_request_id: u64,
168 pub next_txn_id: u64,
173}
174
175fn fresh_txn_seed() -> u64 {
189 std::time::SystemTime::now()
190 .duration_since(std::time::UNIX_EPOCH)
191 .map(|d| d.as_micros() as u64)
192 .unwrap_or(0)
193}
194
195fn restore_counter(persisted_next: u64, max_used_seed: Option<u64>) -> u64 {
196 match max_used_seed {
197 Some(seed) => persisted_next.max(seed.saturating_add(1)),
198 None => persisted_next,
199 }
200}
201
202#[derive(Clone, Debug, PartialEq, Eq)]
206pub struct CoreConfig {
207 pub user_id: UserId,
209 pub device_id: DeviceId,
211 pub server_name: String,
219}
220
221#[derive(Default)]
230pub struct CoreSecrets {
231 pub store_seal_key: Option<Zeroizing<[u8; 32]>>,
239 pub backup_key: Option<Zeroizing<[u8; 32]>>,
243}
244
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248pub enum MessageKind {
249 Text,
251 Notice,
253 Emote,
255}
256
257impl MessageKind {
258 fn msgtype(self) -> &'static str {
259 match self {
260 MessageKind::Text => "m.text",
261 MessageKind::Notice => "m.notice",
262 MessageKind::Emote => "m.emote",
263 }
264 }
265}
266
267#[derive(Clone, Debug, PartialEq, Eq)]
278pub struct OutgoingMessage {
279 pub kind: MessageKind,
281 pub body: String,
284 pub reply_to: Option<EventId>,
286 pub edit_of: Option<EventId>,
289}
290
291#[derive(Clone, Debug, PartialEq, Eq)]
297pub enum CreateRoomKind {
298 Dm {
302 peer: UserId,
304 },
305 Group {
307 name: String,
309 invite: Vec<UserId>,
311 members_can_invite: bool,
315 },
316 Channel {
320 name: String,
322 topic: Option<String>,
324 },
325}
326
327#[derive(Clone, Debug, PartialEq)]
333pub enum MessengerCommand {
334 SendMessage {
340 room_id: RoomId,
342 message: OutgoingMessage,
344 txn_id: Option<TxnId>,
347 },
348 Forward {
374 from_room: RoomId,
376 event_id: EventId,
378 to_room: RoomId,
380 txn_id: Option<TxnId>,
382 },
383 React {
387 room_id: RoomId,
389 target: EventId,
391 key: String,
393 },
394 Redact {
396 room_id: RoomId,
398 target: EventId,
400 reason: Option<String>,
402 },
403 RetrySend {
409 room_id: RoomId,
411 txn_id: TxnId,
413 },
414 CreateRoom {
416 kind: CreateRoomKind,
418 },
419 JoinRoom {
422 room_id: RoomId,
424 },
425 LeaveRoom {
427 room_id: RoomId,
429 },
430 Invite {
432 room_id: RoomId,
434 user_id: UserId,
436 },
437 Kick {
442 room_id: RoomId,
444 user_id: UserId,
446 reason: Option<String>,
448 },
449 SetPowerLevel {
452 room_id: RoomId,
454 user_id: UserId,
456 level: i64,
458 },
459 SetEventsDefault {
463 room_id: RoomId,
465 level: i64,
467 },
468 SetTag {
470 room_id: RoomId,
472 tag: String,
474 order: Option<f64>,
476 },
477 RemoveTag {
479 room_id: RoomId,
481 tag: String,
483 },
484 MarkRead {
487 room_id: RoomId,
489 event_id: EventId,
491 },
492 SetTyping {
495 room_id: RoomId,
497 typing: bool,
499 },
500 RetryDecryption {
505 room_id: RoomId,
507 event_id: EventId,
509 },
510 LoadOlder {
515 room_id: RoomId,
517 },
518 SetAccountData {
524 event_type: String,
526 content: serde_json::Value,
528 },
529 SetRoomAccountData {
538 room_id: RoomId,
540 event_type: String,
542 content: serde_json::Value,
544 },
545 SearchPublicRooms {
550 term: String,
552 },
553 SearchUsers {
558 term: String,
560 },
561}
562
563#[derive(Clone, Debug, PartialEq, Eq)]
568pub enum MessengerEvent {
569 RoomsChanged,
572 TimelineChanged {
574 room_id: RoomId,
576 },
577 TypingChanged {
579 room_id: RoomId,
581 },
582 ReceiptsChanged {
584 room_id: RoomId,
586 },
587 UnreadChanged {
589 room_id: RoomId,
591 },
592 DeviceKeyChanged {
596 user_id: UserId,
598 device_id: DeviceId,
600 },
601 PublicRoomsChanged,
605 UserSearchChanged,
609}
610
611#[derive(Clone)]
616struct PendingDecryptItem {
617 session_id: String,
618 sender: UserId,
619 origin_server_ts: i64,
620 content: MegolmEncryptedContent,
621}
622
623#[derive(Deserialize)]
629struct RoomMessagesResponseBody {
630 #[serde(default)]
631 chunk: Vec<RawEvent>,
632 #[serde(default)]
633 end: Option<String>,
634}
635
636#[derive(Deserialize)]
638struct SendResponseBody {
639 event_id: EventId,
640}
641
642#[derive(Deserialize)]
644struct CreateRoomResponseBody {
645 room_id: RoomId,
646}
647
648#[derive(Clone, Debug, PartialEq)]
651pub struct PublicRoomsResultEntry {
652 pub room_id: RoomId,
654 pub name: Option<String>,
656 pub topic: Option<String>,
658 pub num_joined_members: u64,
660}
661
662#[derive(Deserialize)]
664struct PublicRoomsResponseBody {
665 #[serde(default)]
666 chunk: Vec<PublicRoomsChunkEntry>,
667}
668
669#[derive(Deserialize)]
670struct PublicRoomsChunkEntry {
671 room_id: RoomId,
672 #[serde(default)]
673 name: Option<String>,
674 #[serde(default)]
675 topic: Option<String>,
676 #[serde(default)]
677 num_joined_members: u64,
678}
679
680#[derive(Clone, Debug, PartialEq)]
683pub struct UserDirectoryResultEntry {
684 pub user_id: UserId,
686 pub display_name: Option<String>,
688}
689
690#[derive(Deserialize)]
693struct UserDirectorySearchResponseBody {
694 #[serde(default)]
695 results: Vec<UserDirectorySearchResultEntry>,
696}
697
698#[derive(Deserialize)]
699struct UserDirectorySearchResultEntry {
700 user_id: UserId,
701 #[serde(default)]
702 display_name: Option<String>,
703}
704
705#[derive(Clone)]
710enum SendPayload {
711 Message(OutgoingMessage, Option<serde_json::Map<String, serde_json::Value>>),
715 Event {
719 event_type: String,
721 content: serde_json::Value,
723 },
724 Reaction {
726 target: EventId,
728 key: String,
730 },
731 Redaction {
733 target: EventId,
735 reason: Option<String>,
737 },
738}
739
740struct RoomEventWire {
744 event_type: String,
745 content: serde_json::Value,
746 relates_to: Option<RelatesTo>,
749}
750
751impl SendPayload {
752 fn room_event(&self) -> Option<RoomEventWire> {
755 match self {
756 SendPayload::Message(message, extra) => Some(RoomEventWire {
757 event_type: "m.room.message".to_string(),
758 content: message_wire_content(message, extra.as_ref()),
759 relates_to: message_relates_to(message),
760 }),
761 SendPayload::Event { event_type, content } => {
762 Some(RoomEventWire { event_type: event_type.clone(), content: content.clone(), relates_to: None })
763 }
764 SendPayload::Reaction { .. } | SendPayload::Redaction { .. } => None,
765 }
766 }
767
768 fn is_room_event(&self) -> bool {
769 matches!(self, SendPayload::Message(..) | SendPayload::Event { .. })
770 }
771}
772
773#[derive(Clone)]
779enum WireSend {
780 Event {
782 event_type: String,
785 content: serde_json::Value,
787 },
788 Redact {
790 target: EventId,
792 reason: Option<String>,
794 },
795}
796
797#[derive(Clone)]
802enum SendPhase {
803 Queued,
807 AwaitingKeysQuery,
813 AwaitingKeysClaim {
816 devices: Vec<StoredDevice>,
820 },
821 AwaitingKeyShare {
826 pending: BTreeSet<RequestId>,
828 },
829 AwaitingSend,
831 Failed {
836 errcode: String,
838 },
839}
840
841#[derive(Clone)]
847struct PendingSend {
848 room_id: RoomId,
851 payload: SendPayload,
853 phase: SendPhase,
855 wire: Option<WireSend>,
857}
858
859type AccountDataKey = (Option<RoomId>, String);
862
863struct AccountDataGuard {
875 pending: u32,
877 server_value: Option<serde_json::Value>,
881}
882
883fn account_data_key_from_path(path: &str) -> Option<AccountDataKey> {
886 let segments: Vec<&str> = path.split('/').collect();
887 let n = segments.len();
888 if n < 4 || segments[n - 2] != "account_data" {
889 return None;
890 }
891 let event_type = percent_decode_segment(segments[n - 1])?;
892 if segments[n - 4] == "user" {
893 return Some((None, event_type));
894 }
895 if n >= 6 && segments[n - 4] == "rooms" && segments[n - 6] == "user" {
896 let room_id = RoomId::parse(percent_decode_segment(segments[n - 3])?).ok()?;
897 return Some((Some(room_id), event_type));
898 }
899 None
900}
901
902pub struct MessengerCore<C: RecordCodec> {
904 config: CoreConfig,
905 store: Store<C>,
906 account: OlmAccountState,
907 xsign: Option<CrossSigningState>,
909 xsign_inflight: bool,
910 xsign_attempts: u8,
911 outgoing: OutgoingQueue,
912 counters: Counters,
913 jitter: Box<dyn Jitter>,
914
915 rooms: BTreeMap<RoomId, RoomState>,
916 timelines: BTreeMap<RoomId, Timeline>,
917 direct_account_data: Option<DirectContent>,
918 global_account_data: BTreeMap<String, serde_json::Value>,
919 room_account_data: BTreeMap<RoomId, BTreeMap<String, serde_json::Value>>,
920 typing: BTreeMap<RoomId, Vec<UserId>>,
921 receipts: BTreeMap<RoomId, ReceiptContent>,
922
923 public_rooms_result: Vec<PublicRoomsResultEntry>,
926 latest_public_rooms_request: Option<RequestId>,
931 user_search_result: Vec<UserDirectoryResultEntry>,
934 latest_user_search_request: Option<RequestId>,
937
938 account_data_guard: BTreeMap<AccountDataKey, AccountDataGuard>,
942 account_data_write_keys: BTreeMap<RequestId, AccountDataKey>,
944
945 pending_decrypt: BTreeMap<RoomId, BTreeMap<EventId, PendingDecryptItem>>,
946 key_requests_sent: BTreeSet<(RoomId, String)>,
950 history_claims: BTreeMap<RequestId, Vec<StoredDevice>>,
953 history_claim_tried: BTreeSet<(UserId, DeviceId)>,
955 withheld_sessions: BTreeSet<(RoomId, String)>,
957 key_requests_answered: BTreeSet<(UserId, DeviceId, String)>,
959 unknown_sender_retry: VecDeque<ToDeviceEvent>,
960
961 typing_debounce: BTreeMap<RoomId, i64>,
966 pending_sends: BTreeMap<RoomId, VecDeque<TxnId>>,
972 send_state: BTreeMap<TxnId, PendingSend>,
977 send_request_owner: BTreeMap<RequestId, TxnId>,
981 pending_create_room: BTreeMap<RequestId, CreateRoomKind>,
985
986 pending_kinds: BTreeMap<RequestId, OutgoingRequestKind>,
992 pending_room_messages: BTreeMap<RequestId, RoomId>,
995 sync_request_id: Option<RequestId>,
998 sync_caught_up: bool,
1003 pending_flush_batches: VecDeque<FlushBatch>,
1010
1011 events: Vec<MessengerEvent>,
1012 change_counter: u64,
1013 ingest_error: Option<String>,
1018}
1019
1020impl<C: RecordCodec> MessengerCore<C> {
1021 pub fn sign_scoped(&self, domain: &str, payload: &[u8]) -> Result<(String, String), MessengerError> {
1024 self.account.sign_scoped(domain, payload)
1025 }
1026
1027 pub fn open(
1040 records: impl IntoIterator<Item = SealedRecord>,
1041 codec: C,
1042 config: CoreConfig,
1043 secrets: CoreSecrets,
1044 _now_ms: i64,
1045 jitter: Box<dyn Jitter>,
1046 ) -> Result<Self, MessengerError> {
1047 let _ = secrets;
1051 let mut store = Store::load(records, codec, config.device_id.clone())?;
1052 let mut outgoing = OutgoingQueue::load(&store)?;
1053 let account = OlmAccountState::load_or_create(&mut store)?;
1054
1055 let mut rooms = BTreeMap::new();
1056 let mut timelines = BTreeMap::new();
1057 for room_id in store.room_ids()? {
1058 if let Some(bytes) = store.room_state(&room_id)? {
1059 let state: RoomState = serde_json::from_slice(bytes)
1060 .map_err(|source| MessengerError::Crypto(format!("decode room state for {room_id}: {source}")))?;
1061 rooms.insert(room_id.clone(), state);
1062 timelines.insert(room_id, Timeline::new());
1063 }
1064 }
1065
1066 let pending_requests = store.pending_requests()?;
1067 let max_pending_request_seed = pending_requests.iter().filter_map(|req| req.request.id.as_seed()).max();
1068 outgoing.discard_kind(&mut store, OutgoingRequestKind::Sync)?;
1075 let persisted_counters = store.counters()?.unwrap_or_default();
1076 let mut counters = Counters {
1077 next_request_id: restore_counter(persisted_counters.next_request_id, max_pending_request_seed),
1078 next_txn_id: restore_counter(persisted_counters.next_txn_id, None),
1079 };
1080 if counters != persisted_counters {
1081 store.save_counters(counters)?;
1087 }
1088 counters.next_txn_id = counters.next_txn_id.max(fresh_txn_seed());
1094
1095 let mut core = Self {
1096 config,
1097 store,
1098 account,
1099 outgoing,
1100 counters,
1101 jitter,
1102 rooms,
1103 timelines,
1104 direct_account_data: None,
1105 global_account_data: BTreeMap::new(),
1106 room_account_data: BTreeMap::new(),
1107 typing: BTreeMap::new(),
1108 receipts: BTreeMap::new(),
1109 public_rooms_result: Vec::new(),
1110 latest_public_rooms_request: None,
1111 user_search_result: Vec::new(),
1112 latest_user_search_request: None,
1113 account_data_guard: BTreeMap::new(),
1114 account_data_write_keys: BTreeMap::new(),
1115 pending_decrypt: BTreeMap::new(),
1116 key_requests_sent: BTreeSet::new(),
1117 history_claims: BTreeMap::new(),
1118 history_claim_tried: BTreeSet::new(),
1119 withheld_sessions: BTreeSet::new(),
1120 key_requests_answered: BTreeSet::new(),
1121 unknown_sender_retry: VecDeque::new(),
1122 typing_debounce: BTreeMap::new(),
1123 pending_sends: BTreeMap::new(),
1124 send_state: BTreeMap::new(),
1125 send_request_owner: BTreeMap::new(),
1126 pending_create_room: BTreeMap::new(),
1127 pending_kinds: BTreeMap::new(),
1128 pending_room_messages: BTreeMap::new(),
1129 sync_request_id: None,
1130 sync_caught_up: false,
1131 pending_flush_batches: VecDeque::new(),
1132 events: Vec::new(),
1133 change_counter: 0,
1134 ingest_error: None,
1135 xsign: None,
1136 xsign_inflight: false,
1137 xsign_attempts: 0,
1138 };
1139 core.rebuild_account_data_guard(&pending_requests);
1140 Ok(core)
1141 }
1142
1143 fn rebuild_account_data_guard(&mut self, pending_requests: &[PendingRequest]) {
1151 let mut writes: Vec<&PendingRequest> = pending_requests
1152 .iter()
1153 .filter(|record| matches!(record.request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData))
1154 .collect();
1155 writes.sort_by(|a, b| a.request.id.cmp(&b.request.id));
1156 for record in writes {
1157 let Some(key) = account_data_key_from_path(&record.request.path) else { continue };
1158 let Some(body) = record.request.body.clone() else { continue };
1159 let guard = self.account_data_guard.entry(key.clone()).or_insert(AccountDataGuard { pending: 0, server_value: None });
1160 guard.pending += 1;
1161 self.store_account_data_cache(&key, Some(body));
1162 self.account_data_write_keys.insert(record.request.id.clone(), key);
1163 }
1164 }
1165
1166 fn next_request_id(&mut self) -> Result<RequestId, MessengerError> {
1170 let seed = self.counters.next_request_id;
1171 self.counters.next_request_id = seed.saturating_add(1);
1172 self.store.save_counters(self.counters)?;
1173 Ok(RequestId::next(seed))
1174 }
1175
1176 fn next_txn_id(&mut self) -> Result<TxnId, MessengerError> {
1181 let seed = self.counters.next_txn_id;
1182 self.counters.next_txn_id = seed.saturating_add(1);
1183 self.store.save_counters(self.counters)?;
1184 Ok(TxnId::new(seed))
1185 }
1186
1187 fn enqueue_request(&mut self, request: OutgoingRequest, lane: Lane) -> Result<(), MessengerError> {
1202 let id = request.id.clone();
1203 let kind = request.kind;
1204 if let Some(batch) = self.outgoing.enqueue(&mut self.store, request, lane)? {
1205 self.pending_flush_batches.push_back(batch);
1206 }
1207 self.pending_kinds.insert(id, kind);
1208 Ok(())
1209 }
1210
1211 fn emit(&mut self, event: MessengerEvent) {
1212 self.change_counter = self.change_counter.wrapping_add(1);
1213 self.events.push(event);
1214 }
1215
1216 fn drain_events(&mut self) -> Vec<MessengerEvent> {
1217 std::mem::take(&mut self.events)
1218 }
1219
1220 fn start_new_send(
1236 &mut self,
1237 room_id: RoomId,
1238 payload: SendPayload,
1239 caller_txn_id: Option<TxnId>,
1240 now_ms: i64,
1241 ) -> Result<(), MessengerError> {
1242 let txn_id = match caller_txn_id {
1243 Some(txn_id) => txn_id,
1244 None => self.next_txn_id()?,
1245 };
1246
1247 if let Some(wire) = payload.room_event() {
1248 if self.typing_debounce.remove(&room_id).is_some() {
1249 if let Ok(request_id) = self.next_request_id() {
1250 let request = OutgoingRequest::typing(request_id, &room_id, &self.config.user_id, false, None);
1251 let _ = self.enqueue_request(request, Lane::Other);
1252 }
1253 }
1254 let content = match &payload {
1255 SendPayload::Message(message, _) => message_local_echo_content(message),
1256 _ => interpret_content(&wire.event_type, None, &wire.content),
1257 };
1258 let timeline = self.timelines.entry(room_id.clone()).or_default();
1259 timeline.push_local_echo(
1260 txn_id.clone(),
1261 self.config.user_id.clone(),
1262 now_ms,
1263 &wire.event_type,
1264 content,
1265 wire.content,
1266 );
1267 timeline.mark_send_state(&txn_id, SendState::Sending);
1268 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1269 }
1270
1271 self.send_state.insert(txn_id.clone(), PendingSend { room_id: room_id.clone(), payload, phase: SendPhase::Queued, wire: None });
1272 let queue = self.pending_sends.entry(room_id).or_default();
1273 let was_empty = queue.is_empty();
1274 queue.push_back(txn_id.clone());
1275 if was_empty {
1276 self.start_send_pipeline(&txn_id, now_ms)?;
1277 }
1278 Ok(())
1279 }
1280
1281 fn retry_send(&mut self, room_id: &RoomId, txn_id: &TxnId, now_ms: i64) -> Result<(), MessengerError> {
1286 let Some(pending) = self.send_state.get(txn_id) else { return Ok(()) };
1287 if pending.room_id != *room_id || !matches!(pending.phase, SendPhase::Failed { .. }) {
1288 return Ok(());
1289 }
1290 self.set_send_phase(txn_id, SendPhase::Queued);
1291 let queue = self.pending_sends.entry(room_id.clone()).or_default();
1292 let was_empty = queue.is_empty();
1293 queue.push_back(txn_id.clone());
1294 if was_empty {
1295 self.start_send_pipeline(txn_id, now_ms)?;
1296 }
1297 Ok(())
1298 }
1299
1300 fn set_send_phase(&mut self, txn_id: &TxnId, phase: SendPhase) {
1301 if let Some(entry) = self.send_state.get_mut(txn_id) {
1302 entry.phase = phase;
1303 }
1304 }
1305
1306 fn start_send_pipeline(&mut self, txn_id: &TxnId, now_ms: i64) -> Result<(), MessengerError> {
1315 let Some(pending) = self.send_state.get(txn_id) else { return Ok(()) };
1316 let room_id = pending.room_id.clone();
1317
1318 if pending.wire.is_some() {
1319 return self.begin_awaiting_send(txn_id, &room_id);
1320 }
1321
1322 let is_room_event = pending.payload.is_room_event();
1323 let encrypted_room = self.rooms.get(&room_id).is_some_and(|room| room.encryption.is_some());
1324
1325 if is_room_event && encrypted_room {
1326 self.begin_key_pipeline(txn_id, &room_id, now_ms)
1327 } else {
1328 self.build_plaintext_wire_body(txn_id);
1329 self.begin_awaiting_send(txn_id, &room_id)
1330 }
1331 }
1332
1333 fn encrypted_room_member_ids(&self, room_id: &RoomId) -> BTreeSet<UserId> {
1338 let mut ids: BTreeSet<UserId> = self
1339 .rooms
1340 .get(room_id)
1341 .map(|state| {
1342 state
1343 .members
1344 .iter()
1345 .filter(|(_, member)| matches!(member.membership, Membership::Join | Membership::Invite))
1346 .map(|(user_id, _)| user_id.clone())
1347 .collect()
1348 })
1349 .unwrap_or_default();
1350 ids.insert(self.config.user_id.clone());
1351 ids
1352 }
1353
1354 fn mark_outdated_or_untracked(&mut self, member_ids: &BTreeSet<UserId>) -> Result<Vec<UserId>, MessengerError> {
1360 let tracked: BTreeMap<UserId, bool> = self.store.tracked_users()?.into_iter().collect();
1361 let mut outdated = Vec::new();
1362 let mut newly_tracked = Vec::new();
1363 for user_id in member_ids {
1364 match tracked.get(user_id) {
1365 Some(true) => outdated.push(user_id.clone()),
1366 Some(false) => {}
1367 None => newly_tracked.push(user_id.clone()),
1368 }
1369 }
1370 if !newly_tracked.is_empty() {
1371 DeviceTracker::on_device_lists(&mut self.store, &newly_tracked, &[])?;
1372 outdated.extend(newly_tracked);
1373 }
1374 Ok(outdated)
1375 }
1376
1377 fn known_devices_for(&self, member_ids: &BTreeSet<UserId>) -> Result<Vec<StoredDevice>, MessengerError> {
1380 let mut all = Vec::new();
1381 for user_id in member_ids {
1382 all.extend(DeviceTracker::devices_for_user(&self.store, user_id)?);
1383 }
1384 Ok(all)
1385 }
1386
1387 fn candidate_recipient_devices(&self, member_ids: &BTreeSet<UserId>, known_devices: &[StoredDevice]) -> Vec<StoredDevice> {
1391 known_devices
1392 .iter()
1393 .filter(|device| member_ids.contains(&device.user_id) && !device.blocked)
1394 .filter(|device| !(device.user_id == self.config.user_id && device.device_id == self.config.device_id))
1395 .cloned()
1396 .collect()
1397 }
1398
1399 fn begin_key_pipeline(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1404 let members = self.encrypted_room_member_ids(room_id);
1405 let outdated = self.mark_outdated_or_untracked(&members)?;
1406 if !outdated.is_empty() {
1407 let request_id = self.next_request_id()?;
1408 if let Some(request) = DeviceTracker::keys_query_request(&self.store, request_id.clone())? {
1409 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1410 self.enqueue_request(request, Lane::Other)?;
1411 self.set_send_phase(txn_id, SendPhase::AwaitingKeysQuery);
1412 return Ok(());
1413 }
1414 }
1415 self.begin_keys_claim_or_share(txn_id, room_id, now_ms)
1416 }
1417
1418 fn begin_keys_claim_or_share(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1422 let members = self.encrypted_room_member_ids(room_id);
1423 let known_devices = self.known_devices_for(&members)?;
1424 let candidates = self.candidate_recipient_devices(&members, &known_devices);
1425 let missing: Vec<StoredDevice> =
1426 OlmSessionManager::sessions_missing_for(&self.store, &candidates)?.into_iter().cloned().collect();
1427 if !missing.is_empty() {
1428 let request_id = self.next_request_id()?;
1429 let refs: Vec<&StoredDevice> = missing.iter().collect();
1430 if let Some(request) = OlmSessionManager::keys_claim_request(request_id.clone(), &refs) {
1431 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1432 self.enqueue_request(request, Lane::Other)?;
1433 self.set_send_phase(txn_id, SendPhase::AwaitingKeysClaim { devices: missing });
1434 return Ok(());
1435 }
1436 }
1437 self.begin_share_or_encrypt(txn_id, room_id, now_ms)
1438 }
1439
1440 fn begin_share_or_encrypt(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1453 let Some(encryption) = self.rooms.get(room_id).and_then(|room| room.encryption.clone()) else {
1454 self.build_plaintext_wire_body(txn_id);
1455 return self.begin_awaiting_send(txn_id, room_id);
1456 };
1457 let members = self.encrypted_room_member_ids(room_id);
1458 let known_devices = self.known_devices_for(&members)?;
1459 let update = GroupSessionManager::ensure_outbound_session(
1460 &mut self.store,
1461 room_id,
1462 &self.config.user_id,
1463 &self.config.device_id,
1464 &encryption,
1465 &members,
1466 &known_devices,
1467 now_ms,
1468 )?;
1469 let own_keys = self.account.identity_keys();
1470 GroupSessionManager::adopt_own_outbound_session(
1471 &mut self.store,
1472 room_id,
1473 &self.config.user_id,
1474 own_keys.curve25519,
1475 own_keys.ed25519,
1476 &update,
1477 )?;
1478
1479 if let Some(room_key_content) = update.room_key_content.clone() {
1480 let missing_ids: BTreeSet<(UserId, DeviceId)> = OlmSessionManager::sessions_missing_for(&self.store, &update.new_recipients)?
1481 .into_iter()
1482 .map(|device| (device.user_id.clone(), device.device_id.clone()))
1483 .collect();
1484 let sessioned: Vec<StoredDevice> = update
1485 .new_recipients
1486 .into_iter()
1487 .filter(|device| !missing_ids.contains(&(device.user_id.clone(), device.device_id.clone())))
1488 .collect();
1489
1490 let mut pending_ids = BTreeSet::new();
1491 for chunk in GroupSessionManager::chunk_recipients_for_send_to_device(&sessioned) {
1492 let body = GroupSessionManager::build_room_key_send_to_device_body(
1493 &mut self.store,
1494 &self.account,
1495 &self.config.user_id,
1496 &self.config.device_id,
1497 chunk,
1498 &room_key_content,
1499 )?;
1500 let request_id = self.next_request_id()?;
1501 let share_txn = self.next_txn_id()?;
1502 let request = OutgoingRequest::send_to_device(request_id.clone(), ROOM_KEY_SHARE_EVENT_TYPE, &share_txn, body);
1503 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1504 self.enqueue_request(request, Lane::ToDevice)?;
1505 pending_ids.insert(request_id);
1506 }
1507 if !pending_ids.is_empty() {
1508 self.set_send_phase(txn_id, SendPhase::AwaitingKeyShare { pending: pending_ids });
1509 return Ok(());
1510 }
1511 }
1512
1513 self.finish_encrypt_and_send(txn_id, room_id)?;
1514 self.begin_awaiting_send(txn_id, room_id)
1515 }
1516
1517 fn finish_encrypt_and_send(&mut self, txn_id: &TxnId, room_id: &RoomId) -> Result<(), MessengerError> {
1524 let Some(wire) = self.send_state.get(txn_id).and_then(|pending| pending.payload.room_event()) else {
1525 return Ok(());
1526 };
1527 let sender_curve = self.account.identity_keys().curve25519.to_base64();
1528 let encrypted = GroupSessionManager::encrypt_event(
1529 &mut self.store,
1530 room_id,
1531 &sender_curve,
1532 &self.config.device_id,
1533 &wire.event_type,
1534 wire.content,
1535 wire.relates_to,
1536 )?;
1537 let wire_content = serde_json::to_value(RoomEncryptedContent::Megolm(encrypted))?;
1538 if let Some(entry) = self.send_state.get_mut(txn_id) {
1539 entry.wire = Some(WireSend::Event { event_type: "m.room.encrypted".to_string(), content: wire_content });
1540 }
1541 Ok(())
1542 }
1543
1544 fn build_plaintext_wire_body(&mut self, txn_id: &TxnId) {
1550 let Some(pending) = self.send_state.get(txn_id) else { return };
1551 let wire = match &pending.payload {
1552 SendPayload::Message(..) | SendPayload::Event { .. } => match pending.payload.room_event() {
1553 Some(room_event) => WireSend::Event { event_type: room_event.event_type, content: room_event.content },
1554 None => return,
1555 },
1556 SendPayload::Reaction { target, key } => WireSend::Event {
1557 event_type: "m.reaction".to_string(),
1558 content: serde_json::json!({
1559 "m.relates_to": { "rel_type": "m.annotation", "event_id": target.as_str(), "key": key }
1560 }),
1561 },
1562 SendPayload::Redaction { target, reason } => WireSend::Redact { target: target.clone(), reason: reason.clone() },
1563 };
1564 if let Some(entry) = self.send_state.get_mut(txn_id) {
1565 entry.wire = Some(wire);
1566 }
1567 }
1568
1569 fn begin_awaiting_send(&mut self, txn_id: &TxnId, room_id: &RoomId) -> Result<(), MessengerError> {
1574 let Some(wire) = self.send_state.get(txn_id).and_then(|pending| pending.wire.clone()) else { return Ok(()) };
1575 let request_id = self.next_request_id()?;
1576 let request = match &wire {
1577 WireSend::Event { event_type, content } => {
1578 OutgoingRequest::room_send(request_id.clone(), room_id, event_type, txn_id, content.clone())
1579 }
1580 WireSend::Redact { target, reason } => {
1581 let body = match reason {
1582 Some(reason) => serde_json::json!({ "reason": reason }),
1583 None => serde_json::json!({}),
1584 };
1585 OutgoingRequest::room_redact(request_id.clone(), room_id, target, txn_id, body)
1586 }
1587 };
1588 self.send_request_owner.insert(request_id, txn_id.clone());
1589 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
1590 self.set_send_phase(txn_id, SendPhase::AwaitingSend);
1591 Ok(())
1592 }
1593
1594 fn advance_send_on_success(
1600 &mut self,
1601 txn_id: &TxnId,
1602 request_id: &RequestId,
1603 kind: Option<OutgoingRequestKind>,
1604 response: &HttpResponseDescriptor,
1605 now_ms: i64,
1606 ) {
1607 match kind {
1608 Some(OutgoingRequestKind::KeysQuery) => {
1609 let Some(room_id) = self.send_state.get(txn_id).map(|pending| pending.room_id.clone()) else { return };
1610 let _ = self.begin_keys_claim_or_share(txn_id, &room_id, now_ms);
1611 }
1612 Some(OutgoingRequestKind::KeysClaim) => {
1613 let Some(pending) = self.send_state.get(txn_id) else { return };
1614 let room_id = pending.room_id.clone();
1615 if let SendPhase::AwaitingKeysClaim { devices, .. } = &pending.phase {
1616 let devices = devices.clone();
1617 let refs: Vec<&StoredDevice> = devices.iter().collect();
1618 let _ = OlmSessionManager::on_keys_claim_response(&mut self.store, &self.account, &refs, &response.body);
1619 }
1620 let _ = self.begin_share_or_encrypt(txn_id, &room_id, now_ms);
1621 }
1622 Some(OutgoingRequestKind::SendToDevice) => {
1623 let mut remaining = None;
1624 if let Some(entry) = self.send_state.get_mut(txn_id) {
1625 if let SendPhase::AwaitingKeyShare { pending } = &mut entry.phase {
1626 pending.remove(request_id);
1627 remaining = Some(pending.len());
1628 }
1629 }
1630 if remaining == Some(0) {
1631 let Some(room_id) = self.send_state.get(txn_id).map(|pending| pending.room_id.clone()) else { return };
1632 let _ = self.finish_encrypt_and_send(txn_id, &room_id);
1633 let _ = self.begin_awaiting_send(txn_id, &room_id);
1634 }
1635 }
1636 Some(OutgoingRequestKind::RoomSend) | Some(OutgoingRequestKind::RoomRedact) => {
1637 self.complete_send_success(txn_id, response, now_ms);
1638 }
1639 _ => {}
1640 }
1641 }
1642
1643 fn complete_send_success(&mut self, txn_id: &TxnId, response: &HttpResponseDescriptor, now_ms: i64) {
1649 let Some(pending) = self.send_state.get(txn_id) else { return };
1650 let room_id = pending.room_id.clone();
1651 if pending.payload.is_room_event() {
1652 if let Ok(body) = serde_json::from_slice::<SendResponseBody>(&response.body) {
1653 if let Some(timeline) = self.timelines.get_mut(&room_id) {
1654 timeline.set_echo_event_id(txn_id, body.event_id);
1655 }
1656 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1657 }
1658 }
1659 self.send_state.remove(txn_id);
1660 self.finish_active_send(&room_id, txn_id, now_ms);
1661 }
1662
1663 fn fail_send(&mut self, txn_id: &TxnId, errcode: String, now_ms: i64) {
1669 let Some(pending) = self.send_state.get(txn_id) else { return };
1670 let room_id = pending.room_id.clone();
1671 if pending.payload.is_room_event() {
1672 if let Some(timeline) = self.timelines.get_mut(&room_id) {
1673 timeline.mark_send_state(txn_id, SendState::Failed { reason: errcode.clone() });
1674 }
1675 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1676 }
1677 self.set_send_phase(txn_id, SendPhase::Failed { errcode });
1678 self.finish_active_send(&room_id, txn_id, now_ms);
1679 }
1680
1681 fn finish_active_send(&mut self, room_id: &RoomId, txn_id: &TxnId, now_ms: i64) {
1687 if let Some(queue) = self.pending_sends.get_mut(room_id) {
1688 if queue.front() == Some(txn_id) {
1689 queue.pop_front();
1690 } else {
1691 queue.retain(|id| id != txn_id);
1692 }
1693 }
1694 let next = self.pending_sends.get(room_id).and_then(|queue| queue.front().cloned());
1695 if let Some(next_txn) = next {
1696 let _ = self.start_send_pipeline(&next_txn, now_ms);
1697 }
1698 }
1699
1700 pub fn releasable_requests(&mut self, now_ms: i64) -> Vec<OutgoingRequest> {
1720 self.ensure_sync_enqueued();
1721 self.outgoing.releasable(&self.store, now_ms)
1722 }
1723
1724 fn ensure_sync_enqueued(&mut self) {
1725 if self.sync_request_id.is_some() {
1726 return;
1727 }
1728 let Ok(request_id) = self.next_request_id() else { return };
1729 let since = self.store.sync_token().ok().flatten().map(str::to_string);
1730 let timeout_ms = if since.is_some() && self.sync_caught_up { SYNC_TIMEOUT_MS } else { 0 };
1740 let request = OutgoingRequest::sync(request_id.clone(), since.as_deref(), Some(timeout_ms));
1741 if self.enqueue_request(request, Lane::Sync).is_ok() {
1742 self.sync_request_id = Some(request_id);
1743 }
1744 }
1745
1746 pub fn on_response(&mut self, request_id: RequestId, resp: HttpResponseDescriptor, now_ms: i64) -> Vec<MessengerEvent> {
1752 let kind = self.pending_kinds.get(&request_id).copied();
1753 let room_messages_room = self.pending_room_messages.get(&request_id).cloned();
1754 let create_room_kind = self.pending_create_room.get(&request_id).cloned();
1755 let send_owner = self.send_request_owner.get(&request_id).cloned();
1756
1757 let outcome = self.outgoing.on_response(&mut self.store, &request_id, resp, now_ms, self.jitter.as_mut());
1758 let (is_terminal, terminal_response, failed_errcode) = match outcome {
1759 Ok(Some(ResponseOutcome::Done(response))) => (true, Some(response), None),
1760 Ok(Some(ResponseOutcome::Failed { errcode, .. })) => (true, None, Some(errcode)),
1761 _ => (false, None, None),
1762 };
1763
1764 if is_terminal {
1765 self.pending_kinds.remove(&request_id);
1766 if matches!(kind, Some(OutgoingRequestKind::Sync)) {
1767 self.sync_request_id = None;
1768 if terminal_response.is_some() {
1769 self.sync_caught_up = true;
1770 }
1771 }
1772 if room_messages_room.is_some() {
1773 self.pending_room_messages.remove(&request_id);
1774 }
1775 self.pending_create_room.remove(&request_id);
1776 self.send_request_owner.remove(&request_id);
1777 if let Some(key) = self.account_data_write_keys.remove(&request_id) {
1778 self.settle_account_data_write(&key, terminal_response.is_some());
1779 }
1780 }
1781
1782 if let Some(response) = &terminal_response {
1783 if let Err(error) = self.handle_terminal_success(&request_id, kind, room_messages_room, create_room_kind, response) {
1790 self.ingest_error = Some(match kind {
1791 Some(kind) => format!("{kind:?} response not ingested: {error}"),
1792 None => format!("response not ingested: {error}"),
1793 });
1794 }
1795 }
1796
1797 if is_terminal
1798 && terminal_response.is_none()
1799 && matches!(kind, Some(OutgoingRequestKind::SigningKeysUpload) | Some(OutgoingRequestKind::SignaturesUpload))
1800 {
1801 self.xsign_inflight = false;
1802 }
1803
1804 if let Some(txn_id) = send_owner {
1805 if let Some(response) = &terminal_response {
1806 self.advance_send_on_success(&txn_id, &request_id, kind, response, now_ms);
1807 } else if let Some(errcode) = failed_errcode.clone() {
1808 self.fail_send(&txn_id, errcode, now_ms);
1809 }
1810 }
1811
1812 if matches!(kind, Some(OutgoingRequestKind::KeysUpload)) {
1816 if let Some(errcode) = failed_errcode {
1817 self.ingest_error = Some(format!("keys/upload failed: {errcode}"));
1818 }
1819 }
1820
1821 self.drain_events()
1822 }
1823
1824 pub fn on_transport_error(&mut self, request_id: &RequestId, now_ms: i64) {
1829 let _ = self.outgoing.on_transport_error(request_id, now_ms, self.jitter.as_mut());
1830 }
1831
1832 fn handle_terminal_success(
1833 &mut self,
1834 request_id: &RequestId,
1835 kind: Option<OutgoingRequestKind>,
1836 room_messages_room: Option<RoomId>,
1837 create_room_kind: Option<CreateRoomKind>,
1838 response: &HttpResponseDescriptor,
1839 ) -> Result<(), MessengerError> {
1840 match kind {
1841 Some(OutgoingRequestKind::Sync) => {
1842 let parsed = parse_sync_response(&response.body)?;
1843 self.ingest_sync(&parsed)?;
1844 }
1845 Some(OutgoingRequestKind::KeysUpload) => {
1846 self.account.on_keys_upload_response(&mut self.store)?;
1847 }
1848 Some(OutgoingRequestKind::KeysClaim) if self.history_claims.contains_key(request_id) => {
1849 if let Some(devices) = self.history_claims.remove(request_id) {
1850 let refs: Vec<&StoredDevice> = devices.iter().collect();
1851 let _ = OlmSessionManager::on_keys_claim_response(&mut self.store, &self.account, &refs, &response.body);
1852 }
1853 self.retry_missing_room_keys();
1854 }
1855 Some(OutgoingRequestKind::SigningKeysUpload) => {
1856 self.xsign_inflight = false;
1857 if let Some(state) = self.xsign.as_mut() {
1858 state.uploaded = true;
1859 let snapshot = state.clone();
1860 snapshot.save(&mut self.store)?;
1861 }
1862 self.maybe_enqueue_cross_signing()?;
1863 }
1864 Some(OutgoingRequestKind::SignaturesUpload) => {
1865 self.xsign_inflight = false;
1866 let failures_empty = serde_json::from_slice::<serde_json::Value>(&response.body)
1867 .ok()
1868 .and_then(|v| v.get("failures").and_then(|f| f.as_object()).map(|f| f.is_empty()))
1869 .unwrap_or(true);
1870 if failures_empty {
1871 if let Some(state) = self.xsign.as_mut() {
1872 state.device_signed = true;
1873 let snapshot = state.clone();
1874 snapshot.save(&mut self.store)?;
1875 }
1876 }
1877 }
1878 Some(OutgoingRequestKind::KeysQuery) => {
1879 let outcome = DeviceTracker::on_keys_query_response(&mut self.store, &response.body)?;
1880 if !outcome.key_changes.is_empty() {
1881 let room_ids: Vec<RoomId> = self.rooms.keys().cloned().collect();
1882 for change in &outcome.key_changes {
1883 let _ = GroupSessionManager::forget_shared_device(
1884 &mut self.store,
1885 &room_ids,
1886 &change.user_id,
1887 &change.device_id,
1888 );
1889 self.emit(MessengerEvent::DeviceKeyChanged {
1890 user_id: change.user_id.clone(),
1891 device_id: change.device_id.clone(),
1892 });
1893 }
1894 }
1895 self.retry_unknown_sender_queue()?;
1896 }
1897 Some(OutgoingRequestKind::RoomMessages) => {
1898 if let Some(room_id) = room_messages_room {
1899 self.ingest_room_messages_response(&room_id, &response.body)?;
1900 }
1901 }
1902 Some(OutgoingRequestKind::CreateRoom) => {
1903 if let Some(kind) = create_room_kind {
1904 self.on_create_room_response(kind, response)?;
1905 }
1906 }
1907 Some(OutgoingRequestKind::PublicRooms) if self.latest_public_rooms_request.as_ref() == Some(request_id) => {
1912 let body: PublicRoomsResponseBody = serde_json::from_slice(&response.body)?;
1913 self.public_rooms_result = body
1914 .chunk
1915 .into_iter()
1916 .map(|entry| PublicRoomsResultEntry {
1917 room_id: entry.room_id,
1918 name: entry.name,
1919 topic: entry.topic,
1920 num_joined_members: entry.num_joined_members,
1921 })
1922 .collect();
1923 self.emit(MessengerEvent::PublicRoomsChanged);
1924 }
1925 Some(OutgoingRequestKind::UserDirectorySearch) if self.latest_user_search_request.as_ref() == Some(request_id) => {
1927 let body: UserDirectorySearchResponseBody = serde_json::from_slice(&response.body)?;
1928 self.user_search_result = body
1929 .results
1930 .into_iter()
1931 .map(|entry| UserDirectoryResultEntry { user_id: entry.user_id, display_name: entry.display_name })
1932 .collect();
1933 self.emit(MessengerEvent::UserSearchChanged);
1934 }
1935 _ => {}
1936 }
1937 Ok(())
1938 }
1939
1940 fn on_create_room_response(&mut self, kind: CreateRoomKind, response: &HttpResponseDescriptor) -> Result<(), MessengerError> {
1946 let CreateRoomKind::Dm { peer } = kind else { return Ok(()) };
1947 let body: CreateRoomResponseBody = serde_json::from_slice(&response.body)?;
1948 self.merge_direct_account_data(body.room_id, peer)
1949 }
1950
1951 fn merge_direct_account_data(&mut self, room_id: RoomId, peer: UserId) -> Result<(), MessengerError> {
1963 let mut direct = self.direct_account_data.clone().unwrap_or_default();
1964 let rooms_for_peer = direct.0.entry(peer).or_default();
1965 if !rooms_for_peer.contains(&room_id) {
1966 rooms_for_peer.push(room_id);
1967 }
1968 let value = serde_json::to_value(&direct)?;
1969 self.write_account_data(None, "m.direct".to_string(), value)
1970 }
1971
1972 fn cached_account_data(&self, key: &AccountDataKey) -> Option<&serde_json::Value> {
1973 match &key.0 {
1974 Some(room_id) => self.room_account_data(room_id, &key.1),
1975 None => self.global_account_data(&key.1),
1976 }
1977 }
1978
1979 fn store_account_data_cache(&mut self, key: &AccountDataKey, value: Option<serde_json::Value>) {
1982 if key.0.is_none() && key.1 == "m.direct" {
1983 self.direct_account_data = value.as_ref().and_then(|v| serde_json::from_value::<DirectContent>(v.clone()).ok());
1984 }
1985 match (&key.0, value) {
1986 (None, Some(v)) => {
1987 self.global_account_data.insert(key.1.clone(), v);
1988 }
1989 (None, None) => {
1990 self.global_account_data.remove(&key.1);
1991 }
1992 (Some(room_id), Some(v)) => {
1993 self.room_account_data.entry(room_id.clone()).or_default().insert(key.1.clone(), v);
1994 }
1995 (Some(room_id), None) => {
1996 if let Some(by_type) = self.room_account_data.get_mut(room_id) {
1997 by_type.remove(&key.1);
1998 }
1999 }
2000 }
2001 }
2002
2003 fn write_account_data(&mut self, room_id: Option<RoomId>, event_type: String, value: serde_json::Value) -> Result<(), MessengerError> {
2009 let request_id = self.next_request_id()?;
2010 let request = match &room_id {
2011 Some(room_id) => OutgoingRequest::room_account_data(request_id.clone(), &self.config.user_id, room_id, &event_type, value.clone()),
2012 None => OutgoingRequest::account_data(request_id.clone(), &self.config.user_id, &event_type, value.clone()),
2013 };
2014 let key: AccountDataKey = (room_id, event_type);
2015 let known = self.cached_account_data(&key).cloned();
2016 let guard = self.account_data_guard.entry(key.clone()).or_insert(AccountDataGuard { pending: 0, server_value: known });
2017 guard.pending += 1;
2018 self.store_account_data_cache(&key, Some(value));
2019 match self.enqueue_request(request, Lane::AccountData) {
2020 Ok(()) => {
2021 self.account_data_write_keys.insert(request_id, key);
2022 Ok(())
2023 }
2024 Err(error) => {
2025 self.settle_account_data_write(&key, false);
2026 Err(error)
2027 }
2028 }
2029 }
2030
2031 fn settle_account_data_write(&mut self, key: &AccountDataKey, succeeded: bool) {
2036 let Some(guard) = self.account_data_guard.get_mut(key) else { return };
2037 guard.pending = guard.pending.saturating_sub(1);
2038 if guard.pending > 0 {
2039 return;
2040 }
2041 let Some(guard) = self.account_data_guard.remove(key) else { return };
2042 if !succeeded {
2043 self.store_account_data_cache(key, guard.server_value);
2044 self.emit(MessengerEvent::RoomsChanged);
2045 }
2046 }
2047
2048 fn apply_synced_account_data(&mut self, room_id: Option<&RoomId>, event_type: &str, content: serde_json::Value) {
2052 let key: AccountDataKey = (room_id.cloned(), event_type.to_string());
2053 if let Some(guard) = self.account_data_guard.get_mut(&key) {
2054 guard.server_value = Some(content);
2055 return;
2056 }
2057 self.store_account_data_cache(&key, Some(content));
2058 }
2059
2060 pub fn take_flush_batch(&mut self) -> Option<FlushBatch> {
2063 if let Some(batch) = self.pending_flush_batches.pop_front() {
2064 return Some(batch);
2065 }
2066 self.store.take_flush_batch()
2067 }
2068
2069 pub fn ack_flush(&mut self, id: u64) {
2071 self.store.ack_flush(id);
2072 }
2073
2074 pub fn events(&mut self) -> Vec<MessengerEvent> {
2077 self.drain_events()
2078 }
2079
2080 pub fn take_ingest_error(&mut self) -> Option<String> {
2083 self.ingest_error.take()
2084 }
2085
2086 pub fn change_counter(&self) -> u64 {
2091 self.change_counter
2092 }
2093
2094 pub fn room_ids(&self) -> impl Iterator<Item = &RoomId> {
2096 self.rooms.keys()
2097 }
2098
2099 pub fn room_state(&self, room_id: &RoomId) -> Option<&RoomState> {
2101 self.rooms.get(room_id)
2102 }
2103
2104 pub fn timeline(&self, room_id: &RoomId) -> Option<&Timeline> {
2106 self.timelines.get(room_id)
2107 }
2108
2109 pub fn room_kind(&self, room_id: &RoomId) -> Option<RoomKind> {
2113 self.rooms.get(room_id).map(|state| state.derive_room_kind(room_id, self.direct_account_data.as_ref()))
2114 }
2115
2116 pub fn typing_users(&self, room_id: &RoomId) -> &[UserId] {
2120 self.typing.get(room_id).map(Vec::as_slice).unwrap_or(&[])
2121 }
2122
2123 pub fn receipts(&self, room_id: &RoomId) -> Option<&ReceiptContent> {
2125 self.receipts.get(room_id)
2126 }
2127
2128 pub fn room_account_data(&self, room_id: &RoomId, event_type: &str) -> Option<&serde_json::Value> {
2131 self.room_account_data.get(room_id).and_then(|by_type| by_type.get(event_type))
2132 }
2133
2134 pub fn global_account_data(&self, event_type: &str) -> Option<&serde_json::Value> {
2137 self.global_account_data.get(event_type)
2138 }
2139
2140 pub fn key_request_count(&self) -> usize {
2142 self.key_requests_sent.len()
2143 }
2144
2145 pub fn user_id(&self) -> &UserId {
2150 &self.config.user_id
2151 }
2152
2153 pub fn has_tracked_device(&self, user_id: &UserId) -> bool {
2159 DeviceTracker::devices_for_user(&self.store, user_id).map(|devices| !devices.is_empty()).unwrap_or(false)
2160 }
2161
2162 pub fn public_rooms_result(&self) -> &[PublicRoomsResultEntry] {
2165 &self.public_rooms_result
2166 }
2167
2168 pub fn user_search_result(&self) -> &[UserDirectoryResultEntry] {
2171 &self.user_search_result
2172 }
2173
2174 pub fn send_failure_reason(&self, txn_id: &TxnId) -> Option<&str> {
2179 match self.send_state.get(txn_id).map(|pending| &pending.phase) {
2180 Some(SendPhase::Failed { errcode }) => Some(errcode.as_str()),
2181 _ => None,
2182 }
2183 }
2184
2185 pub fn send_message_with_content(
2193 &mut self,
2194 room_id: RoomId,
2195 message: OutgoingMessage,
2196 extra_content: serde_json::Map<String, serde_json::Value>,
2197 txn_id: Option<TxnId>,
2198 now_ms: i64,
2199 ) -> Result<(), MessengerError> {
2200 self.send_message_inner(room_id, message, Some(extra_content), txn_id, now_ms)
2201 }
2202
2203 fn send_message_inner(
2204 &mut self,
2205 room_id: RoomId,
2206 message: OutgoingMessage,
2207 extra_content: Option<serde_json::Map<String, serde_json::Value>>,
2208 txn_id: Option<TxnId>,
2209 now_ms: i64,
2210 ) -> Result<(), MessengerError> {
2211 if let Some(edit_of) = &message.edit_of {
2212 let owned = self
2213 .timelines
2214 .get(&room_id)
2215 .and_then(|timeline| timeline.item_by_event_id(edit_of))
2216 .is_some_and(|item| item.sender == self.config.user_id);
2217 if !owned {
2218 return Err(MessengerError::EditNotOwned { event_id: edit_of.clone() });
2219 }
2220 }
2221 self.start_new_send(room_id, SendPayload::Message(message, extra_content), txn_id, now_ms)
2222 }
2223
2224 pub fn dispatch(&mut self, cmd: MessengerCommand, now_ms: i64) -> Result<(), MessengerError> {
2231 match cmd {
2232 MessengerCommand::SendMessage { room_id, message, txn_id } => {
2233 self.send_message_inner(room_id, message, None, txn_id, now_ms)?;
2234 }
2235 MessengerCommand::Forward { from_room, event_id, to_room, txn_id } => {
2236 let payload = self.build_forward_payload(&from_room, &event_id, &to_room)?;
2237 self.start_new_send(to_room, payload, txn_id, now_ms)?;
2238 }
2239 MessengerCommand::React { room_id, target, key } => {
2240 self.start_new_send(room_id, SendPayload::Reaction { target, key }, None, now_ms)?;
2241 }
2242 MessengerCommand::Redact { room_id, target, reason } => {
2243 self.start_new_send(room_id, SendPayload::Redaction { target, reason }, None, now_ms)?;
2244 }
2245 MessengerCommand::RetrySend { room_id, txn_id } => {
2246 self.retry_send(&room_id, &txn_id, now_ms)?;
2247 }
2248 MessengerCommand::CreateRoom { kind } => {
2249 self.dispatch_create_room(kind)?;
2250 }
2251 MessengerCommand::JoinRoom { room_id } => {
2252 let request_id = self.next_request_id()?;
2253 let request = OutgoingRequest::join_room(request_id, room_id.as_str());
2254 self.enqueue_request(request, Lane::Other)?;
2255
2256 let dm_peer = self.rooms.get(&room_id).and_then(|room| {
2263 let invited_as_direct = room.members.get(&self.config.user_id).is_some_and(|member| member.is_direct);
2264 if !invited_as_direct {
2265 return None;
2266 }
2267 room.members.keys().find(|user_id| **user_id != self.config.user_id).cloned()
2268 });
2269 if let Some(peer) = dm_peer {
2270 self.merge_direct_account_data(room_id, peer)?;
2271 }
2272 }
2273 MessengerCommand::LeaveRoom { room_id } => {
2274 let request_id = self.next_request_id()?;
2275 let request = OutgoingRequest::leave_room(request_id, &room_id);
2276 self.enqueue_request(request, Lane::Other)?;
2277 }
2278 MessengerCommand::Invite { room_id, user_id } => {
2279 let request_id = self.next_request_id()?;
2280 let request = OutgoingRequest::invite(request_id, &room_id, &user_id);
2281 self.enqueue_request(request, Lane::Other)?;
2282 }
2283 MessengerCommand::Kick { room_id, user_id, reason } => {
2284 let request_id = self.next_request_id()?;
2285 let request = OutgoingRequest::kick(request_id, &room_id, &user_id, reason.as_deref());
2286 self.enqueue_request(request, Lane::Other)?;
2287 }
2288 MessengerCommand::SetPowerLevel { room_id, user_id, level } => {
2289 let mut content = self.room_state(&room_id).map(|st| st.power_levels.clone()).ok_or_else(|| MessengerError::IntentRefused(format!("unknown room {}", room_id.as_str())))?;
2290 content.users.insert(user_id, level);
2291 let body = serde_json::to_value(&content)?;
2292 let request_id = self.next_request_id()?;
2293 let request = OutgoingRequest::set_state(request_id, &room_id, "m.room.power_levels", "", body);
2294 self.enqueue_request(request, Lane::Other)?;
2295 }
2296 MessengerCommand::SetEventsDefault { room_id, level } => {
2297 let mut content = self.room_state(&room_id).map(|st| st.power_levels.clone()).ok_or_else(|| MessengerError::IntentRefused(format!("unknown room {}", room_id.as_str())))?;
2298 content.events_default = level;
2299 let body = serde_json::to_value(&content)?;
2300 let request_id = self.next_request_id()?;
2301 let request = OutgoingRequest::set_state(request_id, &room_id, "m.room.power_levels", "", body);
2302 self.enqueue_request(request, Lane::Other)?;
2303 }
2304 MessengerCommand::SetTag { room_id, tag, order } => {
2305 let mut content = self.current_tag_content(&room_id);
2306 content.tags.insert(tag, TagInfo { order });
2307 self.enqueue_tag_update(room_id, content)?;
2308 }
2309 MessengerCommand::RemoveTag { room_id, tag } => {
2310 let mut content = self.current_tag_content(&room_id);
2311 content.tags.remove(&tag);
2312 self.enqueue_tag_update(room_id, content)?;
2313 }
2314 MessengerCommand::MarkRead { room_id, event_id } => {
2315 let request_id = self.next_request_id()?;
2316 let body = serde_json::json!({ "m.fully_read": event_id.as_str(), "m.read": event_id.as_str() });
2317 let request = OutgoingRequest::read_markers(request_id, &room_id, body);
2318 self.enqueue_request(request, Lane::Other)?;
2319 }
2320 MessengerCommand::SetTyping { room_id, typing } => {
2321 if typing {
2322 let should_send = match self.typing_debounce.get(&room_id) {
2323 Some(&last_sent_ms) => now_ms.saturating_sub(last_sent_ms) >= TYPING_DEBOUNCE_MS,
2324 None => true,
2325 };
2326 if !should_send {
2327 return Ok(());
2328 }
2329 self.typing_debounce.insert(room_id.clone(), now_ms);
2330 } else {
2331 self.typing_debounce.remove(&room_id);
2332 }
2333 let request_id = self.next_request_id()?;
2334 let timeout_ms = typing.then_some(TYPING_TIMEOUT_MS);
2335 let request = OutgoingRequest::typing(request_id, &room_id, &self.config.user_id, typing, timeout_ms);
2336 self.enqueue_request(request, Lane::Other)?;
2337 }
2338 MessengerCommand::RetryDecryption { room_id, event_id } => {
2339 if self.retry_decrypt_one(&room_id, &event_id) {
2340 self.emit(MessengerEvent::TimelineChanged { room_id });
2341 }
2342 }
2343 MessengerCommand::LoadOlder { room_id } => {
2344 let from = self
2345 .timelines
2346 .get(&room_id)
2347 .and_then(|timeline| {
2348 timeline
2349 .older_token()
2350 .map(str::to_string)
2351 .or_else(|| timeline.gap().and_then(|gap| gap.prev_batch.clone()))
2352 })
2353 .or_else(|| {
2359 if self.rooms.contains_key(&room_id) {
2360 self.store.sync_token().ok().flatten().map(str::to_string)
2361 } else {
2362 None
2363 }
2364 });
2365 let Some(from) = from else { return Ok(()) };
2366 let request_id = self.next_request_id()?;
2367 let request =
2368 OutgoingRequest::room_messages(request_id.clone(), &room_id, &from, "b", Some(LOAD_OLDER_PAGE_SIZE));
2369 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
2370 self.pending_room_messages.insert(request_id, room_id);
2371 }
2372 MessengerCommand::SetAccountData { event_type, content } => {
2373 self.write_account_data(None, event_type, content)?;
2374 }
2375 MessengerCommand::SetRoomAccountData { room_id, event_type, content } => {
2376 self.write_account_data(Some(room_id), event_type, content)?;
2377 }
2378 MessengerCommand::SearchPublicRooms { term } => {
2379 let request_id = self.next_request_id()?;
2380 let body = serde_json::json!({ "filter": { "generic_search_term": term }, "limit": 50 });
2381 let request = OutgoingRequest::public_rooms(request_id.clone(), body);
2382 self.latest_public_rooms_request = Some(request_id);
2383 self.enqueue_request(request, Lane::Other)?;
2384 }
2385 MessengerCommand::SearchUsers { term } => {
2386 let request_id = self.next_request_id()?;
2387 let body = serde_json::json!({ "search_term": term, "limit": 20 });
2388 let request = OutgoingRequest::user_directory_search(request_id.clone(), body);
2389 self.latest_user_search_request = Some(request_id);
2390 self.enqueue_request(request, Lane::Other)?;
2391 }
2392 }
2393 Ok(())
2394 }
2395
2396 fn dispatch_create_room(&mut self, kind: CreateRoomKind) -> Result<(), MessengerError> {
2401 let body = match &kind {
2402 CreateRoomKind::Dm { peer } => serde_json::json!({
2403 "is_direct": true,
2404 "invite": [peer.as_str()],
2405 }),
2406 CreateRoomKind::Group { name, invite, members_can_invite } => serde_json::json!({
2407 "visibility": "private",
2408 "is_direct": false,
2409 "name": name,
2410 "invite": invite.iter().map(UserId::as_str).collect::<Vec<_>>(),
2411 "members_can_invite": members_can_invite,
2412 }),
2413 CreateRoomKind::Channel { name, topic } => {
2414 let mut value = serde_json::json!({
2415 "visibility": "public",
2416 "is_direct": false,
2417 "name": name,
2418 });
2419 if let Some(topic) = topic {
2420 value["topic"] = serde_json::Value::String(topic.clone());
2421 }
2422 value
2423 }
2424 };
2425 let request_id = self.next_request_id()?;
2426 let request = OutgoingRequest::create_room(request_id.clone(), body);
2427 self.pending_create_room.insert(request_id.clone(), kind);
2428 self.enqueue_request(request, Lane::Other)?;
2429 Ok(())
2430 }
2431
2432 fn current_tag_content(&self, room_id: &RoomId) -> TagContent {
2437 self.room_account_data(room_id, "m.tag")
2438 .and_then(|value| serde_json::from_value(value.clone()).ok())
2439 .unwrap_or_default()
2440 }
2441
2442 fn enqueue_tag_update(&mut self, room_id: RoomId, content: TagContent) -> Result<(), MessengerError> {
2448 let value = serde_json::to_value(&content)?;
2449 self.write_account_data(Some(room_id), "m.tag".to_string(), value)
2450 }
2451
2452 fn build_forward_payload(
2457 &self,
2458 from_room: &RoomId,
2459 event_id: &EventId,
2460 to_room: &RoomId,
2461 ) -> Result<SendPayload, MessengerError> {
2462 let refuse = |reason: &dyn std::fmt::Display| {
2463 MessengerError::IntentRefused(format!("cannot forward {event_id} from {from_room}: {reason}"))
2464 };
2465 if !self.rooms.contains_key(to_room) {
2466 return Err(refuse(&format!("the target room {to_room} is unknown")));
2467 }
2468 let source = match self.timelines.get(from_room) {
2469 Some(timeline) => timeline.forward_source(event_id),
2470 None => Err(ForwardRefusal::Missing),
2471 }
2472 .map_err(|reason| refuse(&reason))?;
2473 let (marker_key, marker) = self.forward_marker(from_room);
2474 Ok(SendPayload::Event {
2475 event_type: source.event_type,
2476 content: forwardable_content(source.content, marker_key, marker),
2477 })
2478 }
2479
2480 fn forward_marker(&self, from_room: &RoomId) -> (&'static str, serde_json::Value) {
2485 let public_channel = self.room_kind(from_room) == Some(RoomKind::Channel);
2486 if public_channel {
2487 let room_name = self.forward_room_name(from_room);
2488 (KEY_FORWARDED_FROM, serde_json::json!({ "room_id": from_room.as_str(), "room_name": room_name }))
2489 } else {
2490 (KEY_FORWARDED, serde_json::Value::Bool(true))
2491 }
2492 }
2493
2494 fn forward_room_name(&self, room_id: &RoomId) -> String {
2498 if let Some(name) = self.rooms.get(room_id).and_then(|room| room.name.clone()) {
2499 return name;
2500 }
2501 match self.room_kind(room_id) {
2502 Some(RoomKind::Dm) => "Direct",
2503 Some(RoomKind::Channel) => "Channel",
2504 Some(RoomKind::Group) | None => "Group",
2505 }
2506 .to_string()
2507 }
2508
2509 fn retry_decrypt_one(&mut self, room_id: &RoomId, event_id: &EventId) -> bool {
2515 let Some(item) = self.pending_decrypt.get(room_id).and_then(|by_event| by_event.get(event_id)).cloned() else {
2516 return false;
2517 };
2518 match GroupSessionManager::decrypt_event(
2519 &mut self.store,
2520 room_id,
2521 event_id,
2522 item.origin_server_ts,
2523 &item.sender,
2524 &item.content,
2525 ) {
2526 Ok(plaintext) => {
2527 self.apply_decrypted_plaintext(room_id, event_id, &item.sender, item.origin_server_ts, &plaintext);
2528 }
2529 Err(GroupDecryptError::MissingSession { .. }) => {
2530 let _ = self.request_missing_room_key(room_id, &item.sender, &item.content);
2531 return false;
2532 }
2533 Err(_) => return false,
2534 }
2535 if let Some(by_event) = self.pending_decrypt.get_mut(room_id) {
2536 by_event.remove(event_id);
2537 }
2538 true
2539 }
2540
2541 pub fn pull_latest_page(&mut self, room_id: RoomId) -> Result<(), MessengerError> {
2544 let request_id = self.next_request_id()?;
2545 let request = OutgoingRequest::room_messages_latest(request_id.clone(), &room_id, 20);
2546 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
2547 self.pending_room_messages.insert(request_id, room_id);
2548 Ok(())
2549 }
2550
2551 pub fn decrypt_loaded_timeline(&mut self) {
2555 let pending: Vec<(RoomId, Vec<EventId>)> = self
2556 .pending_decrypt
2557 .iter()
2558 .map(|(room_id, by_event)| (room_id.clone(), by_event.keys().cloned().collect()))
2559 .collect();
2560 for (room_id, event_ids) in pending {
2561 for event_id in event_ids {
2562 self.retry_decrypt_one(&room_id, &event_id);
2563 }
2564 }
2565 let rooms: Vec<RoomId> = self.timelines.keys().cloned().collect();
2566 for room_id in rooms {
2567 let sealed: Vec<(EventId, i64, UserId, MegolmEncryptedContent)> = {
2568 let Some(timeline) = self.timelines.get(&room_id) else {
2569 continue;
2570 };
2571 timeline
2572 .items()
2573 .iter()
2574 .filter_map(|item| {
2575 let event_id = item.event_id.clone()?;
2576 let (session_id, ciphertext) = match &item.content {
2577 ItemContent::Encrypted { session_id, ciphertext_b64 } => {
2578 (session_id.clone(), ciphertext_b64.clone())
2579 }
2580 _ => return None,
2581 };
2582 Some((
2583 event_id,
2584 item.origin_server_ts,
2585 item.sender.clone(),
2586 MegolmEncryptedContent {
2587 ciphertext,
2588 session_id,
2589 sender_key: None,
2590 device_id: None,
2591 relates_to: None,
2592 },
2593 ))
2594 })
2595 .collect()
2596 };
2597 for (event_id, origin_server_ts, sender, content) in sealed {
2598 let plaintext = match GroupSessionManager::decrypt_event(
2599 &mut self.store,
2600 &room_id,
2601 &event_id,
2602 origin_server_ts,
2603 &sender,
2604 &content,
2605 ) {
2606 Ok(plaintext) => plaintext,
2607 Err(GroupDecryptError::MissingSession { .. }) => {
2608 let _ = self.request_missing_room_key(&room_id, &sender, &content);
2609 continue;
2610 }
2611 Err(_) => continue,
2612 };
2613 self.apply_decrypted_plaintext(
2614 &room_id,
2615 &event_id,
2616 &sender,
2617 origin_server_ts,
2618 &plaintext,
2619 );
2620 }
2621 }
2622 }
2623
2624 fn apply_decrypted_plaintext(
2637 &mut self,
2638 room_id: &RoomId,
2639 event_id: &EventId,
2640 sender: &UserId,
2641 origin_server_ts: i64,
2642 plaintext: &RoomEventPlaintext,
2643 ) {
2644 let Some(timeline) = self.timelines.get_mut(room_id) else { return };
2645 if let Some(relation) = RelatesTo::from_content(&plaintext.content) {
2646 if matches!(relation, RelatesTo::Annotation { .. } | RelatesTo::Replace { .. }) {
2647 let new_content = plaintext.content.get("m.new_content").cloned();
2648 timeline.apply_decrypted_relation(event_id, sender.clone(), origin_server_ts, relation, new_content);
2649 return;
2650 }
2651 }
2652 timeline.set_decrypted_event(event_id, &plaintext.event_type, &plaintext.content);
2653 }
2654
2655 fn retry_pending_for_session(&mut self, room_id: &RoomId, session_id: &str) {
2656 let Some(by_event) = self.pending_decrypt.get(room_id) else { return };
2657 let candidates: Vec<EventId> =
2658 by_event.iter().filter(|(_, item)| item.session_id == session_id).map(|(event_id, _)| event_id.clone()).collect();
2659 let mut changed = false;
2660 for event_id in candidates {
2661 if self.retry_decrypt_one(room_id, &event_id) {
2662 changed = true;
2663 }
2664 }
2665 if changed {
2666 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
2667 }
2668 }
2669
2670 pub fn republish_device_keys(&mut self) {
2673 self.account.forget_published_device_keys();
2674 }
2675
2676 fn ingest_sync(&mut self, response: &crate::wire::sync::SyncResponse) -> Result<(), MessengerError> {
2677 let mut newly_received_sessions: Vec<(RoomId, String)> = Vec::new();
2678
2679 for event in &response.to_device.events {
2681 self.process_to_device_event(event, &mut newly_received_sessions)?;
2682 }
2683
2684 DeviceTracker::on_device_lists(&mut self.store, &response.device_lists.changed, &response.device_lists.left)?;
2686
2687 let published_otk_count = response.device_one_time_keys_count.get("signed_curve25519").copied().unwrap_or(0);
2689 self.account.on_sync_counts(&mut self.store, published_otk_count, &response.device_unused_fallback_key_types)?;
2690 let keys_upload_request_id = self.next_request_id()?;
2691 if let Some(request) =
2692 self.account.keys_upload_request(keys_upload_request_id, &self.config.user_id, &self.config.device_id)?
2693 {
2694 self.enqueue_request(request, Lane::Other)?;
2695 }
2696 self.maybe_enqueue_cross_signing()?;
2697
2698 let outdated_before_rooms = self.outdated_tracked_users()?;
2700 if !outdated_before_rooms.is_empty() {
2701 self.enqueue_keys_query()?;
2702 }
2703
2704 let rooms_touched =
2706 !response.rooms.join.is_empty() || !response.rooms.invite.is_empty() || !response.rooms.leave.is_empty();
2707 for (room_id, joined) in &response.rooms.join {
2708 self.ingest_joined_room(room_id, joined)?;
2709 }
2710 for (room_id, invited) in &response.rooms.invite {
2711 self.ingest_invited_room(room_id, invited)?;
2712 }
2713 for (room_id, left) in &response.rooms.leave {
2714 self.ingest_left_room(room_id, left)?;
2715 }
2716 if rooms_touched {
2717 self.emit(MessengerEvent::RoomsChanged);
2718 }
2719 if !self.outdated_tracked_users()?.is_subset(&outdated_before_rooms) {
2723 self.enqueue_keys_query()?;
2724 }
2725
2726 for event in &response.account_data.events {
2728 self.apply_synced_account_data(None, &event.event_type, event.content.clone());
2729 }
2730
2731 for (room_id, session_id) in &newly_received_sessions {
2732 self.retry_pending_for_session(room_id, session_id);
2733 }
2734
2735 self.store.save_sync_token(response.next_batch.clone())?;
2738 Ok(())
2739 }
2740
2741 fn process_to_device_event(
2742 &mut self,
2743 event: &ToDeviceEvent,
2744 newly_received_sessions: &mut Vec<(RoomId, String)>,
2745 ) -> Result<(), MessengerError> {
2746 if event.event_type != "m.room.encrypted" {
2747 return Ok(());
2748 }
2749 let Ok(RoomEncryptedContent::Olm(content)) = serde_json::from_value::<RoomEncryptedContent>(event.content.clone())
2750 else {
2751 return Ok(());
2752 };
2753 match OlmSessionManager::decrypt_to_device(&mut self.store, &mut self.account, &self.config.user_id, &event.sender, &content) {
2754 Ok(decrypted) => self.route_decrypted_to_device(decrypted, newly_received_sessions),
2755 Err(OlmDecryptError::UnknownSenderDevice { .. }) => {
2756 self.queue_unknown_sender_retry(event.clone());
2757 Ok(())
2758 }
2759 Err(_) => Ok(()),
2766 }
2767 }
2768
2769 fn route_decrypted_to_device(
2770 &mut self,
2771 decrypted: DecryptedToDevice,
2772 newly_received_sessions: &mut Vec<(RoomId, String)>,
2773 ) -> Result<(), MessengerError> {
2774 match decrypted.event_type.as_str() {
2775 "m.room_key" => {
2776 if let Ok(content) = serde_json::from_value::<RoomKeyContent>(decrypted.content.clone()) {
2777 if content.algorithm == "m.megolm.v1.aes-sha2" {
2778 GroupSessionManager::receive_room_key(
2779 &mut self.store,
2780 &decrypted.sender,
2781 decrypted.sender_device_curve25519,
2782 decrypted.sender_ed25519,
2783 &content,
2784 )?;
2785 newly_received_sessions.push((content.room_id, content.session_id));
2786 }
2787 }
2788 }
2789 "m.forwarded_room_key" => {
2790 if let Ok(content) = serde_json::from_value::<ForwardedRoomKeyContent>(decrypted.content.clone()) {
2791 if let Some((user_id, curve, ed)) = self.forward_binding(&decrypted, &content)? {
2792 let stored = GroupSessionManager::receive_forwarded_room_key(
2793 &mut self.store,
2794 &user_id,
2795 curve,
2796 ed,
2797 &content,
2798 )?;
2799 if stored {
2800 newly_received_sessions.push((content.room_id, content.session_id));
2801 }
2802 }
2803 }
2804 }
2805 "m.room_key_request" => {
2806 if let Ok(content) = serde_json::from_value::<RoomKeyRequestContent>(decrypted.content.clone()) {
2807 self.answer_room_key_request(&decrypted, &content)?;
2808 }
2809 }
2810 "m.room_key.withheld" => {
2811 if let Ok(content) = serde_json::from_value::<RoomKeyWithheldContent>(decrypted.content.clone()) {
2812 if let (Some(room_id), Some(session_id)) = (content.room_id, content.session_id) {
2813 self.withheld_sessions.insert((room_id, session_id));
2814 }
2815 }
2816 }
2817 _ => {}
2818 }
2819 Ok(())
2820 }
2821
2822 fn forward_binding(
2828 &self,
2829 decrypted: &DecryptedToDevice,
2830 content: &ForwardedRoomKeyContent,
2831 ) -> Result<Option<(UserId, mail4agent_vodozemac::Curve25519PublicKey, mail4agent_vodozemac::Ed25519PublicKey)>, MessengerError>
2832 {
2833 let members = self.encrypted_room_member_ids(&content.room_id);
2834 let forwarder_is_us = decrypted.sender == self.config.user_id
2837 || (self.history_forwarding_allowed(&content.room_id) && self.room_admins(&content.room_id).contains(&decrypted.sender));
2838 if !forwarder_is_us && !members.contains(&decrypted.sender) {
2839 return Ok(None);
2840 }
2841 for user_id in &members {
2842 for device in DeviceTracker::devices_for_user(&self.store, user_id)? {
2843 if device.blocked || device.curve25519.to_base64() != content.sender_key {
2844 continue;
2845 }
2846 let same_user = decrypted.sender == device.user_id;
2847 if same_user || forwarder_is_us {
2848 return Ok(Some((device.user_id, device.curve25519, device.ed25519)));
2849 }
2850 }
2851 }
2852 if decrypted.sender_device_curve25519.to_base64() == content.sender_key {
2853 return Ok(Some((
2854 decrypted.sender.clone(),
2855 decrypted.sender_device_curve25519,
2856 decrypted.sender_ed25519,
2857 )));
2858 }
2859 Ok(None)
2860 }
2861
2862 fn answer_room_key_request(
2867 &mut self,
2868 decrypted: &DecryptedToDevice,
2869 content: &RoomKeyRequestContent,
2870 ) -> Result<(), MessengerError> {
2871 if matches!(content.action, RoomKeyRequestAction::RequestCancellation) {
2872 self.key_requests_answered.remove(&(
2873 decrypted.sender.clone(),
2874 content.requesting_device_id.clone(),
2875 content.request_id.clone(),
2876 ));
2877 return Ok(());
2878 }
2879 if !matches!(content.action, RoomKeyRequestAction::Request) {
2880 return Ok(());
2881 }
2882 let Some(body) = content.body.as_ref() else { return Ok(()) };
2883 if body.algorithm != "m.megolm.v1.aes-sha2" {
2884 return Ok(());
2885 }
2886 let members = self.encrypted_room_member_ids(&body.room_id);
2887 if !members.contains(&decrypted.sender) {
2888 return Ok(());
2889 }
2890 if decrypted.sender != self.config.user_id && !self.history_forwarding_allowed(&body.room_id) {
2891 return Ok(());
2892 }
2893 if decrypted.sender == self.config.user_id && content.requesting_device_id == self.config.device_id {
2894 return Ok(());
2895 }
2896 let dedup = (decrypted.sender.clone(), content.requesting_device_id.clone(), content.request_id.clone());
2897 if self.key_requests_answered.contains(&dedup) {
2898 return Ok(());
2899 }
2900 let Some(target) = self.requester_device(decrypted, &content.requesting_device_id)? else {
2901 return Ok(());
2902 };
2903 let our_curve = self.account.identity_keys().curve25519.to_base64();
2904 let (inner_type, payload) =
2905 if let Some(forwarded) = GroupSessionManager::forwarded_room_key_content(
2906 &self.store,
2907 &body.room_id,
2908 &body.session_id,
2909 &our_curve,
2910 )? {
2911 ("m.forwarded_room_key", serde_json::to_value(&forwarded)?)
2912 } else if let Some(room_key) =
2913 GroupSessionManager::outbound_room_key_content(&self.store, &body.room_id, &body.session_id)?
2914 {
2915 ("m.room_key", serde_json::to_value(&room_key)?)
2916 } else {
2917 return Ok(());
2918 };
2919 let sent = self.enqueue_olm_to_devices(std::slice::from_ref(&target), inner_type, payload)?;
2920 if sent > 0 {
2921 self.key_requests_answered.insert(dedup);
2922 }
2923 Ok(())
2924 }
2925
2926 fn requester_device(
2930 &self,
2931 decrypted: &DecryptedToDevice,
2932 requesting_device_id: &DeviceId,
2933 ) -> Result<Option<StoredDevice>, MessengerError> {
2934 let devices = DeviceTracker::devices_for_user(&self.store, &decrypted.sender)?;
2935 if let Some(device) = devices.iter().find(|device| &device.device_id == requesting_device_id) {
2936 return Ok((!device.blocked).then(|| device.clone()));
2937 }
2938 let curve = decrypted.sender_device_curve25519.to_base64();
2939 if let Some(device) = devices.iter().find(|device| device.curve25519.to_base64() == curve) {
2940 return Ok((!device.blocked).then(|| device.clone()));
2941 }
2942 Ok(None)
2943 }
2944
2945 fn request_missing_room_key(
2950 &mut self,
2951 room_id: &RoomId,
2952 sender: &UserId,
2953 content: &MegolmEncryptedContent,
2954 ) -> Result<(), MessengerError> {
2955 let key = (room_id.clone(), content.session_id.clone());
2956 if self.key_requests_sent.contains(&key) || self.withheld_sessions.contains(&key) {
2957 return Ok(());
2958 }
2959 let sender_key = match content.sender_key.clone() {
2960 Some(sender_key) => sender_key,
2961 None => {
2962 let devices = DeviceTracker::devices_for_user(&self.store, sender)?;
2963 let found = content
2964 .device_id
2965 .as_ref()
2966 .and_then(|device_id| devices.iter().find(|device| &device.device_id == device_id))
2967 .or_else(|| devices.iter().find(|device| !device.blocked));
2968 match found {
2969 Some(device) => device.curve25519.to_base64(),
2970 None => return Ok(()),
2971 }
2972 }
2973 };
2974 let mut targets = DeviceTracker::devices_for_user(&self.store, sender)?;
2975 targets.extend(DeviceTracker::devices_for_user(&self.store, &self.config.user_id)?);
2976 if self.history_forwarding_allowed(room_id) {
2979 for admin in self.room_admins(room_id) {
2980 if &admin != sender && admin != self.config.user_id {
2981 targets.extend(DeviceTracker::devices_for_user(&self.store, &admin)?);
2982 }
2983 }
2984 }
2985 if targets.is_empty() {
2986 return Ok(());
2987 }
2988 let missing: Vec<StoredDevice> = OlmSessionManager::sessions_missing_for(&self.store, &targets)?
2992 .into_iter()
2993 .filter(|device| {
2994 !device.blocked
2995 && !(device.user_id == self.config.user_id && device.device_id == self.config.device_id)
2996 && !self.history_claim_tried.contains(&(device.user_id.clone(), device.device_id.clone()))
2997 })
2998 .cloned()
2999 .collect();
3000 if !missing.is_empty() {
3001 let claim_id = self.next_request_id()?;
3002 let refs: Vec<&StoredDevice> = missing.iter().collect();
3003 if let Some(request) = OlmSessionManager::keys_claim_request(claim_id.clone(), &refs) {
3004 for device in &missing {
3005 self.history_claim_tried.insert((device.user_id.clone(), device.device_id.clone()));
3006 }
3007 self.history_claims.insert(claim_id, missing);
3008 self.enqueue_request(request, Lane::Other)?;
3009 return Ok(());
3010 }
3011 }
3012 let request_id = self.next_txn_id()?;
3013 let body = RoomKeyRequestContent {
3014 action: RoomKeyRequestAction::Request,
3015 body: Some(RoomKeyRequestBody {
3016 algorithm: "m.megolm.v1.aes-sha2".to_string(),
3017 room_id: room_id.clone(),
3018 session_id: content.session_id.clone(),
3019 sender_key,
3020 }),
3021 request_id: request_id.as_str().to_string(),
3022 requesting_device_id: self.config.device_id.clone(),
3023 };
3024 let sent = self.enqueue_olm_to_devices(&targets, "m.room_key_request", serde_json::to_value(&body)?)?;
3025 if sent > 0 {
3026 self.key_requests_sent.insert(key);
3027 }
3028 Ok(())
3029 }
3030
3031 fn enqueue_olm_to_devices(
3036 &mut self,
3037 devices: &[StoredDevice],
3038 inner_type: &str,
3039 content: serde_json::Value,
3040 ) -> Result<usize, MessengerError> {
3041 let mut unique = Vec::new();
3042 for device in devices {
3043 if device.blocked || (device.user_id == self.config.user_id && device.device_id == self.config.device_id) {
3044 continue;
3045 }
3046 if unique.iter().any(|have: &StoredDevice| have.user_id == device.user_id && have.device_id == device.device_id)
3047 {
3048 continue;
3049 }
3050 unique.push(device.clone());
3051 }
3052 let missing = OlmSessionManager::sessions_missing_for(&self.store, &unique)?;
3053 let missing_ids: BTreeSet<(UserId, DeviceId)> =
3054 missing.into_iter().map(|device| (device.user_id.clone(), device.device_id.clone())).collect();
3055 let ready: Vec<StoredDevice> = unique
3056 .into_iter()
3057 .filter(|device| !missing_ids.contains(&(device.user_id.clone(), device.device_id.clone())))
3058 .collect();
3059 let mut sent = 0;
3060 for chunk in GroupSessionManager::chunk_recipients_for_send_to_device(&ready) {
3061 let body = GroupSessionManager::build_encrypted_to_device_body(
3062 &mut self.store,
3063 &self.account,
3064 &self.config.user_id,
3065 &self.config.device_id,
3066 chunk,
3067 inner_type,
3068 content.clone(),
3069 )?;
3070 let request_id = self.next_request_id()?;
3071 let share_txn = self.next_txn_id()?;
3072 let request = OutgoingRequest::send_to_device(request_id, ROOM_KEY_SHARE_EVENT_TYPE, &share_txn, body);
3073 self.enqueue_request(request, Lane::ToDevice)?;
3074 sent += chunk.len();
3075 }
3076 Ok(sent)
3077 }
3078
3079 fn queue_unknown_sender_retry(&mut self, event: ToDeviceEvent) {
3080 self.unknown_sender_retry.push_back(event);
3081 while self.unknown_sender_retry.len() > UNKNOWN_SENDER_RETRY_CAPACITY {
3082 self.unknown_sender_retry.pop_front();
3083 }
3084 }
3085
3086 fn retry_unknown_sender_queue(&mut self) -> Result<(), MessengerError> {
3087 let pending: Vec<ToDeviceEvent> = std::mem::take(&mut self.unknown_sender_retry).into_iter().collect();
3088 let mut newly_received_sessions = Vec::new();
3089 for event in pending {
3090 self.process_to_device_event(&event, &mut newly_received_sessions)?;
3091 }
3092 for (room_id, session_id) in &newly_received_sessions {
3093 self.retry_pending_for_session(room_id, session_id);
3094 }
3095 Ok(())
3096 }
3097
3098 fn outdated_tracked_users(&self) -> Result<BTreeSet<UserId>, MessengerError> {
3100 Ok(self.store.tracked_users()?.into_iter().filter_map(|(user_id, outdated)| outdated.then_some(user_id)).collect())
3101 }
3102
3103 fn enqueue_keys_query(&mut self) -> Result<(), MessengerError> {
3105 let request_id = self.next_request_id()?;
3106 if let Some(request) = DeviceTracker::keys_query_request(&self.store, request_id)? {
3107 self.enqueue_request(request, Lane::Other)?;
3108 }
3109 Ok(())
3110 }
3111
3112 fn retry_missing_room_keys(&mut self) {
3122 let work: Vec<(RoomId, EventId)> = self
3123 .pending_decrypt
3124 .iter()
3125 .flat_map(|(room, by_event)| by_event.keys().map(move |event| (room.clone(), event.clone())))
3126 .collect();
3127 for (room_id, event_id) in work {
3128 let _ = self.retry_decrypt_one(&room_id, &event_id);
3129 }
3130 }
3131
3132 fn room_admins(&self, room_id: &RoomId) -> BTreeSet<UserId> {
3137 let Some(state) = self.room_state(room_id) else { return BTreeSet::new() };
3138 self.encrypted_room_member_ids(room_id)
3139 .into_iter()
3140 .filter(|u| state.power_levels.users.get(u).copied().unwrap_or(state.power_levels.users_default) >= 100)
3141 .collect()
3142 }
3143
3144 fn history_forwarding_allowed(&self, room_id: &RoomId) -> bool {
3145 !matches!(
3146 self.rooms.get(room_id).and_then(|room| room.history_visibility.clone()),
3147 Some(crate::wire::events::HistoryVisibility::Joined) | Some(crate::wire::events::HistoryVisibility::Invited)
3148 )
3149 }
3150
3151 fn maybe_enqueue_cross_signing(&mut self) -> Result<(), MessengerError> {
3154 if self.xsign_inflight || self.xsign_attempts >= 4 {
3155 return Ok(());
3156 }
3157 if self.xsign.is_none() {
3158 self.xsign = Some(CrossSigningState::load_or_create(&mut self.store)?);
3159 }
3160 let Some(state) = self.xsign.clone() else { return Ok(()) };
3161 let user_id = self.config.user_id.clone();
3162 let request_id;
3163 let request = if !state.uploaded {
3164 request_id = self.next_request_id()?;
3165 OutgoingRequest::signing_keys_upload(request_id, state.upload_body(&user_id)?)
3166 } else if !state.device_signed {
3167 let device_id = self.config.device_id.clone();
3168 let device_keys = self.account.device_keys_json(&user_id, &device_id)?;
3169 request_id = self.next_request_id()?;
3170 OutgoingRequest::signatures_upload(request_id, state.device_signature_body(&user_id, &device_id, device_keys)?)
3171 } else {
3172 return Ok(());
3173 };
3174 self.xsign_attempts += 1;
3175 self.xsign_inflight = true;
3176 self.enqueue_request(request, Lane::Other)
3177 }
3178
3179 fn track_joined_members<'a>(&mut self, room_id: &RoomId, events: impl Iterator<Item = &'a RawEvent>) -> Result<(), MessengerError> {
3180 if self.rooms.get(room_id).is_none_or(|room| room.encryption.is_none()) {
3181 return Ok(());
3182 }
3183 let joined: Vec<UserId> = events
3184 .filter(|event| event.event_type == "m.room.member")
3185 .filter(|event| event.content.get("membership").and_then(serde_json::Value::as_str) == Some("join"))
3186 .filter_map(|event| event.state_key.as_deref().and_then(|state_key| UserId::parse(state_key).ok()))
3187 .filter(|user_id| *user_id != self.config.user_id)
3188 .collect();
3189 if joined.is_empty() {
3190 return Ok(());
3191 }
3192 DeviceTracker::on_device_lists(&mut self.store, &joined, &[])
3193 }
3194
3195 fn ingest_joined_room(&mut self, room_id: &RoomId, joined: &JoinedRoom) -> Result<(), MessengerError> {
3196 for event in joined.state.events.iter().chain(joined.timeline.events.iter()) {
3197 self.rooms.entry(room_id.clone()).or_default().apply_state_event(event)?;
3198 }
3199
3200 self.track_joined_members(room_id, joined.state.events.iter().chain(joined.timeline.events.iter()))?;
3201
3202 let unread_before = self.rooms.get(room_id).map(RoomState::unread_count);
3203 self.rooms.entry(room_id.clone()).or_default().apply_summary(&joined.summary, &joined.unread_notifications);
3204 let unread_after = self.rooms.get(room_id).map(RoomState::unread_count);
3205 if unread_before != unread_after {
3206 self.emit(MessengerEvent::UnreadChanged { room_id: room_id.clone() });
3207 }
3208
3209 for event in &joined.account_data.events {
3210 self.apply_synced_account_data(Some(room_id), &event.event_type, event.content.clone());
3211 }
3212
3213 if let Some(typing_event) = joined.ephemeral.events.iter().find(|event| event.event_type == "m.typing") {
3214 if let Ok(typing) = serde_json::from_value::<TypingContent>(typing_event.content.clone()) {
3215 self.typing.insert(room_id.clone(), typing.user_ids);
3216 self.emit(MessengerEvent::TypingChanged { room_id: room_id.clone() });
3217 }
3218 }
3219 if let Some(receipt_event) = joined.ephemeral.events.iter().find(|event| event.event_type == "m.receipt") {
3220 if let Ok(receipts) = serde_json::from_value::<ReceiptContent>(receipt_event.content.clone()) {
3221 self.receipts.insert(room_id.clone(), receipts);
3222 self.emit(MessengerEvent::ReceiptsChanged { room_id: room_id.clone() });
3223 }
3224 }
3225
3226 if !joined.timeline.events.is_empty() {
3227 self.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(
3228 &joined.timeline.events,
3229 joined.timeline.limited,
3230 joined.timeline.prev_batch.clone(),
3231 );
3232 self.decrypt_new_timeline_events(room_id, &joined.timeline.events);
3233 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3234 }
3235
3236 self.persist_room_state(room_id)
3237 }
3238
3239 fn ingest_invited_room(&mut self, room_id: &RoomId, invited: &InvitedRoom) -> Result<(), MessengerError> {
3240 for stripped in &invited.invite_state.events {
3241 let synthetic = stripped_state_to_raw_event(stripped);
3242 self.rooms.entry(room_id.clone()).or_default().apply_state_event(&synthetic)?;
3243 }
3244 self.persist_room_state(room_id)
3245 }
3246
3247 fn ingest_left_room(&mut self, room_id: &RoomId, left: &LeftRoom) -> Result<(), MessengerError> {
3248 for event in left.state.events.iter().chain(left.timeline.events.iter()) {
3249 self.rooms.entry(room_id.clone()).or_default().apply_state_event(event)?;
3250 }
3251 for event in &left.account_data.events {
3252 self.apply_synced_account_data(Some(room_id), &event.event_type, event.content.clone());
3253 }
3254 if !left.timeline.events.is_empty() {
3255 self.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(
3256 &left.timeline.events,
3257 left.timeline.limited,
3258 left.timeline.prev_batch.clone(),
3259 );
3260 self.decrypt_new_timeline_events(room_id, &left.timeline.events);
3261 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3262 }
3263 self.persist_room_state(room_id)
3264 }
3265
3266 fn persist_room_state(&mut self, room_id: &RoomId) -> Result<(), MessengerError> {
3267 let Some(state) = self.rooms.get(room_id) else { return Ok(()) };
3268 let bytes = serde_json::to_vec(state)
3269 .map_err(|source| MessengerError::Crypto(format!("encode room state for {room_id}: {source}")))?;
3270 self.store.save_room_state(room_id, bytes)?;
3271 Ok(())
3272 }
3273
3274 fn decrypt_new_timeline_events(&mut self, room_id: &RoomId, events: &[RawEvent]) {
3282 for event in events {
3283 if event.event_type != "m.room.encrypted" || event.state_key.is_some() || event.unsigned.redacted_because.is_some() {
3284 continue;
3285 }
3286 let Ok(RoomEncryptedContent::Megolm(content)) = serde_json::from_value::<RoomEncryptedContent>(event.content.clone())
3287 else {
3288 continue;
3289 };
3290 match GroupSessionManager::decrypt_event(
3291 &mut self.store,
3292 room_id,
3293 &event.event_id,
3294 event.origin_server_ts,
3295 &event.sender,
3296 &content,
3297 ) {
3298 Ok(plaintext) => {
3299 self.apply_decrypted_plaintext(room_id, &event.event_id, &event.sender, event.origin_server_ts, &plaintext);
3300 if let Some(by_event) = self.pending_decrypt.get_mut(room_id) {
3301 by_event.remove(&event.event_id);
3302 }
3303 }
3304 Err(err) => {
3305 if matches!(err, GroupDecryptError::MissingSession { .. }) {
3306 let _ = self.request_missing_room_key(room_id, &event.sender, &content);
3307 }
3308 let reason = format!("{:?}", err.utd_reason());
3317 if let Some(timeline) = self.timelines.get_mut(room_id) {
3318 timeline.set_decrypted(&event.event_id, ItemContent::Undecryptable { reason });
3319 }
3320 self.pending_decrypt.entry(room_id.clone()).or_default().insert(
3321 event.event_id.clone(),
3322 PendingDecryptItem {
3323 session_id: content.session_id.clone(),
3324 sender: event.sender.clone(),
3325 origin_server_ts: event.origin_server_ts,
3326 content,
3327 },
3328 );
3329 }
3330 }
3331 }
3332 }
3333
3334 fn ingest_room_messages_response(&mut self, room_id: &RoomId, body: &[u8]) -> Result<(), MessengerError> {
3335 let parsed: RoomMessagesResponseBody = serde_json::from_slice(body)?;
3336 let mut events = parsed.chunk;
3337 events.reverse();
3338 self.timelines.entry(room_id.clone()).or_default().prepend_back_page(&events, parsed.end);
3339 self.decrypt_new_timeline_events(room_id, &events);
3340 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3341 self.persist_room_state(room_id)
3342 }
3343}
3344
3345impl MessengerCore<SealedRecordCodec> {
3346 pub fn open_sealed(
3357 records: impl IntoIterator<Item = SealedRecord>,
3358 secrets: CoreSecrets,
3359 config: CoreConfig,
3360 now_ms: i64,
3361 jitter: Box<dyn Jitter>,
3362 ) -> Result<Self, MessengerError> {
3363 let seal_key = secrets.store_seal_key.clone().ok_or_else(|| {
3364 MessengerError::Crypto(
3365 "MessengerCore::open_sealed requires CoreSecrets::store_seal_key"
3366 .to_string(),
3367 )
3368 })?;
3369 let codec = SealedRecordCodec::new(seal_key);
3370 Self::open(records, codec, config, secrets, now_ms, jitter)
3371 }
3372}
3373
3374fn stripped_placeholder_event_id() -> EventId {
3380 EventId::parse("$stripped-preview").expect("this literal is a well-formed, hardcoded $-prefixed opaque id")
3381}
3382
3383fn stripped_state_to_raw_event(stripped: &StrippedStateEvent) -> RawEvent {
3388 RawEvent {
3389 event_id: stripped_placeholder_event_id(),
3390 event_type: stripped.event_type.clone(),
3391 sender: stripped.sender.clone(),
3392 origin_server_ts: 0,
3393 state_key: Some(stripped.state_key.clone()),
3394 content: stripped.content.clone(),
3395 unsigned: Unsigned::default(),
3396 }
3397}
3398
3399fn forwardable_content(mut content: serde_json::Value, marker_key: &str, marker: serde_json::Value) -> serde_json::Value {
3406 if let Some(object) = content.as_object_mut() {
3407 for key in ["m.relates_to", "m.mentions", "m.new_content", KEY_FORWARDED, KEY_FORWARDED_FROM] {
3408 object.remove(key);
3409 }
3410 object.insert(marker_key.to_string(), marker);
3411 }
3412 content
3413}
3414
3415fn message_local_echo_content(message: &OutgoingMessage) -> ItemContent {
3422 let body = TextLikeMessageContent { body: message.body.clone(), format: None, formatted_body: None };
3423 match message.kind {
3424 MessageKind::Text => ItemContent::Text(body),
3425 MessageKind::Notice => ItemContent::Notice(body),
3426 MessageKind::Emote => ItemContent::Emote(body),
3427 }
3428}
3429
3430fn message_wire_content(message: &OutgoingMessage, extra_content: Option<&serde_json::Map<String, serde_json::Value>>) -> serde_json::Value {
3439 let msgtype = message.kind.msgtype();
3440 let new_content = serde_json::json!({ "msgtype": msgtype, "body": message.body });
3441 let mut content = new_content.clone();
3442 if let Some(edit_of) = &message.edit_of {
3443 content["body"] = serde_json::Value::String(format!("* {}", message.body));
3444 content["m.new_content"] = new_content;
3445 content["m.relates_to"] = serde_json::json!({ "rel_type": "m.replace", "event_id": edit_of.as_str() });
3446 } else if let Some(reply_to) = &message.reply_to {
3447 content["m.relates_to"] = serde_json::json!({ "m.in_reply_to": { "event_id": reply_to.as_str() } });
3448 }
3449 if let (Some(extra), Some(map)) = (extra_content, content.as_object_mut()) {
3450 for (key, value) in extra {
3451 if key.starts_with(EXTRA_CONTENT_PREFIX) && !map.contains_key(key) {
3452 map.insert(key.clone(), value.clone());
3453 }
3454 }
3455 }
3456 content
3457}
3458
3459pub const EXTRA_CONTENT_PREFIX: &str = "fyi.";
3461
3462fn message_relates_to(message: &OutgoingMessage) -> Option<RelatesTo> {
3468 if let Some(edit_of) = &message.edit_of {
3469 Some(RelatesTo::Replace { event_id: edit_of.clone() })
3470 } else {
3471 message.reply_to.clone().map(|event_id| RelatesTo::InReplyTo(InReplyTo { event_id }))
3472 }
3473}
3474
3475#[cfg(test)]
3476mod tests {
3477 use super::*;
3478 use crate::room::timeline::Forwarded;
3479 use crate::store::InsecurePlainCodecForTests;
3480 use crate::wire::HttpMethod;
3481
3482 #[test]
3483 fn extra_content_adds_only_fyi_keys_and_never_overrides_the_message() {
3484 let mut extra = serde_json::Map::new();
3485 extra.insert("fyi.hatchery.interrupt".into(), serde_json::Value::Bool(true));
3486 extra.insert("body".into(), serde_json::json!("hijack"));
3487 extra.insert("m.relates_to".into(), serde_json::json!({}));
3488 let message = OutgoingMessage { kind: MessageKind::Text, body: "hi".into(), reply_to: None, edit_of: None };
3489 let content = message_wire_content(&message, Some(&extra));
3490 assert_eq!(content, serde_json::json!({ "msgtype": "m.text", "body": "hi", "fyi.hatchery.interrupt": true }));
3491 assert_eq!(message_wire_content(&message, None), serde_json::json!({ "msgtype": "m.text", "body": "hi" }));
3492 }
3493
3494 struct FixedJitter(f64);
3495
3496 impl Jitter for FixedJitter {
3497 fn next_unit(&mut self) -> f64 {
3498 self.0
3499 }
3500 }
3501
3502 fn device_id() -> DeviceId {
3503 DeviceId::parse("DEV1").expect("valid device id")
3504 }
3505
3506 fn user_id() -> UserId {
3507 UserId::parse("@alice:example.org").expect("valid user id")
3508 }
3509
3510 fn test_config() -> CoreConfig {
3511 CoreConfig { user_id: user_id(), device_id: device_id(), server_name: "example.org".to_string() }
3512 }
3513
3514 fn open_fresh_core() -> MessengerCore<InsecurePlainCodecForTests> {
3515 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, test_config(), CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3516 .expect("open succeeds on a brand-new device")
3517 }
3518
3519 fn flush_and_ack(core: &mut MessengerCore<InsecurePlainCodecForTests>) {
3527 while let Some(batch) = core.take_flush_batch() {
3528 core.ack_flush(batch.id);
3529 }
3530 }
3531
3532 #[test]
3533 fn counters_survive_restart_and_never_repeat_txn_ids() {
3534 let device = device_id();
3535 let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
3536 store.save_counters(Counters { next_request_id: 3, next_txn_id: 7 }).expect("save");
3537 let batch = store.take_flush_batch().expect("counters dirtied the store");
3538 store.ack_flush(batch.id);
3539
3540 let reloaded = Store::load(batch.records, InsecurePlainCodecForTests, device).expect("reload");
3541 let restored = reloaded.counters().expect("no error").expect("counters were persisted");
3542 assert_eq!(restored.next_txn_id, 7, "a restart resumes the txn-id sequence exactly where it left off");
3543 assert_eq!(restored.next_request_id, 3);
3544
3545 assert_eq!(restore_counter(3, Some(10)), 11, "resumes past the highest pending id, never repeating it");
3550 assert_eq!(restore_counter(3, None), 3, "nothing pending: the persisted value alone is authoritative");
3551 }
3552
3553 #[test]
3554 fn open_restores_the_request_counter_past_a_still_pending_requests_id() {
3555 let device = device_id();
3556 let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
3557 let mut queue = OutgoingQueue::new();
3558 let pending = OutgoingRequest::sync(RequestId::next(5), None, None);
3559 queue.enqueue(&mut store, pending, Lane::Sync).expect("enqueue");
3560 store.save_counters(Counters { next_request_id: 2, next_txn_id: 0 }).expect("save");
3563 let batch = store.take_flush_batch().expect("pending");
3564 store.ack_flush(batch.id);
3565
3566 let core = MessengerCore::open(
3567 batch.records,
3568 InsecurePlainCodecForTests,
3569 test_config(),
3570 CoreSecrets::default(),
3571 0,
3572 Box::new(FixedJitter(0.0)),
3573 )
3574 .expect("open succeeds");
3575 assert_eq!(
3576 core.counters.next_request_id, 6,
3577 "resumes past the highest id already in flight, not the stale persisted value"
3578 );
3579 }
3580
3581 #[test]
3582 fn sync_token_persisted_after_everything_it_covers() {
3583 let mut core = open_fresh_core();
3584 flush_and_ack(&mut core);
3585 core.releasable_requests(0);
3590 flush_and_ack(&mut core);
3591 let requests = core.releasable_requests(0);
3592 let sync_request = requests.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("a sync request was enqueued");
3593
3594 assert!(core.store.sync_token().expect("no error").is_none(), "no token before the first response");
3595
3596 let body = serde_json::json!({
3597 "next_batch": "s1",
3598 "device_one_time_keys_count": { "signed_curve25519": 0 },
3599 "device_unused_fallback_key_types": []
3600 })
3601 .to_string();
3602 core.on_response(sync_request.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
3603
3604 assert_eq!(core.store.sync_token().expect("no error"), Some("s1"));
3605 }
3606
3607 #[test]
3608 fn only_one_sync_in_flight() {
3609 let mut core = open_fresh_core();
3610 flush_and_ack(&mut core);
3611 core.releasable_requests(0);
3612 flush_and_ack(&mut core);
3613 let first = core.releasable_requests(0);
3614 assert_eq!(first.iter().filter(|r| r.kind == OutgoingRequestKind::Sync).count(), 1);
3615
3616 let second = core.releasable_requests(0);
3620 assert!(second.iter().all(|r| r.kind != OutgoingRequestKind::Sync), "no second sync request released");
3621 }
3622
3623 #[test]
3624 fn first_sync_of_a_session_never_long_polls_then_the_loop_does() {
3625 let mut core = open_fresh_core();
3626 flush_and_ack(&mut core);
3627 core.releasable_requests(0);
3628 flush_and_ack(&mut core);
3629 let first = core.releasable_requests(0);
3630 let first_sync = first.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("a sync request was enqueued");
3631 assert!(first_sync.query.contains(&("timeout".to_string(), "0".to_string())), "the initial sync must answer at once");
3632 assert!(first_sync.query.iter().all(|(key, _)| key != "since"), "a fresh device has no since token");
3633
3634 let body = serde_json::json!({
3635 "next_batch": "s1",
3636 "device_one_time_keys_count": { "signed_curve25519": 0 },
3637 "device_unused_fallback_key_types": []
3638 })
3639 .to_string();
3640 core.on_response(first_sync.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
3641 flush_and_ack(&mut core);
3642
3643 core.releasable_requests(0);
3646 flush_and_ack(&mut core);
3647 let second = core.releasable_requests(0);
3648 let second_sync = second.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("the follow-up sync was enqueued");
3649 assert!(second_sync.query.contains(&("since".to_string(), "s1".to_string())));
3650 assert!(second_sync.query.contains(&("timeout".to_string(), SYNC_TIMEOUT_MS.to_string())), "once caught up the loop long-polls");
3651 }
3652
3653 #[test]
3654 fn room_key_arrival_retries_undecryptable_items() {
3655 use crate::crypto::group_sessions::GroupSessionManager;
3656 use mail4agent_vodozemac::megolm::{GroupSession, SessionConfig};
3657 use mail4agent_vodozemac::olm::Account;
3658
3659 let mut core = open_fresh_core();
3660 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3661 let sender = UserId::parse("@bob:example.org").expect("valid user id");
3662 let identity = Account::new().identity_keys();
3663
3664 let mut group_session = GroupSession::new(SessionConfig::version_1());
3665 let session_id = group_session.session_id();
3666 let session_key = group_session.session_key();
3671 let plaintext = crate::crypto::group_sessions::RoomEventPlaintext {
3672 event_type: "m.room.message".to_string(),
3673 content: serde_json::json!({ "msgtype": "m.text", "body": "hi" }),
3674 room_id: room_id.clone(),
3675 };
3676 let message = group_session.encrypt(serde_json::to_vec(&plaintext).expect("valid JSON"));
3677
3678 let event: RawEvent = serde_json::from_value(serde_json::json!({
3679 "event_id": "$evt:example.org",
3680 "type": "m.room.encrypted",
3681 "sender": sender.as_str(),
3682 "origin_server_ts": 1,
3683 "content": {
3684 "algorithm": "m.megolm.v1.aes-sha2",
3685 "ciphertext": message.to_base64(),
3686 "session_id": session_id,
3687 }
3688 }))
3689 .expect("valid event");
3690
3691 let mut timeline = Timeline::new();
3697 timeline.apply_timeline_batch(std::slice::from_ref(&event), false, None);
3698 core.timelines.insert(room_id.clone(), timeline);
3699 core.decrypt_new_timeline_events(&room_id, std::slice::from_ref(&event));
3700 {
3701 let timeline = core.timeline(&room_id).expect("timeline exists");
3702 let item = timeline.item_by_event_id(&EventId::parse("$evt:example.org").expect("valid event id")).expect("item present");
3703 assert!(matches!(item.content, ItemContent::Undecryptable { .. }));
3704 }
3705
3706 let content = RoomKeyContent {
3710 algorithm: "m.megolm.v1.aes-sha2".to_string(),
3711 room_id: room_id.clone(),
3712 session_id: session_id.clone(),
3713 session_key: session_key.to_base64(),
3714 };
3715 GroupSessionManager::receive_room_key(&mut core.store, &sender, identity.curve25519, identity.ed25519, &content)
3716 .expect("receive room key");
3717 core.retry_pending_for_session(&room_id, &session_id);
3718
3719 let timeline = core.timeline(&room_id).expect("timeline exists");
3720 let item = timeline.item_by_event_id(&EventId::parse("$evt:example.org").expect("valid event id")).expect("item present");
3721 match &item.content {
3722 ItemContent::Text(text) => assert_eq!(text.body, "hi"),
3723 other => panic!("expected the retried decrypt to succeed, got {other:?}"),
3724 }
3725 }
3726
3727 #[test]
3728 fn redelivered_to_device_after_restart_is_dropped_silently() {
3729 use crate::crypto::olm_sessions::OlmSessionManager;
3730
3731 let alice_user = UserId::parse("@alice:example.org").expect("valid user id");
3733 let alice_device = DeviceId::parse("ALICEDEV").expect("valid device id");
3734 let mut alice_store = Store::new(alice_device.clone(), InsecurePlainCodecForTests);
3735 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
3736 let alice_device_keys =
3739 alice_account.device_keys_json(&alice_user, &alice_device).expect("sign alice's device_keys");
3740
3741 let mut bob_store = Store::new(device_id(), InsecurePlainCodecForTests);
3743 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
3744 bob_account.on_sync_counts(&mut bob_store, 0, &["signed_curve25519".to_string()]).expect("top up bob's OTKs");
3745 let upload = bob_account
3746 .keys_upload_request(RequestId::next(0), &user_id(), &device_id())
3747 .expect("builds a request")
3748 .expect("something to upload");
3749 let one_time_keys = upload.body.expect("upload has a body");
3750 let one_time_keys = one_time_keys.get("one_time_keys").and_then(serde_json::Value::as_object).expect("OTKs present");
3751 let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
3752 let claim_body = serde_json::to_vec(&serde_json::json!({
3753 "one_time_keys": { user_id().as_str(): { device_id().as_str(): { key_id: key_value } } }
3754 }))
3755 .expect("valid JSON");
3756 let bob_device_stored = crate::crypto::device_tracker::StoredDevice {
3757 user_id: user_id(),
3758 device_id: device_id(),
3759 curve25519: bob_account.identity_keys().curve25519,
3760 ed25519: bob_account.identity_keys().ed25519,
3761 algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
3762 display_name: None,
3763 verified: false,
3764 blocked: false,
3765 };
3766 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device_stored], &claim_body)
3767 .expect("alice establishes a session with bob");
3768
3769 DeviceTracker::on_keys_query_response(
3775 &mut bob_store,
3776 &serde_json::to_vec(&serde_json::json!({
3777 "device_keys": { alice_user.as_str(): { alice_device.as_str(): alice_device_keys } }
3778 }))
3779 .expect("valid JSON"),
3780 )
3781 .expect("bob learns alice's device");
3782
3783 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3784 let group_session = mail4agent_vodozemac::megolm::GroupSession::new(mail4agent_vodozemac::megolm::SessionConfig::version_1());
3785 let room_key_content = RoomKeyContent {
3786 algorithm: "m.megolm.v1.aes-sha2".to_string(),
3787 room_id,
3788 session_id: group_session.session_id(),
3789 session_key: group_session.session_key().to_base64(),
3790 };
3791 let encrypted = OlmSessionManager::encrypt_to_device(
3792 &mut alice_store,
3793 &alice_account,
3794 &alice_user,
3795 &alice_device,
3796 &bob_device_stored,
3797 "m.room_key",
3798 serde_json::to_value(&room_key_content).expect("valid JSON"),
3799 )
3800 .expect("encrypt to bob");
3801 let to_device_event = ToDeviceEvent {
3802 sender: alice_user.clone(),
3803 event_type: "m.room.encrypted".to_string(),
3804 content: serde_json::to_value(RoomEncryptedContent::Olm(encrypted)).expect("valid JSON"),
3805 };
3806
3807 let mut core =
3808 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, test_config(), CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3809 .expect("open succeeds");
3810 core.store = bob_store;
3811 core.account = bob_account;
3812
3813 let mut sessions = Vec::new();
3814 core.process_to_device_event(&to_device_event, &mut sessions).expect("first decrypt succeeds");
3815 assert_eq!(
3816 sessions,
3817 vec![(room_key_content.room_id.clone(), room_key_content.session_id.clone())],
3818 "the first, genuine delivery is fully validated and accepted"
3819 );
3820
3821 let batch = core.store.take_flush_batch().expect("decrypting dirtied the store");
3825 core.store.ack_flush(batch.id);
3826 let reloaded_store = Store::load(batch.records, InsecurePlainCodecForTests, device_id()).expect("reload succeeds");
3827 core.store = reloaded_store;
3828 core.account = OlmAccountState::load_or_create(&mut core.store).expect("reload reuses the persisted account");
3829
3830 let mut sessions_after_restart = Vec::new();
3834 let result = core.process_to_device_event(&to_device_event, &mut sessions_after_restart);
3835 assert!(result.is_ok(), "a redelivered to-device event is dropped, not surfaced as an ingestion error");
3836 assert!(sessions_after_restart.is_empty(), "the replay never produces a second accepted room key");
3837 }
3838
3839 #[test]
3840 fn unknown_sender_device_is_retried_after_keys_query() {
3841 use crate::crypto::olm_sessions::OlmSessionManager;
3842
3843 let alice_user = UserId::parse("@alice:example.org").expect("valid user id");
3845 let alice_device = DeviceId::parse("ALICEDEV").expect("valid device id");
3846 let mut alice_store = Store::new(alice_device.clone(), InsecurePlainCodecForTests);
3847 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
3848 let alice_device_keys =
3856 alice_account.device_keys_json(&alice_user, &alice_device).expect("sign alice's device_keys");
3857
3858 let bob_user = UserId::parse("@bob:example.org").expect("valid user id");
3866 let bob_device = DeviceId::parse("BOBDEV").expect("valid device id");
3867 let mut bob_store = Store::new(bob_device.clone(), InsecurePlainCodecForTests);
3868 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
3869 bob_account.on_sync_counts(&mut bob_store, 0, &["signed_curve25519".to_string()]).expect("top up bob's OTKs");
3870 let upload = bob_account
3871 .keys_upload_request(RequestId::next(0), &bob_user, &bob_device)
3872 .expect("builds a request")
3873 .expect("something to upload");
3874 let one_time_keys = upload.body.expect("upload has a body");
3875 let one_time_keys = one_time_keys.get("one_time_keys").and_then(serde_json::Value::as_object).expect("OTKs present");
3876 let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
3877 let claim_body = serde_json::to_vec(&serde_json::json!({
3878 "one_time_keys": { bob_user.as_str(): { bob_device.as_str(): { key_id: key_value } } }
3879 }))
3880 .expect("valid JSON");
3881 let bob_device_stored = crate::crypto::device_tracker::StoredDevice {
3882 user_id: bob_user.clone(),
3883 device_id: bob_device.clone(),
3884 curve25519: bob_account.identity_keys().curve25519,
3885 ed25519: bob_account.identity_keys().ed25519,
3886 algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
3887 display_name: None,
3888 verified: false,
3889 blocked: false,
3890 };
3891 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device_stored], &claim_body)
3892 .expect("alice establishes a session with bob");
3893 let encrypted = OlmSessionManager::encrypt_to_device(
3894 &mut alice_store,
3895 &alice_account,
3896 &alice_user,
3897 &alice_device,
3898 &bob_device_stored,
3899 "m.dummy",
3900 serde_json::json!({}),
3901 )
3902 .expect("encrypt to bob");
3903 let to_device_event = ToDeviceEvent {
3904 sender: alice_user.clone(),
3905 event_type: "m.room.encrypted".to_string(),
3906 content: serde_json::to_value(RoomEncryptedContent::Olm(encrypted)).expect("valid JSON"),
3907 };
3908
3909 let bob_config = CoreConfig { user_id: bob_user.clone(), device_id: bob_device.clone(), server_name: "example.org".to_string() };
3910 let mut core =
3911 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, bob_config, CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3912 .expect("open succeeds");
3913 core.store = bob_store;
3914 core.account = bob_account;
3915
3916 let mut sessions = Vec::new();
3920 core.process_to_device_event(&to_device_event, &mut sessions).expect("queues for retry, does not error");
3921 assert_eq!(core.unknown_sender_retry.len(), 1, "queued for a retry after the next keys/query");
3922 let alice_curve = alice_account.identity_keys().curve25519.to_base64();
3923 assert!(
3924 core.store.olm_sessions_for_device(&alice_curve).expect("no error").is_empty(),
3925 "the refused attempt created no Olm session (it would have consumed the one-time key)"
3926 );
3927
3928 let query_body = serde_json::to_vec(&serde_json::json!({
3933 "device_keys": { alice_user.as_str(): { alice_device.as_str(): alice_device_keys } }
3934 }))
3935 .expect("valid JSON");
3936 DeviceTracker::on_keys_query_response(&mut core.store, &query_body).expect("bob learns alice's device");
3937 core.retry_unknown_sender_queue().expect("no error");
3938 assert!(core.unknown_sender_retry.is_empty(), "the queued entry is drained by the retry attempt");
3939 assert_eq!(
3940 core.store.olm_sessions_for_device(&alice_curve).expect("no error").len(),
3941 1,
3942 "the retry decrypted the very same pre-key message and established the inbound session"
3943 );
3944 }
3945
3946 #[test]
3947 fn dispatch_load_older_produces_an_outgoing_request() {
3948 let mut core = open_fresh_core();
3949 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3950 let mut timeline = Timeline::new();
3951 let event: RawEvent = serde_json::from_value(serde_json::json!({
3952 "event_id": "$1:example.org",
3953 "type": "m.room.message",
3954 "sender": "@alice:example.org",
3955 "origin_server_ts": 1,
3956 "content": { "msgtype": "m.text", "body": "hi" }
3957 }))
3958 .expect("valid event");
3959 timeline.apply_timeline_batch(&[event], true, Some("t1".to_string()));
3960 core.timelines.insert(room_id.clone(), timeline);
3961 flush_and_ack(&mut core);
3962
3963 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3964 flush_and_ack(&mut core);
3965 let requests = core.releasable_requests(0);
3966 let load_older = requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomMessages);
3967 assert!(load_older.is_some(), "a RoomMessages request was enqueued using the room's own gap token");
3968 }
3969
3970 #[test]
3971 fn load_older_on_a_resumed_room_pages_back_from_the_sync_token() {
3972 let mut core = open_fresh_core();
3973 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3974 core.store.save_sync_token("s42_7".to_string()).expect("save the sync token");
3975 flush_and_ack(&mut core);
3976
3977 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3979 flush_and_ack(&mut core);
3980 assert!(core.releasable_requests(0).iter().all(|r| r.kind != OutgoingRequestKind::RoomMessages));
3981
3982 core.rooms.insert(room_id.clone(), RoomState::default());
3984 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3985 flush_and_ack(&mut core);
3986 let requests = core.releasable_requests(0);
3987 let load_older = requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomMessages).expect("a RoomMessages request was enqueued");
3988 assert!(load_older.query.contains(&("from".to_string(), "s42_7".to_string())), "pages back from the sync token: {:?}", load_older.query);
3989 }
3990
3991 fn bob_joins_room_body(encrypted: bool) -> serde_json::Value {
3994 let mut state = Vec::new();
3995 if encrypted {
3996 state.push(serde_json::json!({
3997 "event_id": "$enc:example.org", "type": "m.room.encryption", "sender": "@alice:example.org",
3998 "origin_server_ts": 1, "state_key": "", "content": { "algorithm": "m.megolm.v1.aes-sha2" }
3999 }));
4000 }
4001 serde_json::json!({
4002 "next_batch": "s1",
4003 "rooms": { "join": { "!r:example.org": {
4004 "state": { "events": state },
4005 "timeline": { "events": [{
4006 "event_id": "$join:example.org", "type": "m.room.member", "sender": "@bob:example.org",
4007 "origin_server_ts": 2, "state_key": "@bob:example.org", "content": { "membership": "join" }
4008 }] }
4009 } } }
4010 })
4011 }
4012
4013 #[test]
4014 fn a_member_joining_an_encrypted_room_is_tracked_and_queried_right_away() {
4015 let mut core = open_fresh_core();
4016 let bob = UserId::parse("@bob:example.org").expect("valid user id");
4017 deliver_sync(&mut core, bob_joins_room_body(true));
4018 let tracked = core.store.tracked_users().expect("no error");
4019 assert!(tracked.iter().any(|(user_id, outdated)| *user_id == bob && *outdated), "bob is tracked with an outdated device list: {tracked:?}");
4020
4021 flush_and_ack(&mut core);
4022 let requests = core.releasable_requests(0);
4023 let query = requests.iter().find(|r| r.kind == OutgoingRequestKind::KeysQuery).expect("a /keys/query was enqueued for the new member");
4024 assert!(query.body.as_ref().is_some_and(|body| body["device_keys"].get(bob.as_str()).is_some()), "the query names bob: {:?}", query.body);
4025 }
4026
4027 #[test]
4028 fn a_member_joining_a_plaintext_room_is_not_tracked() {
4029 let mut core = open_fresh_core();
4030 deliver_sync(&mut core, bob_joins_room_body(false));
4031 assert!(core.store.tracked_users().expect("no error").is_empty());
4032 flush_and_ack(&mut core);
4033 assert!(core.releasable_requests(0).iter().all(|r| r.kind != OutgoingRequestKind::KeysQuery));
4034 }
4035
4036 #[test]
4037 fn dispatch_mark_read_and_set_typing_produce_outgoing_requests() {
4038 let mut core = open_fresh_core();
4039 flush_and_ack(&mut core);
4040 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4041 let event_id = EventId::parse("$1:example.org").expect("valid event id");
4042
4043 core.dispatch(MessengerCommand::MarkRead { room_id: room_id.clone(), event_id }, 0).expect("dispatch succeeds");
4044 core.dispatch(MessengerCommand::SetTyping { room_id: room_id.clone(), typing: true }, 0).expect("dispatch succeeds");
4045 flush_and_ack(&mut core);
4046
4047 let requests = core.releasable_requests(0);
4048 assert!(requests.iter().any(|r| r.kind == OutgoingRequestKind::ReadMarkers));
4049 assert!(requests.iter().any(|r| r.kind == OutgoingRequestKind::Typing));
4050 }
4051
4052 #[test]
4053 fn snapshot_reflects_the_latest_applied_sync() {
4054 let mut core = open_fresh_core();
4055 flush_and_ack(&mut core);
4056 core.releasable_requests(0);
4057 flush_and_ack(&mut core);
4058 let requests = core.releasable_requests(0);
4059 let sync_request = requests.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("sync enqueued");
4060
4061 let body = serde_json::json!({
4062 "next_batch": "s1",
4063 "rooms": {
4064 "join": {
4065 "!r:example.org": {
4066 "state": { "events": [] },
4067 "timeline": {
4068 "events": [
4069 {
4070 "event_id": "$1:example.org",
4071 "type": "m.room.message",
4072 "sender": "@alice:example.org",
4073 "origin_server_ts": 1,
4074 "content": { "msgtype": "m.text", "body": "hi" }
4075 }
4076 ]
4077 },
4078 "unread_notifications": { "notification_count": 1, "highlight_count": 0 }
4079 }
4080 }
4081 }
4082 })
4083 .to_string();
4084 let events = core.on_response(sync_request.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
4085
4086 assert!(events.contains(&MessengerEvent::RoomsChanged));
4087 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4088 assert!(events.contains(&MessengerEvent::TimelineChanged { room_id: room_id.clone() }));
4089 assert!(events.contains(&MessengerEvent::UnreadChanged { room_id: room_id.clone() }));
4090
4091 let timeline = core.timeline(&room_id).expect("timeline present");
4092 assert_eq!(timeline.items().len(), 1);
4093 assert_eq!(core.room_state(&room_id).expect("room present").unread_count(), 1);
4094 assert_eq!(core.change_counter(), events.len() as u64);
4095 assert!(core.events().is_empty());
4098 }
4099
4100 #[test]
4101 fn dispatch_set_account_data_updates_optimistically_and_sends_a_put() {
4102 let mut core = open_fresh_core();
4103 flush_and_ack(&mut core);
4104 let content = serde_json::json!({ "note": "keep" });
4105 core.dispatch(
4106 MessengerCommand::SetAccountData { event_type: "com.example.prefs".to_string(), content: content.clone() },
4107 0,
4108 )
4109 .expect("dispatch succeeds");
4110
4111 assert_eq!(
4112 core.global_account_data("com.example.prefs"),
4113 Some(&content),
4114 "SetAccountData updates the core's own cache before any round trip"
4115 );
4116
4117 flush_and_ack(&mut core);
4118 let requests = core.releasable_requests(0);
4119 let request = requests.iter().find(|r| r.kind == OutgoingRequestKind::AccountData).expect("an AccountData request was enqueued");
4120 assert_eq!(request.method, HttpMethod::Put);
4121 assert_eq!(request.path, "/_matrix/client/v3/user/%40alice%3Aexample.org/account_data/com.example.prefs");
4122 assert_eq!(request.body.as_ref(), Some(&content));
4123 }
4124
4125 #[test]
4126 fn dispatch_set_room_account_data_updates_optimistically_and_sends_a_put() {
4127 let mut core = open_fresh_core();
4128 flush_and_ack(&mut core);
4129 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4130 let content = serde_json::json!({ "flag": true });
4131 core.dispatch(
4132 MessengerCommand::SetRoomAccountData {
4133 room_id: room_id.clone(),
4134 event_type: "com.example.flag".to_string(),
4135 content: content.clone(),
4136 },
4137 0,
4138 )
4139 .expect("dispatch succeeds");
4140
4141 assert_eq!(
4142 core.room_account_data(&room_id, "com.example.flag"),
4143 Some(&content),
4144 "SetRoomAccountData updates the core's own cache before any round trip"
4145 );
4146
4147 flush_and_ack(&mut core);
4148 let requests = core.releasable_requests(0);
4149 let request =
4150 requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomAccountData).expect("a RoomAccountData request was enqueued");
4151 assert_eq!(request.method, HttpMethod::Put);
4152 assert_eq!(
4153 request.path,
4154 "/_matrix/client/v3/user/%40alice%3Aexample.org/rooms/%21r%3Aexample.org/account_data/com.example.flag"
4155 );
4156 assert_eq!(request.body.as_ref(), Some(&content));
4157 }
4158
4159 #[test]
4160 fn dispatch_search_public_rooms_sends_the_expected_request() {
4161 let mut core = open_fresh_core();
4162 flush_and_ack(&mut core);
4163 core.dispatch(MessengerCommand::SearchPublicRooms { term: "trading".to_string() }, 0).expect("dispatch succeeds");
4164 flush_and_ack(&mut core);
4165 let requests = core.releasable_requests(0);
4166 let request = requests.iter().find(|r| r.kind == OutgoingRequestKind::PublicRooms).expect("a PublicRooms request was enqueued");
4167 assert_eq!(request.method, HttpMethod::Post);
4168 assert_eq!(request.path, "/_matrix/client/v3/publicRooms");
4169 assert_eq!(request.body, Some(serde_json::json!({ "filter": { "generic_search_term": "trading" }, "limit": 50 })));
4170 }
4171
4172 #[test]
4173 fn dispatch_search_users_sends_the_expected_request() {
4174 let mut core = open_fresh_core();
4175 flush_and_ack(&mut core);
4176 core.dispatch(MessengerCommand::SearchUsers { term: "bob".to_string() }, 0).expect("dispatch succeeds");
4177 flush_and_ack(&mut core);
4178 let requests = core.releasable_requests(0);
4179 let request =
4180 requests.iter().find(|r| r.kind == OutgoingRequestKind::UserDirectorySearch).expect("a UserDirectorySearch request was enqueued");
4181 assert_eq!(request.method, HttpMethod::Post);
4182 assert_eq!(request.path, "/_matrix/client/v3/user_directory/search");
4183 assert_eq!(request.body, Some(serde_json::json!({ "search_term": "bob", "limit": 20 })));
4184 }
4185
4186 #[test]
4187 fn public_rooms_response_superseded_by_a_later_search_is_dropped() {
4188 let mut core = open_fresh_core();
4189 flush_and_ack(&mut core);
4190 core.dispatch(MessengerCommand::SearchPublicRooms { term: "first".to_string() }, 0).expect("dispatch succeeds");
4191 flush_and_ack(&mut core);
4192 let first_request = core
4193 .releasable_requests(0)
4194 .into_iter()
4195 .find(|r| r.kind == OutgoingRequestKind::PublicRooms)
4196 .expect("the first search's own request");
4197
4198 core.dispatch(MessengerCommand::SearchPublicRooms { term: "second".to_string() }, 0).expect("dispatch succeeds");
4201 flush_and_ack(&mut core);
4202 let second_request = core
4203 .releasable_requests(0)
4204 .into_iter()
4205 .find(|r| r.kind == OutgoingRequestKind::PublicRooms)
4206 .expect("the second search's own request");
4207
4208 let stale_body = serde_json::json!({ "chunk": [{ "room_id": "!stale:example.org", "num_joined_members": 1 }] });
4210 core.on_response(first_request.id, HttpResponseDescriptor { status: 200, body: serde_json::to_vec(&stale_body).expect("json") }, 0);
4211 assert!(core.public_rooms_result().is_empty(), "a response to a superseded search is dropped");
4212
4213 let fresh_body = serde_json::json!({ "chunk": [{ "room_id": "!fresh:example.org", "num_joined_members": 2 }] });
4215 core.on_response(second_request.id, HttpResponseDescriptor { status: 200, body: serde_json::to_vec(&fresh_body).expect("json") }, 0);
4216 assert_eq!(core.public_rooms_result().len(), 1);
4217 assert_eq!(core.public_rooms_result()[0].room_id, RoomId::parse("!fresh:example.org").expect("valid room id"));
4218 }
4219
4220 fn is_account_data_write(request: &OutgoingRequest) -> bool {
4221 matches!(request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData)
4222 }
4223
4224 #[test]
4225 fn pin_then_archive_back_to_back_keeps_both_tags() {
4226 let mut core = open_fresh_core();
4227 flush_and_ack(&mut core);
4228 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4229 for tag in ["m.favourite", "u.example.archived"] {
4230 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4231 .expect("dispatch succeeds");
4232 }
4233
4234 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4236 assert!(cached["tags"].get("m.favourite").is_some());
4237 assert!(cached["tags"].get("u.example.archived").is_some());
4238
4239 flush_and_ack(&mut core);
4240 let first_release = core.releasable_requests(0);
4241 let first_writes: Vec<&OutgoingRequest> = first_release.iter().filter(|r| is_account_data_write(r)).collect();
4242 assert_eq!(first_writes.len(), 1, "the second PUT is not released while the first is in flight");
4243 let first_body = first_writes[0].body.clone().expect("a body");
4244 assert!(first_body["tags"].get("m.favourite").is_some());
4245 assert!(first_body["tags"].get("u.example.archived").is_none(), "the first PUT carries only what existed when it was built");
4246
4247 core.on_response(first_writes[0].id.clone(), HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }, 0);
4248 flush_and_ack(&mut core);
4249 let second_release = core.releasable_requests(0);
4250 let second = second_release.iter().find(|r| is_account_data_write(r)).expect("the second PUT is released after the first completed");
4251 let second_body = second.body.clone().expect("a body");
4252 assert!(second_body["tags"].get("m.favourite").is_some(), "the later PUT still carries the earlier tag");
4253 assert!(second_body["tags"].get("u.example.archived").is_some());
4254 }
4255
4256 #[test]
4257 fn account_data_lane_is_fifo_single_flight() {
4258 let mut core = open_fresh_core();
4259 flush_and_ack(&mut core);
4260 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4261 core.dispatch(MessengerCommand::SetAccountData { event_type: "org.t.one".to_string(), content: serde_json::json!({}) }, 0)
4262 .expect("dispatch succeeds");
4263 core.dispatch(
4264 MessengerCommand::SetRoomAccountData {
4265 room_id: room_id.clone(),
4266 event_type: "org.t.two".to_string(),
4267 content: serde_json::json!({}),
4268 },
4269 0,
4270 )
4271 .expect("dispatch succeeds");
4272 core.dispatch(MessengerCommand::SetAccountData { event_type: "org.t.three".to_string(), content: serde_json::json!({}) }, 0)
4273 .expect("dispatch succeeds");
4274 core.dispatch(MessengerCommand::RemoveTag { room_id: room_id.clone(), tag: "m.favourite".to_string() }, 0)
4275 .expect("dispatch succeeds");
4276 flush_and_ack(&mut core);
4277
4278 let mut released_paths = Vec::new();
4279 for _ in 0..4 {
4280 let release = core.releasable_requests(0);
4281 let writes: Vec<&OutgoingRequest> = release.iter().filter(|r| is_account_data_write(r)).collect();
4282 assert_eq!(writes.len(), 1, "exactly one account-data write in flight at a time");
4283 released_paths.push(writes[0].path.clone());
4284 core.on_response(writes[0].id.clone(), HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }, 0);
4285 flush_and_ack(&mut core);
4286 }
4287 assert!(released_paths[0].ends_with("/account_data/org.t.one"));
4288 assert!(released_paths[1].ends_with("/account_data/org.t.two"));
4289 assert!(released_paths[2].ends_with("/account_data/org.t.three"));
4290 assert!(released_paths[3].ends_with("/account_data/m.tag"));
4291 assert!(core.releasable_requests(0).iter().all(|r| !is_account_data_write(r)), "the lane is drained");
4292 }
4293
4294 fn deliver_sync(core: &mut MessengerCore<InsecurePlainCodecForTests>, body: serde_json::Value) -> Vec<OutgoingRequest> {
4299 let mut others = Vec::new();
4300 for _ in 0..3 {
4301 flush_and_ack(core);
4302 let mut sync_request = None;
4303 for request in core.releasable_requests(0) {
4304 if request.kind == OutgoingRequestKind::Sync && sync_request.is_none() {
4305 sync_request = Some(request);
4306 } else {
4307 others.push(request);
4308 }
4309 }
4310 if let Some(sync_request) = sync_request {
4311 let body = serde_json::to_vec(&body).expect("valid JSON");
4312 core.on_response(sync_request.id, HttpResponseDescriptor { status: 200, body }, 0);
4313 return others;
4314 }
4315 }
4316 panic!("no sync request became releasable");
4317 }
4318
4319 fn tag_sync_body(room_id: &str, tags: serde_json::Value, next_batch: &str) -> serde_json::Value {
4320 let mut join = serde_json::Map::new();
4321 join.insert(
4322 room_id.to_string(),
4323 serde_json::json!({ "account_data": { "events": [{ "type": "m.tag", "content": { "tags": tags } }] } }),
4324 );
4325 serde_json::json!({ "next_batch": next_batch, "rooms": { "join": serde_json::Value::Object(join) } })
4326 }
4327
4328 fn ok_response() -> HttpResponseDescriptor {
4329 HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }
4330 }
4331
4332 #[test]
4333 fn sync_echo_of_an_older_tag_write_does_not_clobber_a_newer_optimistic_value() {
4334 let mut core = open_fresh_core();
4335 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4336 for tag in ["m.favourite", "u.example.archived"] {
4337 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4338 .expect("dispatch succeeds");
4339 }
4340
4341 let released = deliver_sync(&mut core, tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {} }), "s1"));
4343 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4344 assert!(cached["tags"].get("u.example.archived").is_some(), "the older echo must not clobber the newer optimistic value");
4345 assert!(cached["tags"].get("m.favourite").is_some());
4346 let key = (Some(room_id.clone()), "m.tag".to_string());
4347 assert_eq!(
4348 core.account_data_guard.get(&key).expect("writes are outstanding").server_value,
4349 Some(serde_json::json!({ "tags": { "m.favourite": {} } })),
4350 "the echo is remembered as the server's value"
4351 );
4352
4353 let first = released.iter().find(|r| r.kind == OutgoingRequestKind::RoomAccountData).expect("first write released");
4356 core.on_response(first.id.clone(), ok_response(), 0);
4357 flush_and_ack(&mut core);
4358 let second = core
4359 .releasable_requests(0)
4360 .into_iter()
4361 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4362 .expect("second write released after the first");
4363 core.on_response(second.id, ok_response(), 0);
4364 assert!(core.account_data_guard.is_empty());
4365 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4366 assert!(cached["tags"].get("m.favourite").is_some() && cached["tags"].get("u.example.archived").is_some());
4367
4368 deliver_sync(
4370 &mut core,
4371 tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {}, "u.example.archived": {} }), "s2"),
4372 );
4373 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4374 assert!(cached["tags"].get("u.example.archived").is_some());
4375 }
4376
4377 #[test]
4378 fn terminal_account_data_failure_restores_the_server_value() {
4379 let mut core = open_fresh_core();
4380 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4381 deliver_sync(&mut core, tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {} }), "s1"));
4382
4383 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: "u.example.archived".to_string(), order: None }, 0)
4384 .expect("dispatch succeeds");
4385 assert!(core.room_account_data(&room_id, "m.tag").expect("cached")["tags"].get("u.example.archived").is_some());
4386 flush_and_ack(&mut core);
4387 let write = core
4388 .releasable_requests(0)
4389 .into_iter()
4390 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4391 .expect("the write is released");
4392
4393 let counter_before = core.change_counter();
4394 let forbidden =
4395 HttpResponseDescriptor { status: 403, body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec() };
4396 let events = core.on_response(write.id, forbidden, 0);
4397 assert!(core.account_data_guard.is_empty());
4398 let restored = core.room_account_data(&room_id, "m.tag").expect("the server's value is back");
4399 assert!(restored["tags"].get("u.example.archived").is_none(), "the failed write's optimistic value is gone");
4400 assert!(restored["tags"].get("m.favourite").is_some());
4401 assert!(events.contains(&MessengerEvent::RoomsChanged));
4402 assert!(core.change_counter() > counter_before, "the UI is told to re-render");
4403
4404 for tag in ["u.a", "u.b"] {
4408 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4409 .expect("dispatch succeeds");
4410 }
4411 flush_and_ack(&mut core);
4412 let first = core
4413 .releasable_requests(0)
4414 .into_iter()
4415 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4416 .expect("first write released");
4417 let forbidden =
4418 HttpResponseDescriptor { status: 403, body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec() };
4419 core.on_response(first.id, forbidden, 0);
4420 let cached = core.room_account_data(&room_id, "m.tag").expect("cached");
4421 assert!(cached["tags"].get("u.a").is_some() && cached["tags"].get("u.b").is_some(), "cache untouched while a write is pending");
4422 flush_and_ack(&mut core);
4423 let second = core
4424 .releasable_requests(0)
4425 .into_iter()
4426 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4427 .expect("second write released");
4428 core.on_response(second.id, ok_response(), 0);
4429 let cached = core.room_account_data(&room_id, "m.tag").expect("cached");
4430 assert!(cached["tags"].get("u.b").is_some(), "the last write succeeded: its optimistic value stands");
4431 }
4432
4433 #[test]
4434 fn pending_account_data_writes_rebuild_their_guard_after_a_restart() {
4435 let mut core = open_fresh_core();
4436 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4437 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: "m.favourite".to_string(), order: None }, 0)
4438 .expect("dispatch succeeds");
4439 core.dispatch(
4440 MessengerCommand::SetAccountData { event_type: "org.t.one".to_string(), content: serde_json::json!({ "n": 1 }) },
4441 0,
4442 )
4443 .expect("dispatch succeeds");
4444
4445 let mut records = Vec::new();
4447 while let Some(batch) = core.take_flush_batch() {
4448 records.extend(batch.records.clone());
4449 core.ack_flush(batch.id);
4450 }
4451 let reopened = MessengerCore::open(
4452 records,
4453 InsecurePlainCodecForTests,
4454 test_config(),
4455 CoreSecrets::default(),
4456 0,
4457 Box::new(FixedJitter(0.0)),
4458 )
4459 .expect("reopen succeeds");
4460
4461 assert_eq!(reopened.account_data_guard.len(), 2, "one guard per key with a pending write");
4462 assert!(reopened.account_data_guard.values().all(|guard| guard.pending == 1 && guard.server_value.is_none()));
4463 assert_eq!(reopened.global_account_data("org.t.one"), Some(&serde_json::json!({ "n": 1 })));
4464 assert!(reopened.room_account_data(&room_id, "m.tag").expect("cached")["tags"].get("m.favourite").is_some());
4465 }
4466
4467 type TestCore = MessengerCore<InsecurePlainCodecForTests>;
4472
4473 fn room(id: &str) -> RoomId {
4474 RoomId::parse(id).expect("valid room id")
4475 }
4476
4477 fn event(id: &str) -> EventId {
4478 EventId::parse(id).expect("valid event id")
4479 }
4480
4481 fn add_room(core: &mut TestCore, room_id: &RoomId, encrypted: bool, name: Option<&str>) {
4484 let mut state = RoomState::new();
4485 if encrypted {
4486 state.encryption = Some(crate::wire::events::RoomEncryptionContent {
4487 algorithm: "m.megolm.v1.aes-sha2".to_string(),
4488 rotation_period_ms: 604_800_000,
4489 rotation_period_msgs: 100,
4490 });
4491 }
4492 state.name = name.map(str::to_string);
4493 core.rooms.insert(room_id.clone(), state);
4494 core.timelines.entry(room_id.clone()).or_default();
4495 }
4496
4497 #[test]
4500 fn set_events_default_opens_a_channel_for_every_member() {
4501 let mut core = open_fresh_core();
4502 let chan = room("!chan:example.org");
4503 add_room(&mut core, &chan, false, Some("Talk"));
4504 mark_as_channel(&mut core, &chan);
4505 core.dispatch(MessengerCommand::SetEventsDefault { room_id: chan.clone(), level: 0 }, 1_000).expect("dispatch");
4506 core.releasable_requests(1_000);
4507 flush_and_ack(&mut core);
4508 let sent = core.releasable_requests(1_000);
4509 let put = sent.iter().find(|r| r.kind == OutgoingRequestKind::SetState).expect("a state PUT");
4510 assert!(put.path.ends_with("/state/m.room.power_levels/"), "{}", put.path);
4511 let body = put.body.clone().expect("body");
4512 assert_eq!(body["events_default"], 0);
4513 assert_eq!(body["users_default"], 0);
4514 }
4515
4516 #[test]
4517 fn set_events_default_refuses_an_unknown_room() {
4518 let mut core = open_fresh_core();
4519 let err = core.dispatch(MessengerCommand::SetEventsDefault { room_id: room("!nope:example.org"), level: 0 }, 1_000);
4520 assert!(err.is_err());
4521 }
4522
4523 fn mark_as_channel(core: &mut TestCore, room_id: &RoomId) {
4524 let Some(state) = core.rooms.get_mut(room_id) else { return };
4525 state
4526 .apply_state_event(&state_event_raw(
4527 "m.room.join_rules",
4528 "",
4529 "@alice:example.org",
4530 serde_json::json!({ "join_rule": "public" }),
4531 ))
4532 .expect("join_rules");
4533 state
4534 .apply_state_event(&state_event_raw(
4535 "m.room.power_levels",
4536 "",
4537 "@alice:example.org",
4538 serde_json::json!({ "events_default": 50, "users_default": 0 }),
4539 ))
4540 .expect("power_levels");
4541 }
4542
4543 fn state_event_raw(event_type: &str, state_key: &str, sender: &str, content: serde_json::Value) -> RawEvent {
4544 serde_json::from_value(serde_json::json!({
4545 "event_id": format!("${event_type}:example.org"),
4546 "type": event_type,
4547 "sender": sender,
4548 "origin_server_ts": 1,
4549 "state_key": state_key,
4550 "content": content,
4551 }))
4552 .expect("valid raw event")
4553 }
4554
4555 fn raw_event(event_id: &str, event_type: &str, sender: &str, content: serde_json::Value) -> RawEvent {
4556 serde_json::from_value(serde_json::json!({
4557 "event_id": event_id,
4558 "type": event_type,
4559 "sender": sender,
4560 "origin_server_ts": 1,
4561 "content": content,
4562 }))
4563 .expect("valid raw event")
4564 }
4565
4566 fn seed_plain(core: &mut TestCore, room_id: &RoomId, event_id: &str, sender: &str, event_type: &str, content: serde_json::Value) {
4568 let raw = raw_event(event_id, event_type, sender, content);
4569 core.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(&[raw], false, None);
4570 }
4571
4572 fn seed_sealed(core: &mut TestCore, room_id: &RoomId, event_id: &str, sender: &str) {
4574 let content = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2", "ciphertext": "AAAA", "session_id": "s1" });
4575 seed_plain(core, room_id, event_id, sender, "m.room.encrypted", content);
4576 }
4577
4578 fn seed_decrypted(
4581 core: &mut TestCore,
4582 room_id: &RoomId,
4583 event_id_str: &str,
4584 sender: &str,
4585 event_type: &str,
4586 content: serde_json::Value,
4587 ) {
4588 seed_sealed(core, room_id, event_id_str, sender);
4589 if let Some(timeline) = core.timelines.get_mut(room_id) {
4590 timeline.set_decrypted_event(&event(event_id_str), event_type, &content);
4591 }
4592 }
4593
4594 fn sent_room_event(core: &mut TestCore) -> (String, serde_json::Value) {
4598 flush_and_ack(core);
4599 let request = core
4600 .releasable_requests(0)
4601 .into_iter()
4602 .find(|request| request.kind == OutgoingRequestKind::RoomSend)
4603 .expect("a room send is releasable");
4604 let confirmed = format!("$sent{}:example.org", core.counters.next_request_id);
4605 let body = serde_json::json!({ "event_id": confirmed }).to_string().into_bytes();
4606 core.on_response(request.id.clone(), HttpResponseDescriptor { status: 200, body }, 0);
4607 (request.path, request.body.expect("a room send has a body"))
4608 }
4609
4610 fn forward(core: &mut TestCore, from: &RoomId, event_id: &str, to: &RoomId) -> Result<(), MessengerError> {
4611 core.dispatch(
4612 MessengerCommand::Forward { from_room: from.clone(), event_id: event(event_id), to_room: to.clone(), txn_id: None },
4613 0,
4614 )
4615 }
4616
4617 fn last_item(core: &TestCore, room_id: &RoomId) -> crate::room::timeline::TimelineItem {
4618 core.timeline(room_id).and_then(|timeline| timeline.items().last()).expect("the room has an item").clone()
4619 }
4620
4621 #[test]
4622 fn forward_from_encrypted_room_carries_only_the_forwarded_flag() {
4623 let mut core = open_fresh_core();
4624 let dm = room("!dm:example.org");
4625 let target = room("!target:example.org");
4626 add_room(&mut core, &dm, true, Some("Secret DM name"));
4627 add_room(&mut core, &target, false, None);
4628 seed_decrypted(
4629 &mut core,
4630 &dm,
4631 "$src:example.org",
4632 "@bob:example.org",
4633 "m.room.message",
4634 serde_json::json!({
4635 "msgtype": "m.text",
4636 "body": "quarterly numbers",
4637 "m.relates_to": { "m.in_reply_to": { "event_id": "$parent:example.org" } },
4638 "m.mentions": { "user_ids": ["@carol:example.org"] },
4639 }),
4640 );
4641
4642 forward(&mut core, &dm, "$src:example.org", &target).expect("a decrypted message is forwardable");
4643
4644 let (path, body) = sent_room_event(&mut core);
4645 assert!(path.contains("/send/m.room.message/"), "the event type is kept: {path}");
4646 assert_eq!(
4647 body,
4648 serde_json::json!({ "msgtype": "m.text", "body": "quarterly numbers", "forwarded": true }),
4649 "the text plus the bare flag; the reply pointer and the mentions are stripped"
4650 );
4651 let wire = body.to_string();
4652 for leaked in ["bob", "carol", "!dm", "Secret DM name", "$parent"] {
4653 assert!(!wire.contains(leaked), "{leaked:?} must not travel with a forward out of an encrypted room: {wire}");
4654 }
4655 let echo = last_item(&core, &target);
4656 assert_eq!(echo.forwarded, Some(Forwarded::Hidden), "the forwarder's own echo shows the same marker");
4657 }
4658
4659 #[test]
4660 fn forward_from_public_channel_carries_room_id_and_name_no_sender() {
4661 let mut core = open_fresh_core();
4662 let channel = room("!chan:example.org");
4663 let target = room("!target:example.org");
4664 add_room(&mut core, &channel, true, Some("Announcements"));
4665 mark_as_channel(&mut core, &channel);
4666 add_room(&mut core, &target, false, None);
4667 seed_plain(
4668 &mut core,
4669 &channel,
4670 "$post:example.org",
4671 "@dave:example.org",
4672 "m.room.message",
4673 serde_json::json!({
4674 "msgtype": "m.text",
4675 "body": "BTC breaks out",
4676 "m.mentions": { "room": true },
4677 "m.relates_to": { "m.in_reply_to": { "event_id": "$older:example.org" } },
4678 }),
4679 );
4680
4681 forward(&mut core, &channel, "$post:example.org", &target).expect("a channel post is forwardable");
4682
4683 let (_, body) = sent_room_event(&mut core);
4684 assert_eq!(
4685 body,
4686 serde_json::json!({
4687 "msgtype": "m.text",
4688 "body": "BTC breaks out",
4689 "forwarded_from": { "room_id": "!chan:example.org", "room_name": "Announcements" },
4690 })
4691 );
4692 assert!(!body.to_string().contains("dave"), "the original sender never travels");
4693 assert_eq!(
4694 last_item(&core, &target).forwarded,
4695 Some(Forwarded::Channel { room_id: channel.clone(), room_name: "Announcements".to_string() })
4696 );
4697 }
4698
4699 #[test]
4700 fn forward_from_an_unnamed_room_never_lists_member_names() {
4701 let mut core = open_fresh_core();
4702 let channel = room("!chan:example.org");
4703 let target = room("!target:example.org");
4704 add_room(&mut core, &channel, true, None);
4705 mark_as_channel(&mut core, &channel);
4706 if let Some(state) = core.rooms.get_mut(&channel) {
4707 state.members.insert(
4708 UserId::parse("@erin:example.org").expect("valid user id"),
4709 crate::room::state::MemberState {
4710 membership: Membership::Join,
4711 displayname: Some("Erin Private".to_string()),
4712 is_direct: false,
4713 invited_by: None,
4714 },
4715 );
4716 }
4717 add_room(&mut core, &target, false, None);
4718 seed_plain(&mut core, &channel, "$post:example.org", "@erin:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "hi" }));
4719
4720 forward(&mut core, &channel, "$post:example.org", &target).expect("forwardable");
4721
4722 let (_, body) = sent_room_event(&mut core);
4723 let name = body["forwarded_from"]["room_name"].as_str().expect("a name is sent");
4724 assert!(!name.contains("Erin"), "an unnamed room is attributed by a generic label, not by its members: {name}");
4725 }
4726
4727 #[test]
4728 fn forward_replaces_the_sources_own_forward_marker() {
4729 let mut core = open_fresh_core();
4730 let dm = room("!dm:example.org");
4731 let target = room("!target:example.org");
4732 add_room(&mut core, &dm, true, None);
4733 add_room(&mut core, &target, false, None);
4734 seed_decrypted(
4735 &mut core,
4736 &dm,
4737 "$src:example.org",
4738 "@bob:example.org",
4739 "m.room.message",
4740 serde_json::json!({
4741 "msgtype": "m.text",
4742 "body": "second hop",
4743 "forwarded_from": { "room_id": "!older:example.org", "room_name": "Older channel" },
4744 }),
4745 );
4746
4747 forward(&mut core, &dm, "$src:example.org", &target).expect("forwardable");
4748
4749 let (_, body) = sent_room_event(&mut core);
4750 assert_eq!(body, serde_json::json!({ "msgtype": "m.text", "body": "second hop", "forwarded": true }));
4751 }
4752
4753 #[test]
4754 fn forward_of_edited_item_sends_the_latest_text() {
4755 let mut core = open_fresh_core();
4756 let channel = room("!chan:example.org");
4757 let dm = room("!dm:example.org");
4758 let target = room("!target:example.org");
4759 add_room(&mut core, &channel, false, Some("Announcements"));
4760 add_room(&mut core, &dm, true, None);
4761 add_room(&mut core, &target, false, None);
4762
4763 seed_plain(&mut core, &channel, "$orig:example.org", "@dave:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "typo text" }));
4765 seed_plain(
4766 &mut core,
4767 &channel,
4768 "$edit:example.org",
4769 "@dave:example.org",
4770 "m.room.message",
4771 serde_json::json!({
4772 "msgtype": "m.text",
4773 "body": "* fixed text",
4774 "m.new_content": { "msgtype": "m.text", "body": "fixed text" },
4775 "m.relates_to": { "rel_type": "m.replace", "event_id": "$orig:example.org" },
4776 }),
4777 );
4778 forward(&mut core, &channel, "$orig:example.org", &target).expect("forwardable");
4779 let (_, body) = sent_room_event(&mut core);
4780 assert_eq!(body["body"], "fixed text", "the latest edit's text, not the original and not the `* ` fallback");
4781 assert!(body.get("m.new_content").is_none() && body.get("m.relates_to").is_none());
4782
4783 seed_decrypted(&mut core, &dm, "$secret:example.org", "@bob:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "typo secret" }));
4785 seed_sealed(&mut core, &dm, "$secret-edit:example.org", "@bob:example.org");
4786 let applied = core.timelines.get_mut(&dm).expect("timeline").apply_decrypted_relation(
4787 &event("$secret-edit:example.org"),
4788 UserId::parse("@bob:example.org").expect("valid user id"),
4789 2,
4790 RelatesTo::Replace { event_id: event("$secret:example.org") },
4791 Some(serde_json::json!({ "msgtype": "m.text", "body": "fixed secret" })),
4792 );
4793 assert!(applied, "the decrypted edit folds onto its target");
4794 forward(&mut core, &dm, "$secret:example.org", &target).expect("forwardable");
4795 let (_, body) = sent_room_event(&mut core);
4796 assert_eq!(body, serde_json::json!({ "msgtype": "m.text", "body": "fixed secret", "forwarded": true }));
4797 }
4798
4799 #[test]
4800 fn forward_of_unknown_msgtype_keeps_its_fields() {
4801 let mut core = open_fresh_core();
4802 let dm = room("!dm:example.org");
4803 let target = room("!target:example.org");
4804 add_room(&mut core, &dm, true, None);
4805 add_room(&mut core, &target, false, None);
4806 let card = serde_json::json!({
4807 "msgtype": "com.example.card",
4808 "body": "opaque",
4809 "note": "kept raw",
4810 });
4811 seed_decrypted(&mut core, &dm, "$card:example.org", "@bob:example.org", "m.room.message", card.clone());
4812
4813 forward(&mut core, &dm, "$card:example.org", &target).expect("an unrecognized msgtype is still an m.room.message");
4814
4815 let (path, body) = sent_room_event(&mut core);
4816 assert!(path.contains("/send/m.room.message/"));
4817 let mut expected = card;
4818 expected["forwarded"] = serde_json::Value::Bool(true);
4819 assert_eq!(body, expected, "unrecognized msgtype fields pass through plus the marker");
4820 assert!(matches!(last_item(&core, &target).content, ItemContent::Unknown), "the echo does not invent a card type");
4821 }
4822
4823 #[test]
4824 fn forward_refuses_redacted_sealed_unknown_and_missing() {
4825 let mut core = open_fresh_core();
4826 let dm = room("!dm:example.org");
4827 let target = room("!target:example.org");
4828 add_room(&mut core, &dm, true, None);
4829 add_room(&mut core, &target, false, None);
4830
4831 seed_decrypted(&mut core, &dm, "$redacted:example.org", "@bob:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "gone" }));
4832 core.timelines.get_mut(&dm).expect("timeline").apply_redaction(&event("$redacted:example.org"));
4833 seed_sealed(&mut core, &dm, "$sealed:example.org", "@bob:example.org");
4834 seed_sealed(&mut core, &dm, "$utd:example.org", "@bob:example.org");
4835 core.timelines
4836 .get_mut(&dm)
4837 .expect("timeline")
4838 .set_decrypted(&event("$utd:example.org"), ItemContent::Undecryptable { reason: "MissingSession".to_string() });
4839 seed_plain(&mut core, &dm, "$call:example.org", "@bob:example.org", "m.call.invite", serde_json::json!({ "call_id": "c1" }));
4840
4841 for id in ["$redacted:example.org", "$sealed:example.org", "$utd:example.org", "$call:example.org", "$missing:example.org"] {
4842 match forward(&mut core, &dm, id, &target) {
4843 Err(MessengerError::IntentRefused(reason)) => {
4844 assert!(reason.contains(id), "the refusal names the event {id}: {reason}");
4845 }
4846 other => panic!("forwarding {id} must be refused, got {other:?}"),
4847 }
4848 }
4849 match forward(&mut core, &room("!nowhere:example.org"), "$redacted:example.org", &target) {
4850 Err(MessengerError::IntentRefused(reason)) => assert!(reason.contains("$redacted:example.org"), "{reason}"),
4851 other => panic!("an unknown source room means a missing event, got {other:?}"),
4852 }
4853 match forward(&mut core, &dm, "$sealed:example.org", &room("!nowhere:example.org")) {
4854 Err(MessengerError::IntentRefused(reason)) => assert!(reason.contains("unknown"), "{reason}"),
4855 other => panic!("an unknown target room is refused, got {other:?}"),
4856 }
4857
4858 assert!(core.send_state.is_empty(), "a refused forward queues nothing");
4859 assert!(core.timeline(&target).is_some_and(|timeline| timeline.items().is_empty()), "and leaves no echo");
4860 }
4861}