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
134const SYNC_TIMEOUT_MS: u64 = 30_000;
136const LOAD_OLDER_PAGE_SIZE: u32 = 50;
139const TYPING_TIMEOUT_MS: u64 = 30_000;
142const UNKNOWN_SENDER_RETRY_CAPACITY: usize = 256;
147const TYPING_DEBOUNCE_MS: i64 = 4_000;
151const ROOM_KEY_SHARE_EVENT_TYPE: &str = "m.room.encrypted";
159
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
164pub struct Counters {
165 pub next_request_id: u64,
167 pub next_txn_id: u64,
172}
173
174fn fresh_txn_seed() -> u64 {
188 std::time::SystemTime::now()
189 .duration_since(std::time::UNIX_EPOCH)
190 .map(|d| d.as_micros() as u64)
191 .unwrap_or(0)
192}
193
194fn restore_counter(persisted_next: u64, max_used_seed: Option<u64>) -> u64 {
195 match max_used_seed {
196 Some(seed) => persisted_next.max(seed.saturating_add(1)),
197 None => persisted_next,
198 }
199}
200
201#[derive(Clone, Debug, PartialEq, Eq)]
205pub struct CoreConfig {
206 pub user_id: UserId,
208 pub device_id: DeviceId,
210 pub server_name: String,
218}
219
220#[derive(Default)]
229pub struct CoreSecrets {
230 pub store_seal_key: Option<Zeroizing<[u8; 32]>>,
238 pub backup_key: Option<Zeroizing<[u8; 32]>>,
242}
243
244#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247pub enum MessageKind {
248 Text,
250 Notice,
252 Emote,
254}
255
256impl MessageKind {
257 fn msgtype(self) -> &'static str {
258 match self {
259 MessageKind::Text => "m.text",
260 MessageKind::Notice => "m.notice",
261 MessageKind::Emote => "m.emote",
262 }
263 }
264}
265
266#[derive(Clone, Debug, PartialEq, Eq)]
277pub struct OutgoingMessage {
278 pub kind: MessageKind,
280 pub body: String,
283 pub reply_to: Option<EventId>,
285 pub edit_of: Option<EventId>,
288}
289
290#[derive(Clone, Debug, PartialEq, Eq)]
296pub enum CreateRoomKind {
297 Dm {
301 peer: UserId,
303 },
304 Group {
306 name: String,
308 invite: Vec<UserId>,
310 members_can_invite: bool,
314 },
315 Channel {
318 name: String,
320 topic: Option<String>,
322 },
323}
324
325#[derive(Clone, Debug, PartialEq)]
331pub enum MessengerCommand {
332 SendMessage {
338 room_id: RoomId,
340 message: OutgoingMessage,
342 txn_id: Option<TxnId>,
345 },
346 Forward {
372 from_room: RoomId,
374 event_id: EventId,
376 to_room: RoomId,
378 txn_id: Option<TxnId>,
380 },
381 React {
385 room_id: RoomId,
387 target: EventId,
389 key: String,
391 },
392 Redact {
394 room_id: RoomId,
396 target: EventId,
398 reason: Option<String>,
400 },
401 RetrySend {
407 room_id: RoomId,
409 txn_id: TxnId,
411 },
412 CreateRoom {
414 kind: CreateRoomKind,
416 },
417 JoinRoom {
420 room_id: RoomId,
422 },
423 LeaveRoom {
425 room_id: RoomId,
427 },
428 Invite {
430 room_id: RoomId,
432 user_id: UserId,
434 },
435 Kick {
440 room_id: RoomId,
442 user_id: UserId,
444 reason: Option<String>,
446 },
447 SetPowerLevel {
450 room_id: RoomId,
452 user_id: UserId,
454 level: i64,
456 },
457 SetTag {
459 room_id: RoomId,
461 tag: String,
463 order: Option<f64>,
465 },
466 RemoveTag {
468 room_id: RoomId,
470 tag: String,
472 },
473 MarkRead {
476 room_id: RoomId,
478 event_id: EventId,
480 },
481 SetTyping {
484 room_id: RoomId,
486 typing: bool,
488 },
489 RetryDecryption {
494 room_id: RoomId,
496 event_id: EventId,
498 },
499 LoadOlder {
504 room_id: RoomId,
506 },
507 SetAccountData {
513 event_type: String,
515 content: serde_json::Value,
517 },
518 SetRoomAccountData {
527 room_id: RoomId,
529 event_type: String,
531 content: serde_json::Value,
533 },
534 SearchPublicRooms {
539 term: String,
541 },
542 SearchUsers {
547 term: String,
549 },
550}
551
552#[derive(Clone, Debug, PartialEq, Eq)]
557pub enum MessengerEvent {
558 RoomsChanged,
561 TimelineChanged {
563 room_id: RoomId,
565 },
566 TypingChanged {
568 room_id: RoomId,
570 },
571 ReceiptsChanged {
573 room_id: RoomId,
575 },
576 UnreadChanged {
578 room_id: RoomId,
580 },
581 DeviceKeyChanged {
585 user_id: UserId,
587 device_id: DeviceId,
589 },
590 PublicRoomsChanged,
594 UserSearchChanged,
598}
599
600#[derive(Clone)]
605struct PendingDecryptItem {
606 session_id: String,
607 sender: UserId,
608 origin_server_ts: i64,
609 content: MegolmEncryptedContent,
610}
611
612#[derive(Deserialize)]
618struct RoomMessagesResponseBody {
619 #[serde(default)]
620 chunk: Vec<RawEvent>,
621 #[serde(default)]
622 end: Option<String>,
623}
624
625#[derive(Deserialize)]
627struct SendResponseBody {
628 event_id: EventId,
629}
630
631#[derive(Deserialize)]
633struct CreateRoomResponseBody {
634 room_id: RoomId,
635}
636
637#[derive(Clone, Debug, PartialEq)]
640pub struct PublicRoomsResultEntry {
641 pub room_id: RoomId,
643 pub name: Option<String>,
645 pub topic: Option<String>,
647 pub num_joined_members: u64,
649}
650
651#[derive(Deserialize)]
653struct PublicRoomsResponseBody {
654 #[serde(default)]
655 chunk: Vec<PublicRoomsChunkEntry>,
656}
657
658#[derive(Deserialize)]
659struct PublicRoomsChunkEntry {
660 room_id: RoomId,
661 #[serde(default)]
662 name: Option<String>,
663 #[serde(default)]
664 topic: Option<String>,
665 #[serde(default)]
666 num_joined_members: u64,
667}
668
669#[derive(Clone, Debug, PartialEq)]
672pub struct UserDirectoryResultEntry {
673 pub user_id: UserId,
675 pub display_name: Option<String>,
677}
678
679#[derive(Deserialize)]
682struct UserDirectorySearchResponseBody {
683 #[serde(default)]
684 results: Vec<UserDirectorySearchResultEntry>,
685}
686
687#[derive(Deserialize)]
688struct UserDirectorySearchResultEntry {
689 user_id: UserId,
690 #[serde(default)]
691 display_name: Option<String>,
692}
693
694#[derive(Clone)]
699enum SendPayload {
700 Message(OutgoingMessage),
703 Event {
707 event_type: String,
709 content: serde_json::Value,
711 },
712 Reaction {
714 target: EventId,
716 key: String,
718 },
719 Redaction {
721 target: EventId,
723 reason: Option<String>,
725 },
726}
727
728struct RoomEventWire {
732 event_type: String,
733 content: serde_json::Value,
734 relates_to: Option<RelatesTo>,
737}
738
739impl SendPayload {
740 fn room_event(&self) -> Option<RoomEventWire> {
743 match self {
744 SendPayload::Message(message) => Some(RoomEventWire {
745 event_type: "m.room.message".to_string(),
746 content: message_wire_content(message),
747 relates_to: message_relates_to(message),
748 }),
749 SendPayload::Event { event_type, content } => {
750 Some(RoomEventWire { event_type: event_type.clone(), content: content.clone(), relates_to: None })
751 }
752 SendPayload::Reaction { .. } | SendPayload::Redaction { .. } => None,
753 }
754 }
755
756 fn is_room_event(&self) -> bool {
757 matches!(self, SendPayload::Message(_) | SendPayload::Event { .. })
758 }
759}
760
761#[derive(Clone)]
767enum WireSend {
768 Event {
770 event_type: String,
773 content: serde_json::Value,
775 },
776 Redact {
778 target: EventId,
780 reason: Option<String>,
782 },
783}
784
785#[derive(Clone)]
790enum SendPhase {
791 Queued,
795 AwaitingKeysQuery,
801 AwaitingKeysClaim {
804 devices: Vec<StoredDevice>,
808 },
809 AwaitingKeyShare {
814 pending: BTreeSet<RequestId>,
816 },
817 AwaitingSend,
819 Failed {
824 errcode: String,
826 },
827}
828
829#[derive(Clone)]
835struct PendingSend {
836 room_id: RoomId,
839 payload: SendPayload,
841 phase: SendPhase,
843 wire: Option<WireSend>,
845}
846
847type AccountDataKey = (Option<RoomId>, String);
850
851struct AccountDataGuard {
863 pending: u32,
865 server_value: Option<serde_json::Value>,
869}
870
871fn account_data_key_from_path(path: &str) -> Option<AccountDataKey> {
874 let segments: Vec<&str> = path.split('/').collect();
875 let n = segments.len();
876 if n < 4 || segments[n - 2] != "account_data" {
877 return None;
878 }
879 let event_type = percent_decode_segment(segments[n - 1])?;
880 if segments[n - 4] == "user" {
881 return Some((None, event_type));
882 }
883 if n >= 6 && segments[n - 4] == "rooms" && segments[n - 6] == "user" {
884 let room_id = RoomId::parse(percent_decode_segment(segments[n - 3])?).ok()?;
885 return Some((Some(room_id), event_type));
886 }
887 None
888}
889
890pub struct MessengerCore<C: RecordCodec> {
892 config: CoreConfig,
893 store: Store<C>,
894 account: OlmAccountState,
895 xsign: Option<CrossSigningState>,
897 xsign_inflight: bool,
898 xsign_attempts: u8,
899 outgoing: OutgoingQueue,
900 counters: Counters,
901 jitter: Box<dyn Jitter>,
902
903 rooms: BTreeMap<RoomId, RoomState>,
904 timelines: BTreeMap<RoomId, Timeline>,
905 direct_account_data: Option<DirectContent>,
906 global_account_data: BTreeMap<String, serde_json::Value>,
907 room_account_data: BTreeMap<RoomId, BTreeMap<String, serde_json::Value>>,
908 typing: BTreeMap<RoomId, Vec<UserId>>,
909 receipts: BTreeMap<RoomId, ReceiptContent>,
910
911 public_rooms_result: Vec<PublicRoomsResultEntry>,
914 latest_public_rooms_request: Option<RequestId>,
919 user_search_result: Vec<UserDirectoryResultEntry>,
922 latest_user_search_request: Option<RequestId>,
925
926 account_data_guard: BTreeMap<AccountDataKey, AccountDataGuard>,
930 account_data_write_keys: BTreeMap<RequestId, AccountDataKey>,
932
933 pending_decrypt: BTreeMap<RoomId, BTreeMap<EventId, PendingDecryptItem>>,
934 key_requests_sent: BTreeSet<(RoomId, String)>,
938 history_claims: BTreeMap<RequestId, Vec<StoredDevice>>,
941 history_claim_tried: BTreeSet<(UserId, DeviceId)>,
943 withheld_sessions: BTreeSet<(RoomId, String)>,
945 key_requests_answered: BTreeSet<(UserId, DeviceId, String)>,
947 unknown_sender_retry: VecDeque<ToDeviceEvent>,
948
949 typing_debounce: BTreeMap<RoomId, i64>,
954 pending_sends: BTreeMap<RoomId, VecDeque<TxnId>>,
960 send_state: BTreeMap<TxnId, PendingSend>,
965 send_request_owner: BTreeMap<RequestId, TxnId>,
969 pending_create_room: BTreeMap<RequestId, CreateRoomKind>,
973
974 pending_kinds: BTreeMap<RequestId, OutgoingRequestKind>,
980 pending_room_messages: BTreeMap<RequestId, RoomId>,
983 sync_request_id: Option<RequestId>,
986 sync_caught_up: bool,
991 pending_flush_batches: VecDeque<FlushBatch>,
998
999 events: Vec<MessengerEvent>,
1000 change_counter: u64,
1001 ingest_error: Option<String>,
1006}
1007
1008impl<C: RecordCodec> MessengerCore<C> {
1009 pub fn open(
1022 records: impl IntoIterator<Item = SealedRecord>,
1023 codec: C,
1024 config: CoreConfig,
1025 secrets: CoreSecrets,
1026 _now_ms: i64,
1027 jitter: Box<dyn Jitter>,
1028 ) -> Result<Self, MessengerError> {
1029 let _ = secrets;
1033 let mut store = Store::load(records, codec, config.device_id.clone())?;
1034 let mut outgoing = OutgoingQueue::load(&store)?;
1035 let account = OlmAccountState::load_or_create(&mut store)?;
1036
1037 let mut rooms = BTreeMap::new();
1038 let mut timelines = BTreeMap::new();
1039 for room_id in store.room_ids()? {
1040 if let Some(bytes) = store.room_state(&room_id)? {
1041 let state: RoomState = serde_json::from_slice(bytes)
1042 .map_err(|source| MessengerError::Crypto(format!("decode room state for {room_id}: {source}")))?;
1043 rooms.insert(room_id.clone(), state);
1044 timelines.insert(room_id, Timeline::new());
1045 }
1046 }
1047
1048 let pending_requests = store.pending_requests()?;
1049 let max_pending_request_seed = pending_requests.iter().filter_map(|req| req.request.id.as_seed()).max();
1050 outgoing.discard_kind(&mut store, OutgoingRequestKind::Sync)?;
1057 let persisted_counters = store.counters()?.unwrap_or_default();
1058 let mut counters = Counters {
1059 next_request_id: restore_counter(persisted_counters.next_request_id, max_pending_request_seed),
1060 next_txn_id: restore_counter(persisted_counters.next_txn_id, None),
1061 };
1062 if counters != persisted_counters {
1063 store.save_counters(counters)?;
1069 }
1070 counters.next_txn_id = counters.next_txn_id.max(fresh_txn_seed());
1076
1077 let mut core = Self {
1078 config,
1079 store,
1080 account,
1081 outgoing,
1082 counters,
1083 jitter,
1084 rooms,
1085 timelines,
1086 direct_account_data: None,
1087 global_account_data: BTreeMap::new(),
1088 room_account_data: BTreeMap::new(),
1089 typing: BTreeMap::new(),
1090 receipts: BTreeMap::new(),
1091 public_rooms_result: Vec::new(),
1092 latest_public_rooms_request: None,
1093 user_search_result: Vec::new(),
1094 latest_user_search_request: None,
1095 account_data_guard: BTreeMap::new(),
1096 account_data_write_keys: BTreeMap::new(),
1097 pending_decrypt: BTreeMap::new(),
1098 key_requests_sent: BTreeSet::new(),
1099 history_claims: BTreeMap::new(),
1100 history_claim_tried: BTreeSet::new(),
1101 withheld_sessions: BTreeSet::new(),
1102 key_requests_answered: BTreeSet::new(),
1103 unknown_sender_retry: VecDeque::new(),
1104 typing_debounce: BTreeMap::new(),
1105 pending_sends: BTreeMap::new(),
1106 send_state: BTreeMap::new(),
1107 send_request_owner: BTreeMap::new(),
1108 pending_create_room: BTreeMap::new(),
1109 pending_kinds: BTreeMap::new(),
1110 pending_room_messages: BTreeMap::new(),
1111 sync_request_id: None,
1112 sync_caught_up: false,
1113 pending_flush_batches: VecDeque::new(),
1114 events: Vec::new(),
1115 change_counter: 0,
1116 ingest_error: None,
1117 xsign: None,
1118 xsign_inflight: false,
1119 xsign_attempts: 0,
1120 };
1121 core.rebuild_account_data_guard(&pending_requests);
1122 Ok(core)
1123 }
1124
1125 fn rebuild_account_data_guard(&mut self, pending_requests: &[PendingRequest]) {
1133 let mut writes: Vec<&PendingRequest> = pending_requests
1134 .iter()
1135 .filter(|record| matches!(record.request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData))
1136 .collect();
1137 writes.sort_by(|a, b| a.request.id.cmp(&b.request.id));
1138 for record in writes {
1139 let Some(key) = account_data_key_from_path(&record.request.path) else { continue };
1140 let Some(body) = record.request.body.clone() else { continue };
1141 let guard = self.account_data_guard.entry(key.clone()).or_insert(AccountDataGuard { pending: 0, server_value: None });
1142 guard.pending += 1;
1143 self.store_account_data_cache(&key, Some(body));
1144 self.account_data_write_keys.insert(record.request.id.clone(), key);
1145 }
1146 }
1147
1148 fn next_request_id(&mut self) -> Result<RequestId, MessengerError> {
1152 let seed = self.counters.next_request_id;
1153 self.counters.next_request_id = seed.saturating_add(1);
1154 self.store.save_counters(self.counters)?;
1155 Ok(RequestId::next(seed))
1156 }
1157
1158 fn next_txn_id(&mut self) -> Result<TxnId, MessengerError> {
1163 let seed = self.counters.next_txn_id;
1164 self.counters.next_txn_id = seed.saturating_add(1);
1165 self.store.save_counters(self.counters)?;
1166 Ok(TxnId::new(seed))
1167 }
1168
1169 fn enqueue_request(&mut self, request: OutgoingRequest, lane: Lane) -> Result<(), MessengerError> {
1184 let id = request.id.clone();
1185 let kind = request.kind;
1186 if let Some(batch) = self.outgoing.enqueue(&mut self.store, request, lane)? {
1187 self.pending_flush_batches.push_back(batch);
1188 }
1189 self.pending_kinds.insert(id, kind);
1190 Ok(())
1191 }
1192
1193 fn emit(&mut self, event: MessengerEvent) {
1194 self.change_counter = self.change_counter.wrapping_add(1);
1195 self.events.push(event);
1196 }
1197
1198 fn drain_events(&mut self) -> Vec<MessengerEvent> {
1199 std::mem::take(&mut self.events)
1200 }
1201
1202 fn start_new_send(
1218 &mut self,
1219 room_id: RoomId,
1220 payload: SendPayload,
1221 caller_txn_id: Option<TxnId>,
1222 now_ms: i64,
1223 ) -> Result<(), MessengerError> {
1224 let txn_id = match caller_txn_id {
1225 Some(txn_id) => txn_id,
1226 None => self.next_txn_id()?,
1227 };
1228
1229 if let Some(wire) = payload.room_event() {
1230 if self.typing_debounce.remove(&room_id).is_some() {
1231 if let Ok(request_id) = self.next_request_id() {
1232 let request = OutgoingRequest::typing(request_id, &room_id, &self.config.user_id, false, None);
1233 let _ = self.enqueue_request(request, Lane::Other);
1234 }
1235 }
1236 let content = match &payload {
1237 SendPayload::Message(message) => message_local_echo_content(message),
1238 _ => interpret_content(&wire.event_type, None, &wire.content),
1239 };
1240 let timeline = self.timelines.entry(room_id.clone()).or_default();
1241 timeline.push_local_echo(
1242 txn_id.clone(),
1243 self.config.user_id.clone(),
1244 now_ms,
1245 &wire.event_type,
1246 content,
1247 wire.content,
1248 );
1249 timeline.mark_send_state(&txn_id, SendState::Sending);
1250 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1251 }
1252
1253 self.send_state.insert(txn_id.clone(), PendingSend { room_id: room_id.clone(), payload, phase: SendPhase::Queued, wire: None });
1254 let queue = self.pending_sends.entry(room_id).or_default();
1255 let was_empty = queue.is_empty();
1256 queue.push_back(txn_id.clone());
1257 if was_empty {
1258 self.start_send_pipeline(&txn_id, now_ms)?;
1259 }
1260 Ok(())
1261 }
1262
1263 fn retry_send(&mut self, room_id: &RoomId, txn_id: &TxnId, now_ms: i64) -> Result<(), MessengerError> {
1268 let Some(pending) = self.send_state.get(txn_id) else { return Ok(()) };
1269 if pending.room_id != *room_id || !matches!(pending.phase, SendPhase::Failed { .. }) {
1270 return Ok(());
1271 }
1272 self.set_send_phase(txn_id, SendPhase::Queued);
1273 let queue = self.pending_sends.entry(room_id.clone()).or_default();
1274 let was_empty = queue.is_empty();
1275 queue.push_back(txn_id.clone());
1276 if was_empty {
1277 self.start_send_pipeline(txn_id, now_ms)?;
1278 }
1279 Ok(())
1280 }
1281
1282 fn set_send_phase(&mut self, txn_id: &TxnId, phase: SendPhase) {
1283 if let Some(entry) = self.send_state.get_mut(txn_id) {
1284 entry.phase = phase;
1285 }
1286 }
1287
1288 fn start_send_pipeline(&mut self, txn_id: &TxnId, now_ms: i64) -> Result<(), MessengerError> {
1297 let Some(pending) = self.send_state.get(txn_id) else { return Ok(()) };
1298 let room_id = pending.room_id.clone();
1299
1300 if pending.wire.is_some() {
1301 return self.begin_awaiting_send(txn_id, &room_id);
1302 }
1303
1304 let is_room_event = pending.payload.is_room_event();
1305 let encrypted_room = self.rooms.get(&room_id).is_some_and(|room| room.encryption.is_some());
1306
1307 if is_room_event && encrypted_room {
1308 self.begin_key_pipeline(txn_id, &room_id, now_ms)
1309 } else {
1310 self.build_plaintext_wire_body(txn_id);
1311 self.begin_awaiting_send(txn_id, &room_id)
1312 }
1313 }
1314
1315 fn encrypted_room_member_ids(&self, room_id: &RoomId) -> BTreeSet<UserId> {
1320 let mut ids: BTreeSet<UserId> = self
1321 .rooms
1322 .get(room_id)
1323 .map(|state| {
1324 state
1325 .members
1326 .iter()
1327 .filter(|(_, member)| matches!(member.membership, Membership::Join | Membership::Invite))
1328 .map(|(user_id, _)| user_id.clone())
1329 .collect()
1330 })
1331 .unwrap_or_default();
1332 ids.insert(self.config.user_id.clone());
1333 ids
1334 }
1335
1336 fn mark_outdated_or_untracked(&mut self, member_ids: &BTreeSet<UserId>) -> Result<Vec<UserId>, MessengerError> {
1342 let tracked: BTreeMap<UserId, bool> = self.store.tracked_users()?.into_iter().collect();
1343 let mut outdated = Vec::new();
1344 let mut newly_tracked = Vec::new();
1345 for user_id in member_ids {
1346 match tracked.get(user_id) {
1347 Some(true) => outdated.push(user_id.clone()),
1348 Some(false) => {}
1349 None => newly_tracked.push(user_id.clone()),
1350 }
1351 }
1352 if !newly_tracked.is_empty() {
1353 DeviceTracker::on_device_lists(&mut self.store, &newly_tracked, &[])?;
1354 outdated.extend(newly_tracked);
1355 }
1356 Ok(outdated)
1357 }
1358
1359 fn known_devices_for(&self, member_ids: &BTreeSet<UserId>) -> Result<Vec<StoredDevice>, MessengerError> {
1362 let mut all = Vec::new();
1363 for user_id in member_ids {
1364 all.extend(DeviceTracker::devices_for_user(&self.store, user_id)?);
1365 }
1366 Ok(all)
1367 }
1368
1369 fn candidate_recipient_devices(&self, member_ids: &BTreeSet<UserId>, known_devices: &[StoredDevice]) -> Vec<StoredDevice> {
1373 known_devices
1374 .iter()
1375 .filter(|device| member_ids.contains(&device.user_id) && !device.blocked)
1376 .filter(|device| !(device.user_id == self.config.user_id && device.device_id == self.config.device_id))
1377 .cloned()
1378 .collect()
1379 }
1380
1381 fn begin_key_pipeline(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1386 let members = self.encrypted_room_member_ids(room_id);
1387 let outdated = self.mark_outdated_or_untracked(&members)?;
1388 if !outdated.is_empty() {
1389 let request_id = self.next_request_id()?;
1390 if let Some(request) = DeviceTracker::keys_query_request(&self.store, request_id.clone())? {
1391 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1392 self.enqueue_request(request, Lane::Other)?;
1393 self.set_send_phase(txn_id, SendPhase::AwaitingKeysQuery);
1394 return Ok(());
1395 }
1396 }
1397 self.begin_keys_claim_or_share(txn_id, room_id, now_ms)
1398 }
1399
1400 fn begin_keys_claim_or_share(&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 known_devices = self.known_devices_for(&members)?;
1406 let candidates = self.candidate_recipient_devices(&members, &known_devices);
1407 let missing: Vec<StoredDevice> =
1408 OlmSessionManager::sessions_missing_for(&self.store, &candidates)?.into_iter().cloned().collect();
1409 if !missing.is_empty() {
1410 let request_id = self.next_request_id()?;
1411 let refs: Vec<&StoredDevice> = missing.iter().collect();
1412 if let Some(request) = OlmSessionManager::keys_claim_request(request_id.clone(), &refs) {
1413 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1414 self.enqueue_request(request, Lane::Other)?;
1415 self.set_send_phase(txn_id, SendPhase::AwaitingKeysClaim { devices: missing });
1416 return Ok(());
1417 }
1418 }
1419 self.begin_share_or_encrypt(txn_id, room_id, now_ms)
1420 }
1421
1422 fn begin_share_or_encrypt(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1435 let Some(encryption) = self.rooms.get(room_id).and_then(|room| room.encryption.clone()) else {
1436 self.build_plaintext_wire_body(txn_id);
1437 return self.begin_awaiting_send(txn_id, room_id);
1438 };
1439 let members = self.encrypted_room_member_ids(room_id);
1440 let known_devices = self.known_devices_for(&members)?;
1441 let update = GroupSessionManager::ensure_outbound_session(
1442 &mut self.store,
1443 room_id,
1444 &self.config.user_id,
1445 &self.config.device_id,
1446 &encryption,
1447 &members,
1448 &known_devices,
1449 now_ms,
1450 )?;
1451 let own_keys = self.account.identity_keys();
1452 GroupSessionManager::adopt_own_outbound_session(
1453 &mut self.store,
1454 room_id,
1455 &self.config.user_id,
1456 own_keys.curve25519,
1457 own_keys.ed25519,
1458 &update,
1459 )?;
1460
1461 if let Some(room_key_content) = update.room_key_content.clone() {
1462 let missing_ids: BTreeSet<(UserId, DeviceId)> = OlmSessionManager::sessions_missing_for(&self.store, &update.new_recipients)?
1463 .into_iter()
1464 .map(|device| (device.user_id.clone(), device.device_id.clone()))
1465 .collect();
1466 let sessioned: Vec<StoredDevice> = update
1467 .new_recipients
1468 .into_iter()
1469 .filter(|device| !missing_ids.contains(&(device.user_id.clone(), device.device_id.clone())))
1470 .collect();
1471
1472 let mut pending_ids = BTreeSet::new();
1473 for chunk in GroupSessionManager::chunk_recipients_for_send_to_device(&sessioned) {
1474 let body = GroupSessionManager::build_room_key_send_to_device_body(
1475 &mut self.store,
1476 &self.account,
1477 &self.config.user_id,
1478 &self.config.device_id,
1479 chunk,
1480 &room_key_content,
1481 )?;
1482 let request_id = self.next_request_id()?;
1483 let share_txn = self.next_txn_id()?;
1484 let request = OutgoingRequest::send_to_device(request_id.clone(), ROOM_KEY_SHARE_EVENT_TYPE, &share_txn, body);
1485 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1486 self.enqueue_request(request, Lane::ToDevice)?;
1487 pending_ids.insert(request_id);
1488 }
1489 if !pending_ids.is_empty() {
1490 self.set_send_phase(txn_id, SendPhase::AwaitingKeyShare { pending: pending_ids });
1491 return Ok(());
1492 }
1493 }
1494
1495 self.finish_encrypt_and_send(txn_id, room_id)?;
1496 self.begin_awaiting_send(txn_id, room_id)
1497 }
1498
1499 fn finish_encrypt_and_send(&mut self, txn_id: &TxnId, room_id: &RoomId) -> Result<(), MessengerError> {
1506 let Some(wire) = self.send_state.get(txn_id).and_then(|pending| pending.payload.room_event()) else {
1507 return Ok(());
1508 };
1509 let sender_curve = self.account.identity_keys().curve25519.to_base64();
1510 let encrypted = GroupSessionManager::encrypt_event(
1511 &mut self.store,
1512 room_id,
1513 &sender_curve,
1514 &self.config.device_id,
1515 &wire.event_type,
1516 wire.content,
1517 wire.relates_to,
1518 )?;
1519 let wire_content = serde_json::to_value(RoomEncryptedContent::Megolm(encrypted))?;
1520 if let Some(entry) = self.send_state.get_mut(txn_id) {
1521 entry.wire = Some(WireSend::Event { event_type: "m.room.encrypted".to_string(), content: wire_content });
1522 }
1523 Ok(())
1524 }
1525
1526 fn build_plaintext_wire_body(&mut self, txn_id: &TxnId) {
1532 let Some(pending) = self.send_state.get(txn_id) else { return };
1533 let wire = match &pending.payload {
1534 SendPayload::Message(_) | SendPayload::Event { .. } => match pending.payload.room_event() {
1535 Some(room_event) => WireSend::Event { event_type: room_event.event_type, content: room_event.content },
1536 None => return,
1537 },
1538 SendPayload::Reaction { target, key } => WireSend::Event {
1539 event_type: "m.reaction".to_string(),
1540 content: serde_json::json!({
1541 "m.relates_to": { "rel_type": "m.annotation", "event_id": target.as_str(), "key": key }
1542 }),
1543 },
1544 SendPayload::Redaction { target, reason } => WireSend::Redact { target: target.clone(), reason: reason.clone() },
1545 };
1546 if let Some(entry) = self.send_state.get_mut(txn_id) {
1547 entry.wire = Some(wire);
1548 }
1549 }
1550
1551 fn begin_awaiting_send(&mut self, txn_id: &TxnId, room_id: &RoomId) -> Result<(), MessengerError> {
1556 let Some(wire) = self.send_state.get(txn_id).and_then(|pending| pending.wire.clone()) else { return Ok(()) };
1557 let request_id = self.next_request_id()?;
1558 let request = match &wire {
1559 WireSend::Event { event_type, content } => {
1560 OutgoingRequest::room_send(request_id.clone(), room_id, event_type, txn_id, content.clone())
1561 }
1562 WireSend::Redact { target, reason } => {
1563 let body = match reason {
1564 Some(reason) => serde_json::json!({ "reason": reason }),
1565 None => serde_json::json!({}),
1566 };
1567 OutgoingRequest::room_redact(request_id.clone(), room_id, target, txn_id, body)
1568 }
1569 };
1570 self.send_request_owner.insert(request_id, txn_id.clone());
1571 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
1572 self.set_send_phase(txn_id, SendPhase::AwaitingSend);
1573 Ok(())
1574 }
1575
1576 fn advance_send_on_success(
1582 &mut self,
1583 txn_id: &TxnId,
1584 request_id: &RequestId,
1585 kind: Option<OutgoingRequestKind>,
1586 response: &HttpResponseDescriptor,
1587 now_ms: i64,
1588 ) {
1589 match kind {
1590 Some(OutgoingRequestKind::KeysQuery) => {
1591 let Some(room_id) = self.send_state.get(txn_id).map(|pending| pending.room_id.clone()) else { return };
1592 let _ = self.begin_keys_claim_or_share(txn_id, &room_id, now_ms);
1593 }
1594 Some(OutgoingRequestKind::KeysClaim) => {
1595 let Some(pending) = self.send_state.get(txn_id) else { return };
1596 let room_id = pending.room_id.clone();
1597 if let SendPhase::AwaitingKeysClaim { devices, .. } = &pending.phase {
1598 let devices = devices.clone();
1599 let refs: Vec<&StoredDevice> = devices.iter().collect();
1600 let _ = OlmSessionManager::on_keys_claim_response(&mut self.store, &self.account, &refs, &response.body);
1601 }
1602 let _ = self.begin_share_or_encrypt(txn_id, &room_id, now_ms);
1603 }
1604 Some(OutgoingRequestKind::SendToDevice) => {
1605 let mut remaining = None;
1606 if let Some(entry) = self.send_state.get_mut(txn_id) {
1607 if let SendPhase::AwaitingKeyShare { pending } = &mut entry.phase {
1608 pending.remove(request_id);
1609 remaining = Some(pending.len());
1610 }
1611 }
1612 if remaining == Some(0) {
1613 let Some(room_id) = self.send_state.get(txn_id).map(|pending| pending.room_id.clone()) else { return };
1614 let _ = self.finish_encrypt_and_send(txn_id, &room_id);
1615 let _ = self.begin_awaiting_send(txn_id, &room_id);
1616 }
1617 }
1618 Some(OutgoingRequestKind::RoomSend) | Some(OutgoingRequestKind::RoomRedact) => {
1619 self.complete_send_success(txn_id, response, now_ms);
1620 }
1621 _ => {}
1622 }
1623 }
1624
1625 fn complete_send_success(&mut self, txn_id: &TxnId, response: &HttpResponseDescriptor, now_ms: i64) {
1631 let Some(pending) = self.send_state.get(txn_id) else { return };
1632 let room_id = pending.room_id.clone();
1633 if pending.payload.is_room_event() {
1634 if let Ok(body) = serde_json::from_slice::<SendResponseBody>(&response.body) {
1635 if let Some(timeline) = self.timelines.get_mut(&room_id) {
1636 timeline.set_echo_event_id(txn_id, body.event_id);
1637 }
1638 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1639 }
1640 }
1641 self.send_state.remove(txn_id);
1642 self.finish_active_send(&room_id, txn_id, now_ms);
1643 }
1644
1645 fn fail_send(&mut self, txn_id: &TxnId, errcode: String, now_ms: i64) {
1651 let Some(pending) = self.send_state.get(txn_id) else { return };
1652 let room_id = pending.room_id.clone();
1653 if pending.payload.is_room_event() {
1654 if let Some(timeline) = self.timelines.get_mut(&room_id) {
1655 timeline.mark_send_state(txn_id, SendState::Failed { reason: errcode.clone() });
1656 }
1657 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1658 }
1659 self.set_send_phase(txn_id, SendPhase::Failed { errcode });
1660 self.finish_active_send(&room_id, txn_id, now_ms);
1661 }
1662
1663 fn finish_active_send(&mut self, room_id: &RoomId, txn_id: &TxnId, now_ms: i64) {
1669 if let Some(queue) = self.pending_sends.get_mut(room_id) {
1670 if queue.front() == Some(txn_id) {
1671 queue.pop_front();
1672 } else {
1673 queue.retain(|id| id != txn_id);
1674 }
1675 }
1676 let next = self.pending_sends.get(room_id).and_then(|queue| queue.front().cloned());
1677 if let Some(next_txn) = next {
1678 let _ = self.start_send_pipeline(&next_txn, now_ms);
1679 }
1680 }
1681
1682 pub fn releasable_requests(&mut self, now_ms: i64) -> Vec<OutgoingRequest> {
1702 self.ensure_sync_enqueued();
1703 self.outgoing.releasable(&self.store, now_ms)
1704 }
1705
1706 fn ensure_sync_enqueued(&mut self) {
1707 if self.sync_request_id.is_some() {
1708 return;
1709 }
1710 let Ok(request_id) = self.next_request_id() else { return };
1711 let since = self.store.sync_token().ok().flatten().map(str::to_string);
1712 let timeout_ms = if since.is_some() && self.sync_caught_up { SYNC_TIMEOUT_MS } else { 0 };
1722 let request = OutgoingRequest::sync(request_id.clone(), since.as_deref(), Some(timeout_ms));
1723 if self.enqueue_request(request, Lane::Sync).is_ok() {
1724 self.sync_request_id = Some(request_id);
1725 }
1726 }
1727
1728 pub fn on_response(&mut self, request_id: RequestId, resp: HttpResponseDescriptor, now_ms: i64) -> Vec<MessengerEvent> {
1734 let kind = self.pending_kinds.get(&request_id).copied();
1735 let room_messages_room = self.pending_room_messages.get(&request_id).cloned();
1736 let create_room_kind = self.pending_create_room.get(&request_id).cloned();
1737 let send_owner = self.send_request_owner.get(&request_id).cloned();
1738
1739 let outcome = self.outgoing.on_response(&mut self.store, &request_id, resp, now_ms, self.jitter.as_mut());
1740 let (is_terminal, terminal_response, failed_errcode) = match outcome {
1741 Ok(Some(ResponseOutcome::Done(response))) => (true, Some(response), None),
1742 Ok(Some(ResponseOutcome::Failed { errcode, .. })) => (true, None, Some(errcode)),
1743 _ => (false, None, None),
1744 };
1745
1746 if is_terminal {
1747 self.pending_kinds.remove(&request_id);
1748 if matches!(kind, Some(OutgoingRequestKind::Sync)) {
1749 self.sync_request_id = None;
1750 if terminal_response.is_some() {
1751 self.sync_caught_up = true;
1752 }
1753 }
1754 if room_messages_room.is_some() {
1755 self.pending_room_messages.remove(&request_id);
1756 }
1757 self.pending_create_room.remove(&request_id);
1758 self.send_request_owner.remove(&request_id);
1759 if let Some(key) = self.account_data_write_keys.remove(&request_id) {
1760 self.settle_account_data_write(&key, terminal_response.is_some());
1761 }
1762 }
1763
1764 if let Some(response) = &terminal_response {
1765 if let Err(error) = self.handle_terminal_success(&request_id, kind, room_messages_room, create_room_kind, response) {
1772 self.ingest_error = Some(match kind {
1773 Some(kind) => format!("{kind:?} response not ingested: {error}"),
1774 None => format!("response not ingested: {error}"),
1775 });
1776 }
1777 }
1778
1779 if is_terminal
1780 && terminal_response.is_none()
1781 && matches!(kind, Some(OutgoingRequestKind::SigningKeysUpload) | Some(OutgoingRequestKind::SignaturesUpload))
1782 {
1783 self.xsign_inflight = false;
1784 }
1785
1786 if let Some(txn_id) = send_owner {
1787 if let Some(response) = &terminal_response {
1788 self.advance_send_on_success(&txn_id, &request_id, kind, response, now_ms);
1789 } else if let Some(errcode) = failed_errcode.clone() {
1790 self.fail_send(&txn_id, errcode, now_ms);
1791 }
1792 }
1793
1794 if matches!(kind, Some(OutgoingRequestKind::KeysUpload)) {
1798 if let Some(errcode) = failed_errcode {
1799 self.ingest_error = Some(format!("keys/upload failed: {errcode}"));
1800 }
1801 }
1802
1803 self.drain_events()
1804 }
1805
1806 pub fn on_transport_error(&mut self, request_id: &RequestId, now_ms: i64) {
1811 let _ = self.outgoing.on_transport_error(request_id, now_ms, self.jitter.as_mut());
1812 }
1813
1814 fn handle_terminal_success(
1815 &mut self,
1816 request_id: &RequestId,
1817 kind: Option<OutgoingRequestKind>,
1818 room_messages_room: Option<RoomId>,
1819 create_room_kind: Option<CreateRoomKind>,
1820 response: &HttpResponseDescriptor,
1821 ) -> Result<(), MessengerError> {
1822 match kind {
1823 Some(OutgoingRequestKind::Sync) => {
1824 let parsed = parse_sync_response(&response.body)?;
1825 self.ingest_sync(&parsed)?;
1826 }
1827 Some(OutgoingRequestKind::KeysUpload) => {
1828 self.account.on_keys_upload_response(&mut self.store)?;
1829 }
1830 Some(OutgoingRequestKind::KeysClaim) if self.history_claims.contains_key(request_id) => {
1831 if let Some(devices) = self.history_claims.remove(request_id) {
1832 let refs: Vec<&StoredDevice> = devices.iter().collect();
1833 let _ = OlmSessionManager::on_keys_claim_response(&mut self.store, &self.account, &refs, &response.body);
1834 }
1835 self.retry_missing_room_keys();
1836 }
1837 Some(OutgoingRequestKind::SigningKeysUpload) => {
1838 self.xsign_inflight = false;
1839 if let Some(state) = self.xsign.as_mut() {
1840 state.uploaded = true;
1841 let snapshot = state.clone();
1842 snapshot.save(&mut self.store)?;
1843 }
1844 self.maybe_enqueue_cross_signing()?;
1845 }
1846 Some(OutgoingRequestKind::SignaturesUpload) => {
1847 self.xsign_inflight = false;
1848 let failures_empty = serde_json::from_slice::<serde_json::Value>(&response.body)
1849 .ok()
1850 .and_then(|v| v.get("failures").and_then(|f| f.as_object()).map(|f| f.is_empty()))
1851 .unwrap_or(true);
1852 if failures_empty {
1853 if let Some(state) = self.xsign.as_mut() {
1854 state.device_signed = true;
1855 let snapshot = state.clone();
1856 snapshot.save(&mut self.store)?;
1857 }
1858 }
1859 }
1860 Some(OutgoingRequestKind::KeysQuery) => {
1861 let outcome = DeviceTracker::on_keys_query_response(&mut self.store, &response.body)?;
1862 if !outcome.key_changes.is_empty() {
1863 let room_ids: Vec<RoomId> = self.rooms.keys().cloned().collect();
1864 for change in &outcome.key_changes {
1865 let _ = GroupSessionManager::forget_shared_device(
1866 &mut self.store,
1867 &room_ids,
1868 &change.user_id,
1869 &change.device_id,
1870 );
1871 self.emit(MessengerEvent::DeviceKeyChanged {
1872 user_id: change.user_id.clone(),
1873 device_id: change.device_id.clone(),
1874 });
1875 }
1876 }
1877 self.retry_unknown_sender_queue()?;
1878 }
1879 Some(OutgoingRequestKind::RoomMessages) => {
1880 if let Some(room_id) = room_messages_room {
1881 self.ingest_room_messages_response(&room_id, &response.body)?;
1882 }
1883 }
1884 Some(OutgoingRequestKind::CreateRoom) => {
1885 if let Some(kind) = create_room_kind {
1886 self.on_create_room_response(kind, response)?;
1887 }
1888 }
1889 Some(OutgoingRequestKind::PublicRooms) if self.latest_public_rooms_request.as_ref() == Some(request_id) => {
1894 let body: PublicRoomsResponseBody = serde_json::from_slice(&response.body)?;
1895 self.public_rooms_result = body
1896 .chunk
1897 .into_iter()
1898 .map(|entry| PublicRoomsResultEntry {
1899 room_id: entry.room_id,
1900 name: entry.name,
1901 topic: entry.topic,
1902 num_joined_members: entry.num_joined_members,
1903 })
1904 .collect();
1905 self.emit(MessengerEvent::PublicRoomsChanged);
1906 }
1907 Some(OutgoingRequestKind::UserDirectorySearch) if self.latest_user_search_request.as_ref() == Some(request_id) => {
1909 let body: UserDirectorySearchResponseBody = serde_json::from_slice(&response.body)?;
1910 self.user_search_result = body
1911 .results
1912 .into_iter()
1913 .map(|entry| UserDirectoryResultEntry { user_id: entry.user_id, display_name: entry.display_name })
1914 .collect();
1915 self.emit(MessengerEvent::UserSearchChanged);
1916 }
1917 _ => {}
1918 }
1919 Ok(())
1920 }
1921
1922 fn on_create_room_response(&mut self, kind: CreateRoomKind, response: &HttpResponseDescriptor) -> Result<(), MessengerError> {
1928 let CreateRoomKind::Dm { peer } = kind else { return Ok(()) };
1929 let body: CreateRoomResponseBody = serde_json::from_slice(&response.body)?;
1930 self.merge_direct_account_data(body.room_id, peer)
1931 }
1932
1933 fn merge_direct_account_data(&mut self, room_id: RoomId, peer: UserId) -> Result<(), MessengerError> {
1945 let mut direct = self.direct_account_data.clone().unwrap_or_default();
1946 let rooms_for_peer = direct.0.entry(peer).or_default();
1947 if !rooms_for_peer.contains(&room_id) {
1948 rooms_for_peer.push(room_id);
1949 }
1950 let value = serde_json::to_value(&direct)?;
1951 self.write_account_data(None, "m.direct".to_string(), value)
1952 }
1953
1954 fn cached_account_data(&self, key: &AccountDataKey) -> Option<&serde_json::Value> {
1955 match &key.0 {
1956 Some(room_id) => self.room_account_data(room_id, &key.1),
1957 None => self.global_account_data(&key.1),
1958 }
1959 }
1960
1961 fn store_account_data_cache(&mut self, key: &AccountDataKey, value: Option<serde_json::Value>) {
1964 if key.0.is_none() && key.1 == "m.direct" {
1965 self.direct_account_data = value.as_ref().and_then(|v| serde_json::from_value::<DirectContent>(v.clone()).ok());
1966 }
1967 match (&key.0, value) {
1968 (None, Some(v)) => {
1969 self.global_account_data.insert(key.1.clone(), v);
1970 }
1971 (None, None) => {
1972 self.global_account_data.remove(&key.1);
1973 }
1974 (Some(room_id), Some(v)) => {
1975 self.room_account_data.entry(room_id.clone()).or_default().insert(key.1.clone(), v);
1976 }
1977 (Some(room_id), None) => {
1978 if let Some(by_type) = self.room_account_data.get_mut(room_id) {
1979 by_type.remove(&key.1);
1980 }
1981 }
1982 }
1983 }
1984
1985 fn write_account_data(&mut self, room_id: Option<RoomId>, event_type: String, value: serde_json::Value) -> Result<(), MessengerError> {
1991 let request_id = self.next_request_id()?;
1992 let request = match &room_id {
1993 Some(room_id) => OutgoingRequest::room_account_data(request_id.clone(), &self.config.user_id, room_id, &event_type, value.clone()),
1994 None => OutgoingRequest::account_data(request_id.clone(), &self.config.user_id, &event_type, value.clone()),
1995 };
1996 let key: AccountDataKey = (room_id, event_type);
1997 let known = self.cached_account_data(&key).cloned();
1998 let guard = self.account_data_guard.entry(key.clone()).or_insert(AccountDataGuard { pending: 0, server_value: known });
1999 guard.pending += 1;
2000 self.store_account_data_cache(&key, Some(value));
2001 match self.enqueue_request(request, Lane::AccountData) {
2002 Ok(()) => {
2003 self.account_data_write_keys.insert(request_id, key);
2004 Ok(())
2005 }
2006 Err(error) => {
2007 self.settle_account_data_write(&key, false);
2008 Err(error)
2009 }
2010 }
2011 }
2012
2013 fn settle_account_data_write(&mut self, key: &AccountDataKey, succeeded: bool) {
2018 let Some(guard) = self.account_data_guard.get_mut(key) else { return };
2019 guard.pending = guard.pending.saturating_sub(1);
2020 if guard.pending > 0 {
2021 return;
2022 }
2023 let Some(guard) = self.account_data_guard.remove(key) else { return };
2024 if !succeeded {
2025 self.store_account_data_cache(key, guard.server_value);
2026 self.emit(MessengerEvent::RoomsChanged);
2027 }
2028 }
2029
2030 fn apply_synced_account_data(&mut self, room_id: Option<&RoomId>, event_type: &str, content: serde_json::Value) {
2034 let key: AccountDataKey = (room_id.cloned(), event_type.to_string());
2035 if let Some(guard) = self.account_data_guard.get_mut(&key) {
2036 guard.server_value = Some(content);
2037 return;
2038 }
2039 self.store_account_data_cache(&key, Some(content));
2040 }
2041
2042 pub fn take_flush_batch(&mut self) -> Option<FlushBatch> {
2045 if let Some(batch) = self.pending_flush_batches.pop_front() {
2046 return Some(batch);
2047 }
2048 self.store.take_flush_batch()
2049 }
2050
2051 pub fn ack_flush(&mut self, id: u64) {
2053 self.store.ack_flush(id);
2054 }
2055
2056 pub fn events(&mut self) -> Vec<MessengerEvent> {
2059 self.drain_events()
2060 }
2061
2062 pub fn take_ingest_error(&mut self) -> Option<String> {
2065 self.ingest_error.take()
2066 }
2067
2068 pub fn change_counter(&self) -> u64 {
2073 self.change_counter
2074 }
2075
2076 pub fn room_ids(&self) -> impl Iterator<Item = &RoomId> {
2078 self.rooms.keys()
2079 }
2080
2081 pub fn room_state(&self, room_id: &RoomId) -> Option<&RoomState> {
2083 self.rooms.get(room_id)
2084 }
2085
2086 pub fn timeline(&self, room_id: &RoomId) -> Option<&Timeline> {
2088 self.timelines.get(room_id)
2089 }
2090
2091 pub fn room_kind(&self, room_id: &RoomId) -> Option<RoomKind> {
2095 self.rooms.get(room_id).map(|state| state.derive_room_kind(room_id, self.direct_account_data.as_ref()))
2096 }
2097
2098 pub fn typing_users(&self, room_id: &RoomId) -> &[UserId] {
2102 self.typing.get(room_id).map(Vec::as_slice).unwrap_or(&[])
2103 }
2104
2105 pub fn receipts(&self, room_id: &RoomId) -> Option<&ReceiptContent> {
2107 self.receipts.get(room_id)
2108 }
2109
2110 pub fn room_account_data(&self, room_id: &RoomId, event_type: &str) -> Option<&serde_json::Value> {
2113 self.room_account_data.get(room_id).and_then(|by_type| by_type.get(event_type))
2114 }
2115
2116 pub fn global_account_data(&self, event_type: &str) -> Option<&serde_json::Value> {
2119 self.global_account_data.get(event_type)
2120 }
2121
2122 pub fn key_request_count(&self) -> usize {
2124 self.key_requests_sent.len()
2125 }
2126
2127 pub fn user_id(&self) -> &UserId {
2132 &self.config.user_id
2133 }
2134
2135 pub fn has_tracked_device(&self, user_id: &UserId) -> bool {
2141 DeviceTracker::devices_for_user(&self.store, user_id).map(|devices| !devices.is_empty()).unwrap_or(false)
2142 }
2143
2144 pub fn public_rooms_result(&self) -> &[PublicRoomsResultEntry] {
2147 &self.public_rooms_result
2148 }
2149
2150 pub fn user_search_result(&self) -> &[UserDirectoryResultEntry] {
2153 &self.user_search_result
2154 }
2155
2156 pub fn send_failure_reason(&self, txn_id: &TxnId) -> Option<&str> {
2161 match self.send_state.get(txn_id).map(|pending| &pending.phase) {
2162 Some(SendPhase::Failed { errcode }) => Some(errcode.as_str()),
2163 _ => None,
2164 }
2165 }
2166
2167 pub fn dispatch(&mut self, cmd: MessengerCommand, now_ms: i64) -> Result<(), MessengerError> {
2174 match cmd {
2175 MessengerCommand::SendMessage { room_id, message, txn_id } => {
2176 if let Some(edit_of) = &message.edit_of {
2177 let owned = self
2178 .timelines
2179 .get(&room_id)
2180 .and_then(|timeline| timeline.item_by_event_id(edit_of))
2181 .is_some_and(|item| item.sender == self.config.user_id);
2182 if !owned {
2183 return Err(MessengerError::EditNotOwned { event_id: edit_of.clone() });
2184 }
2185 }
2186 self.start_new_send(room_id, SendPayload::Message(message), txn_id, now_ms)?;
2187 }
2188 MessengerCommand::Forward { from_room, event_id, to_room, txn_id } => {
2189 let payload = self.build_forward_payload(&from_room, &event_id, &to_room)?;
2190 self.start_new_send(to_room, payload, txn_id, now_ms)?;
2191 }
2192 MessengerCommand::React { room_id, target, key } => {
2193 self.start_new_send(room_id, SendPayload::Reaction { target, key }, None, now_ms)?;
2194 }
2195 MessengerCommand::Redact { room_id, target, reason } => {
2196 self.start_new_send(room_id, SendPayload::Redaction { target, reason }, None, now_ms)?;
2197 }
2198 MessengerCommand::RetrySend { room_id, txn_id } => {
2199 self.retry_send(&room_id, &txn_id, now_ms)?;
2200 }
2201 MessengerCommand::CreateRoom { kind } => {
2202 self.dispatch_create_room(kind)?;
2203 }
2204 MessengerCommand::JoinRoom { room_id } => {
2205 let request_id = self.next_request_id()?;
2206 let request = OutgoingRequest::join_room(request_id, room_id.as_str());
2207 self.enqueue_request(request, Lane::Other)?;
2208
2209 let dm_peer = self.rooms.get(&room_id).and_then(|room| {
2216 let invited_as_direct = room.members.get(&self.config.user_id).is_some_and(|member| member.is_direct);
2217 if !invited_as_direct {
2218 return None;
2219 }
2220 room.members.keys().find(|user_id| **user_id != self.config.user_id).cloned()
2221 });
2222 if let Some(peer) = dm_peer {
2223 self.merge_direct_account_data(room_id, peer)?;
2224 }
2225 }
2226 MessengerCommand::LeaveRoom { room_id } => {
2227 let request_id = self.next_request_id()?;
2228 let request = OutgoingRequest::leave_room(request_id, &room_id);
2229 self.enqueue_request(request, Lane::Other)?;
2230 }
2231 MessengerCommand::Invite { room_id, user_id } => {
2232 let request_id = self.next_request_id()?;
2233 let request = OutgoingRequest::invite(request_id, &room_id, &user_id);
2234 self.enqueue_request(request, Lane::Other)?;
2235 }
2236 MessengerCommand::Kick { room_id, user_id, reason } => {
2237 let request_id = self.next_request_id()?;
2238 let request = OutgoingRequest::kick(request_id, &room_id, &user_id, reason.as_deref());
2239 self.enqueue_request(request, Lane::Other)?;
2240 }
2241 MessengerCommand::SetPowerLevel { room_id, user_id, level } => {
2242 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())))?;
2243 content.users.insert(user_id, level);
2244 let body = serde_json::to_value(&content)?;
2245 let request_id = self.next_request_id()?;
2246 let request = OutgoingRequest::set_state(request_id, &room_id, "m.room.power_levels", "", body);
2247 self.enqueue_request(request, Lane::Other)?;
2248 }
2249 MessengerCommand::SetTag { room_id, tag, order } => {
2250 let mut content = self.current_tag_content(&room_id);
2251 content.tags.insert(tag, TagInfo { order });
2252 self.enqueue_tag_update(room_id, content)?;
2253 }
2254 MessengerCommand::RemoveTag { room_id, tag } => {
2255 let mut content = self.current_tag_content(&room_id);
2256 content.tags.remove(&tag);
2257 self.enqueue_tag_update(room_id, content)?;
2258 }
2259 MessengerCommand::MarkRead { room_id, event_id } => {
2260 let request_id = self.next_request_id()?;
2261 let body = serde_json::json!({ "m.fully_read": event_id.as_str(), "m.read": event_id.as_str() });
2262 let request = OutgoingRequest::read_markers(request_id, &room_id, body);
2263 self.enqueue_request(request, Lane::Other)?;
2264 }
2265 MessengerCommand::SetTyping { room_id, typing } => {
2266 if typing {
2267 let should_send = match self.typing_debounce.get(&room_id) {
2268 Some(&last_sent_ms) => now_ms.saturating_sub(last_sent_ms) >= TYPING_DEBOUNCE_MS,
2269 None => true,
2270 };
2271 if !should_send {
2272 return Ok(());
2273 }
2274 self.typing_debounce.insert(room_id.clone(), now_ms);
2275 } else {
2276 self.typing_debounce.remove(&room_id);
2277 }
2278 let request_id = self.next_request_id()?;
2279 let timeout_ms = typing.then_some(TYPING_TIMEOUT_MS);
2280 let request = OutgoingRequest::typing(request_id, &room_id, &self.config.user_id, typing, timeout_ms);
2281 self.enqueue_request(request, Lane::Other)?;
2282 }
2283 MessengerCommand::RetryDecryption { room_id, event_id } => {
2284 if self.retry_decrypt_one(&room_id, &event_id) {
2285 self.emit(MessengerEvent::TimelineChanged { room_id });
2286 }
2287 }
2288 MessengerCommand::LoadOlder { room_id } => {
2289 let from = self
2290 .timelines
2291 .get(&room_id)
2292 .and_then(|timeline| {
2293 timeline
2294 .older_token()
2295 .map(str::to_string)
2296 .or_else(|| timeline.gap().and_then(|gap| gap.prev_batch.clone()))
2297 })
2298 .or_else(|| {
2304 if self.rooms.contains_key(&room_id) {
2305 self.store.sync_token().ok().flatten().map(str::to_string)
2306 } else {
2307 None
2308 }
2309 });
2310 let Some(from) = from else { return Ok(()) };
2311 let request_id = self.next_request_id()?;
2312 let request =
2313 OutgoingRequest::room_messages(request_id.clone(), &room_id, &from, "b", Some(LOAD_OLDER_PAGE_SIZE));
2314 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
2315 self.pending_room_messages.insert(request_id, room_id);
2316 }
2317 MessengerCommand::SetAccountData { event_type, content } => {
2318 self.write_account_data(None, event_type, content)?;
2319 }
2320 MessengerCommand::SetRoomAccountData { room_id, event_type, content } => {
2321 self.write_account_data(Some(room_id), event_type, content)?;
2322 }
2323 MessengerCommand::SearchPublicRooms { term } => {
2324 let request_id = self.next_request_id()?;
2325 let body = serde_json::json!({ "filter": { "generic_search_term": term }, "limit": 50 });
2326 let request = OutgoingRequest::public_rooms(request_id.clone(), body);
2327 self.latest_public_rooms_request = Some(request_id);
2328 self.enqueue_request(request, Lane::Other)?;
2329 }
2330 MessengerCommand::SearchUsers { term } => {
2331 let request_id = self.next_request_id()?;
2332 let body = serde_json::json!({ "search_term": term, "limit": 20 });
2333 let request = OutgoingRequest::user_directory_search(request_id.clone(), body);
2334 self.latest_user_search_request = Some(request_id);
2335 self.enqueue_request(request, Lane::Other)?;
2336 }
2337 }
2338 Ok(())
2339 }
2340
2341 fn dispatch_create_room(&mut self, kind: CreateRoomKind) -> Result<(), MessengerError> {
2346 let body = match &kind {
2347 CreateRoomKind::Dm { peer } => serde_json::json!({
2348 "is_direct": true,
2349 "invite": [peer.as_str()],
2350 }),
2351 CreateRoomKind::Group { name, invite, members_can_invite } => serde_json::json!({
2352 "visibility": "private",
2353 "is_direct": false,
2354 "name": name,
2355 "invite": invite.iter().map(UserId::as_str).collect::<Vec<_>>(),
2356 "members_can_invite": members_can_invite,
2357 }),
2358 CreateRoomKind::Channel { name, topic } => {
2359 let mut value = serde_json::json!({
2360 "visibility": "public",
2361 "is_direct": false,
2362 "name": name,
2363 });
2364 if let Some(topic) = topic {
2365 value["topic"] = serde_json::Value::String(topic.clone());
2366 }
2367 value
2368 }
2369 };
2370 let request_id = self.next_request_id()?;
2371 let request = OutgoingRequest::create_room(request_id.clone(), body);
2372 self.pending_create_room.insert(request_id.clone(), kind);
2373 self.enqueue_request(request, Lane::Other)?;
2374 Ok(())
2375 }
2376
2377 fn current_tag_content(&self, room_id: &RoomId) -> TagContent {
2382 self.room_account_data(room_id, "m.tag")
2383 .and_then(|value| serde_json::from_value(value.clone()).ok())
2384 .unwrap_or_default()
2385 }
2386
2387 fn enqueue_tag_update(&mut self, room_id: RoomId, content: TagContent) -> Result<(), MessengerError> {
2393 let value = serde_json::to_value(&content)?;
2394 self.write_account_data(Some(room_id), "m.tag".to_string(), value)
2395 }
2396
2397 fn build_forward_payload(
2402 &self,
2403 from_room: &RoomId,
2404 event_id: &EventId,
2405 to_room: &RoomId,
2406 ) -> Result<SendPayload, MessengerError> {
2407 let refuse = |reason: &dyn std::fmt::Display| {
2408 MessengerError::IntentRefused(format!("cannot forward {event_id} from {from_room}: {reason}"))
2409 };
2410 if !self.rooms.contains_key(to_room) {
2411 return Err(refuse(&format!("the target room {to_room} is unknown")));
2412 }
2413 let source = match self.timelines.get(from_room) {
2414 Some(timeline) => timeline.forward_source(event_id),
2415 None => Err(ForwardRefusal::Missing),
2416 }
2417 .map_err(|reason| refuse(&reason))?;
2418 let (marker_key, marker) = self.forward_marker(from_room);
2419 Ok(SendPayload::Event {
2420 event_type: source.event_type,
2421 content: forwardable_content(source.content, marker_key, marker),
2422 })
2423 }
2424
2425 fn forward_marker(&self, from_room: &RoomId) -> (&'static str, serde_json::Value) {
2430 let public_channel = self.room_kind(from_room) == Some(RoomKind::Channel);
2431 if public_channel {
2432 let room_name = self.forward_room_name(from_room);
2433 (KEY_FORWARDED_FROM, serde_json::json!({ "room_id": from_room.as_str(), "room_name": room_name }))
2434 } else {
2435 (KEY_FORWARDED, serde_json::Value::Bool(true))
2436 }
2437 }
2438
2439 fn forward_room_name(&self, room_id: &RoomId) -> String {
2443 if let Some(name) = self.rooms.get(room_id).and_then(|room| room.name.clone()) {
2444 return name;
2445 }
2446 match self.room_kind(room_id) {
2447 Some(RoomKind::Dm) => "Direct",
2448 Some(RoomKind::Channel) => "Channel",
2449 Some(RoomKind::Group) | None => "Group",
2450 }
2451 .to_string()
2452 }
2453
2454 fn retry_decrypt_one(&mut self, room_id: &RoomId, event_id: &EventId) -> bool {
2460 let Some(item) = self.pending_decrypt.get(room_id).and_then(|by_event| by_event.get(event_id)).cloned() else {
2461 return false;
2462 };
2463 match GroupSessionManager::decrypt_event(
2464 &mut self.store,
2465 room_id,
2466 event_id,
2467 item.origin_server_ts,
2468 &item.sender,
2469 &item.content,
2470 ) {
2471 Ok(plaintext) => {
2472 self.apply_decrypted_plaintext(room_id, event_id, &item.sender, item.origin_server_ts, &plaintext);
2473 }
2474 Err(GroupDecryptError::MissingSession { .. }) => {
2475 let _ = self.request_missing_room_key(room_id, &item.sender, &item.content);
2476 return false;
2477 }
2478 Err(_) => return false,
2479 }
2480 if let Some(by_event) = self.pending_decrypt.get_mut(room_id) {
2481 by_event.remove(event_id);
2482 }
2483 true
2484 }
2485
2486 pub fn pull_latest_page(&mut self, room_id: RoomId) -> Result<(), MessengerError> {
2489 let request_id = self.next_request_id()?;
2490 let request = OutgoingRequest::room_messages_latest(request_id.clone(), &room_id, 20);
2491 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
2492 self.pending_room_messages.insert(request_id, room_id);
2493 Ok(())
2494 }
2495
2496 pub fn decrypt_loaded_timeline(&mut self) {
2500 let pending: Vec<(RoomId, Vec<EventId>)> = self
2501 .pending_decrypt
2502 .iter()
2503 .map(|(room_id, by_event)| (room_id.clone(), by_event.keys().cloned().collect()))
2504 .collect();
2505 for (room_id, event_ids) in pending {
2506 for event_id in event_ids {
2507 self.retry_decrypt_one(&room_id, &event_id);
2508 }
2509 }
2510 let rooms: Vec<RoomId> = self.timelines.keys().cloned().collect();
2511 for room_id in rooms {
2512 let sealed: Vec<(EventId, i64, UserId, MegolmEncryptedContent)> = {
2513 let Some(timeline) = self.timelines.get(&room_id) else {
2514 continue;
2515 };
2516 timeline
2517 .items()
2518 .iter()
2519 .filter_map(|item| {
2520 let event_id = item.event_id.clone()?;
2521 let (session_id, ciphertext) = match &item.content {
2522 ItemContent::Encrypted { session_id, ciphertext_b64 } => {
2523 (session_id.clone(), ciphertext_b64.clone())
2524 }
2525 _ => return None,
2526 };
2527 Some((
2528 event_id,
2529 item.origin_server_ts,
2530 item.sender.clone(),
2531 MegolmEncryptedContent {
2532 ciphertext,
2533 session_id,
2534 sender_key: None,
2535 device_id: None,
2536 relates_to: None,
2537 },
2538 ))
2539 })
2540 .collect()
2541 };
2542 for (event_id, origin_server_ts, sender, content) in sealed {
2543 let plaintext = match GroupSessionManager::decrypt_event(
2544 &mut self.store,
2545 &room_id,
2546 &event_id,
2547 origin_server_ts,
2548 &sender,
2549 &content,
2550 ) {
2551 Ok(plaintext) => plaintext,
2552 Err(GroupDecryptError::MissingSession { .. }) => {
2553 let _ = self.request_missing_room_key(&room_id, &sender, &content);
2554 continue;
2555 }
2556 Err(_) => continue,
2557 };
2558 self.apply_decrypted_plaintext(
2559 &room_id,
2560 &event_id,
2561 &sender,
2562 origin_server_ts,
2563 &plaintext,
2564 );
2565 }
2566 }
2567 }
2568
2569 fn apply_decrypted_plaintext(
2582 &mut self,
2583 room_id: &RoomId,
2584 event_id: &EventId,
2585 sender: &UserId,
2586 origin_server_ts: i64,
2587 plaintext: &RoomEventPlaintext,
2588 ) {
2589 let Some(timeline) = self.timelines.get_mut(room_id) else { return };
2590 if let Some(relation) = RelatesTo::from_content(&plaintext.content) {
2591 if matches!(relation, RelatesTo::Annotation { .. } | RelatesTo::Replace { .. }) {
2592 let new_content = plaintext.content.get("m.new_content").cloned();
2593 timeline.apply_decrypted_relation(event_id, sender.clone(), origin_server_ts, relation, new_content);
2594 return;
2595 }
2596 }
2597 timeline.set_decrypted_event(event_id, &plaintext.event_type, &plaintext.content);
2598 }
2599
2600 fn retry_pending_for_session(&mut self, room_id: &RoomId, session_id: &str) {
2601 let Some(by_event) = self.pending_decrypt.get(room_id) else { return };
2602 let candidates: Vec<EventId> =
2603 by_event.iter().filter(|(_, item)| item.session_id == session_id).map(|(event_id, _)| event_id.clone()).collect();
2604 let mut changed = false;
2605 for event_id in candidates {
2606 if self.retry_decrypt_one(room_id, &event_id) {
2607 changed = true;
2608 }
2609 }
2610 if changed {
2611 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
2612 }
2613 }
2614
2615 pub fn republish_device_keys(&mut self) {
2618 self.account.forget_published_device_keys();
2619 }
2620
2621 fn ingest_sync(&mut self, response: &crate::wire::sync::SyncResponse) -> Result<(), MessengerError> {
2622 let mut newly_received_sessions: Vec<(RoomId, String)> = Vec::new();
2623
2624 for event in &response.to_device.events {
2626 self.process_to_device_event(event, &mut newly_received_sessions)?;
2627 }
2628
2629 DeviceTracker::on_device_lists(&mut self.store, &response.device_lists.changed, &response.device_lists.left)?;
2631
2632 let published_otk_count = response.device_one_time_keys_count.get("signed_curve25519").copied().unwrap_or(0);
2634 self.account.on_sync_counts(&mut self.store, published_otk_count, &response.device_unused_fallback_key_types)?;
2635 let keys_upload_request_id = self.next_request_id()?;
2636 if let Some(request) =
2637 self.account.keys_upload_request(keys_upload_request_id, &self.config.user_id, &self.config.device_id)?
2638 {
2639 self.enqueue_request(request, Lane::Other)?;
2640 }
2641 self.maybe_enqueue_cross_signing()?;
2642
2643 let outdated_before_rooms = self.outdated_tracked_users()?;
2645 if !outdated_before_rooms.is_empty() {
2646 self.enqueue_keys_query()?;
2647 }
2648
2649 let rooms_touched =
2651 !response.rooms.join.is_empty() || !response.rooms.invite.is_empty() || !response.rooms.leave.is_empty();
2652 for (room_id, joined) in &response.rooms.join {
2653 self.ingest_joined_room(room_id, joined)?;
2654 }
2655 for (room_id, invited) in &response.rooms.invite {
2656 self.ingest_invited_room(room_id, invited)?;
2657 }
2658 for (room_id, left) in &response.rooms.leave {
2659 self.ingest_left_room(room_id, left)?;
2660 }
2661 if rooms_touched {
2662 self.emit(MessengerEvent::RoomsChanged);
2663 }
2664 if !self.outdated_tracked_users()?.is_subset(&outdated_before_rooms) {
2668 self.enqueue_keys_query()?;
2669 }
2670
2671 for event in &response.account_data.events {
2673 self.apply_synced_account_data(None, &event.event_type, event.content.clone());
2674 }
2675
2676 for (room_id, session_id) in &newly_received_sessions {
2677 self.retry_pending_for_session(room_id, session_id);
2678 }
2679
2680 self.store.save_sync_token(response.next_batch.clone())?;
2683 Ok(())
2684 }
2685
2686 fn process_to_device_event(
2687 &mut self,
2688 event: &ToDeviceEvent,
2689 newly_received_sessions: &mut Vec<(RoomId, String)>,
2690 ) -> Result<(), MessengerError> {
2691 if event.event_type != "m.room.encrypted" {
2692 return Ok(());
2693 }
2694 let Ok(RoomEncryptedContent::Olm(content)) = serde_json::from_value::<RoomEncryptedContent>(event.content.clone())
2695 else {
2696 return Ok(());
2697 };
2698 match OlmSessionManager::decrypt_to_device(&mut self.store, &mut self.account, &self.config.user_id, &event.sender, &content) {
2699 Ok(decrypted) => self.route_decrypted_to_device(decrypted, newly_received_sessions),
2700 Err(OlmDecryptError::UnknownSenderDevice { .. }) => {
2701 self.queue_unknown_sender_retry(event.clone());
2702 Ok(())
2703 }
2704 Err(_) => Ok(()),
2711 }
2712 }
2713
2714 fn route_decrypted_to_device(
2715 &mut self,
2716 decrypted: DecryptedToDevice,
2717 newly_received_sessions: &mut Vec<(RoomId, String)>,
2718 ) -> Result<(), MessengerError> {
2719 match decrypted.event_type.as_str() {
2720 "m.room_key" => {
2721 if let Ok(content) = serde_json::from_value::<RoomKeyContent>(decrypted.content.clone()) {
2722 if content.algorithm == "m.megolm.v1.aes-sha2" {
2723 GroupSessionManager::receive_room_key(
2724 &mut self.store,
2725 &decrypted.sender,
2726 decrypted.sender_device_curve25519,
2727 decrypted.sender_ed25519,
2728 &content,
2729 )?;
2730 newly_received_sessions.push((content.room_id, content.session_id));
2731 }
2732 }
2733 }
2734 "m.forwarded_room_key" => {
2735 if let Ok(content) = serde_json::from_value::<ForwardedRoomKeyContent>(decrypted.content.clone()) {
2736 if let Some((user_id, curve, ed)) = self.forward_binding(&decrypted, &content)? {
2737 let stored = GroupSessionManager::receive_forwarded_room_key(
2738 &mut self.store,
2739 &user_id,
2740 curve,
2741 ed,
2742 &content,
2743 )?;
2744 if stored {
2745 newly_received_sessions.push((content.room_id, content.session_id));
2746 }
2747 }
2748 }
2749 }
2750 "m.room_key_request" => {
2751 if let Ok(content) = serde_json::from_value::<RoomKeyRequestContent>(decrypted.content.clone()) {
2752 self.answer_room_key_request(&decrypted, &content)?;
2753 }
2754 }
2755 "m.room_key.withheld" => {
2756 if let Ok(content) = serde_json::from_value::<RoomKeyWithheldContent>(decrypted.content.clone()) {
2757 if let (Some(room_id), Some(session_id)) = (content.room_id, content.session_id) {
2758 self.withheld_sessions.insert((room_id, session_id));
2759 }
2760 }
2761 }
2762 _ => {}
2763 }
2764 Ok(())
2765 }
2766
2767 fn forward_binding(
2773 &self,
2774 decrypted: &DecryptedToDevice,
2775 content: &ForwardedRoomKeyContent,
2776 ) -> Result<Option<(UserId, mail4agent_vodozemac::Curve25519PublicKey, mail4agent_vodozemac::Ed25519PublicKey)>, MessengerError>
2777 {
2778 let members = self.encrypted_room_member_ids(&content.room_id);
2779 let forwarder_is_us = decrypted.sender == self.config.user_id
2782 || (self.history_forwarding_allowed(&content.room_id) && self.room_admins(&content.room_id).contains(&decrypted.sender));
2783 if !forwarder_is_us && !members.contains(&decrypted.sender) {
2784 return Ok(None);
2785 }
2786 for user_id in &members {
2787 for device in DeviceTracker::devices_for_user(&self.store, user_id)? {
2788 if device.blocked || device.curve25519.to_base64() != content.sender_key {
2789 continue;
2790 }
2791 let same_user = decrypted.sender == device.user_id;
2792 if same_user || forwarder_is_us {
2793 return Ok(Some((device.user_id, device.curve25519, device.ed25519)));
2794 }
2795 }
2796 }
2797 if decrypted.sender_device_curve25519.to_base64() == content.sender_key {
2798 return Ok(Some((
2799 decrypted.sender.clone(),
2800 decrypted.sender_device_curve25519,
2801 decrypted.sender_ed25519,
2802 )));
2803 }
2804 Ok(None)
2805 }
2806
2807 fn answer_room_key_request(
2812 &mut self,
2813 decrypted: &DecryptedToDevice,
2814 content: &RoomKeyRequestContent,
2815 ) -> Result<(), MessengerError> {
2816 if matches!(content.action, RoomKeyRequestAction::RequestCancellation) {
2817 self.key_requests_answered.remove(&(
2818 decrypted.sender.clone(),
2819 content.requesting_device_id.clone(),
2820 content.request_id.clone(),
2821 ));
2822 return Ok(());
2823 }
2824 if !matches!(content.action, RoomKeyRequestAction::Request) {
2825 return Ok(());
2826 }
2827 let Some(body) = content.body.as_ref() else { return Ok(()) };
2828 if body.algorithm != "m.megolm.v1.aes-sha2" {
2829 return Ok(());
2830 }
2831 let members = self.encrypted_room_member_ids(&body.room_id);
2832 if !members.contains(&decrypted.sender) {
2833 return Ok(());
2834 }
2835 if decrypted.sender != self.config.user_id && !self.history_forwarding_allowed(&body.room_id) {
2836 return Ok(());
2837 }
2838 if decrypted.sender == self.config.user_id && content.requesting_device_id == self.config.device_id {
2839 return Ok(());
2840 }
2841 let dedup = (decrypted.sender.clone(), content.requesting_device_id.clone(), content.request_id.clone());
2842 if self.key_requests_answered.contains(&dedup) {
2843 return Ok(());
2844 }
2845 let Some(target) = self.requester_device(decrypted, &content.requesting_device_id)? else {
2846 return Ok(());
2847 };
2848 let our_curve = self.account.identity_keys().curve25519.to_base64();
2849 let (inner_type, payload) =
2850 if let Some(forwarded) = GroupSessionManager::forwarded_room_key_content(
2851 &self.store,
2852 &body.room_id,
2853 &body.session_id,
2854 &our_curve,
2855 )? {
2856 ("m.forwarded_room_key", serde_json::to_value(&forwarded)?)
2857 } else if let Some(room_key) =
2858 GroupSessionManager::outbound_room_key_content(&self.store, &body.room_id, &body.session_id)?
2859 {
2860 ("m.room_key", serde_json::to_value(&room_key)?)
2861 } else {
2862 return Ok(());
2863 };
2864 let sent = self.enqueue_olm_to_devices(std::slice::from_ref(&target), inner_type, payload)?;
2865 if sent > 0 {
2866 self.key_requests_answered.insert(dedup);
2867 }
2868 Ok(())
2869 }
2870
2871 fn requester_device(
2875 &self,
2876 decrypted: &DecryptedToDevice,
2877 requesting_device_id: &DeviceId,
2878 ) -> Result<Option<StoredDevice>, MessengerError> {
2879 let devices = DeviceTracker::devices_for_user(&self.store, &decrypted.sender)?;
2880 if let Some(device) = devices.iter().find(|device| &device.device_id == requesting_device_id) {
2881 return Ok((!device.blocked).then(|| device.clone()));
2882 }
2883 let curve = decrypted.sender_device_curve25519.to_base64();
2884 if let Some(device) = devices.iter().find(|device| device.curve25519.to_base64() == curve) {
2885 return Ok((!device.blocked).then(|| device.clone()));
2886 }
2887 Ok(None)
2888 }
2889
2890 fn request_missing_room_key(
2895 &mut self,
2896 room_id: &RoomId,
2897 sender: &UserId,
2898 content: &MegolmEncryptedContent,
2899 ) -> Result<(), MessengerError> {
2900 let key = (room_id.clone(), content.session_id.clone());
2901 if self.key_requests_sent.contains(&key) || self.withheld_sessions.contains(&key) {
2902 return Ok(());
2903 }
2904 let sender_key = match content.sender_key.clone() {
2905 Some(sender_key) => sender_key,
2906 None => {
2907 let devices = DeviceTracker::devices_for_user(&self.store, sender)?;
2908 let found = content
2909 .device_id
2910 .as_ref()
2911 .and_then(|device_id| devices.iter().find(|device| &device.device_id == device_id))
2912 .or_else(|| devices.iter().find(|device| !device.blocked));
2913 match found {
2914 Some(device) => device.curve25519.to_base64(),
2915 None => return Ok(()),
2916 }
2917 }
2918 };
2919 let mut targets = DeviceTracker::devices_for_user(&self.store, sender)?;
2920 targets.extend(DeviceTracker::devices_for_user(&self.store, &self.config.user_id)?);
2921 if self.history_forwarding_allowed(room_id) {
2924 for admin in self.room_admins(room_id) {
2925 if &admin != sender && admin != self.config.user_id {
2926 targets.extend(DeviceTracker::devices_for_user(&self.store, &admin)?);
2927 }
2928 }
2929 }
2930 if targets.is_empty() {
2931 return Ok(());
2932 }
2933 let missing: Vec<StoredDevice> = OlmSessionManager::sessions_missing_for(&self.store, &targets)?
2937 .into_iter()
2938 .filter(|device| {
2939 !device.blocked
2940 && !(device.user_id == self.config.user_id && device.device_id == self.config.device_id)
2941 && !self.history_claim_tried.contains(&(device.user_id.clone(), device.device_id.clone()))
2942 })
2943 .cloned()
2944 .collect();
2945 if !missing.is_empty() {
2946 let claim_id = self.next_request_id()?;
2947 let refs: Vec<&StoredDevice> = missing.iter().collect();
2948 if let Some(request) = OlmSessionManager::keys_claim_request(claim_id.clone(), &refs) {
2949 for device in &missing {
2950 self.history_claim_tried.insert((device.user_id.clone(), device.device_id.clone()));
2951 }
2952 self.history_claims.insert(claim_id, missing);
2953 self.enqueue_request(request, Lane::Other)?;
2954 return Ok(());
2955 }
2956 }
2957 let request_id = self.next_txn_id()?;
2958 let body = RoomKeyRequestContent {
2959 action: RoomKeyRequestAction::Request,
2960 body: Some(RoomKeyRequestBody {
2961 algorithm: "m.megolm.v1.aes-sha2".to_string(),
2962 room_id: room_id.clone(),
2963 session_id: content.session_id.clone(),
2964 sender_key,
2965 }),
2966 request_id: request_id.as_str().to_string(),
2967 requesting_device_id: self.config.device_id.clone(),
2968 };
2969 let sent = self.enqueue_olm_to_devices(&targets, "m.room_key_request", serde_json::to_value(&body)?)?;
2970 if sent > 0 {
2971 self.key_requests_sent.insert(key);
2972 }
2973 Ok(())
2974 }
2975
2976 fn enqueue_olm_to_devices(
2981 &mut self,
2982 devices: &[StoredDevice],
2983 inner_type: &str,
2984 content: serde_json::Value,
2985 ) -> Result<usize, MessengerError> {
2986 let mut unique = Vec::new();
2987 for device in devices {
2988 if device.blocked || (device.user_id == self.config.user_id && device.device_id == self.config.device_id) {
2989 continue;
2990 }
2991 if unique.iter().any(|have: &StoredDevice| have.user_id == device.user_id && have.device_id == device.device_id)
2992 {
2993 continue;
2994 }
2995 unique.push(device.clone());
2996 }
2997 let missing = OlmSessionManager::sessions_missing_for(&self.store, &unique)?;
2998 let missing_ids: BTreeSet<(UserId, DeviceId)> =
2999 missing.into_iter().map(|device| (device.user_id.clone(), device.device_id.clone())).collect();
3000 let ready: Vec<StoredDevice> = unique
3001 .into_iter()
3002 .filter(|device| !missing_ids.contains(&(device.user_id.clone(), device.device_id.clone())))
3003 .collect();
3004 let mut sent = 0;
3005 for chunk in GroupSessionManager::chunk_recipients_for_send_to_device(&ready) {
3006 let body = GroupSessionManager::build_encrypted_to_device_body(
3007 &mut self.store,
3008 &self.account,
3009 &self.config.user_id,
3010 &self.config.device_id,
3011 chunk,
3012 inner_type,
3013 content.clone(),
3014 )?;
3015 let request_id = self.next_request_id()?;
3016 let share_txn = self.next_txn_id()?;
3017 let request = OutgoingRequest::send_to_device(request_id, ROOM_KEY_SHARE_EVENT_TYPE, &share_txn, body);
3018 self.enqueue_request(request, Lane::ToDevice)?;
3019 sent += chunk.len();
3020 }
3021 Ok(sent)
3022 }
3023
3024 fn queue_unknown_sender_retry(&mut self, event: ToDeviceEvent) {
3025 self.unknown_sender_retry.push_back(event);
3026 while self.unknown_sender_retry.len() > UNKNOWN_SENDER_RETRY_CAPACITY {
3027 self.unknown_sender_retry.pop_front();
3028 }
3029 }
3030
3031 fn retry_unknown_sender_queue(&mut self) -> Result<(), MessengerError> {
3032 let pending: Vec<ToDeviceEvent> = std::mem::take(&mut self.unknown_sender_retry).into_iter().collect();
3033 let mut newly_received_sessions = Vec::new();
3034 for event in pending {
3035 self.process_to_device_event(&event, &mut newly_received_sessions)?;
3036 }
3037 for (room_id, session_id) in &newly_received_sessions {
3038 self.retry_pending_for_session(room_id, session_id);
3039 }
3040 Ok(())
3041 }
3042
3043 fn outdated_tracked_users(&self) -> Result<BTreeSet<UserId>, MessengerError> {
3045 Ok(self.store.tracked_users()?.into_iter().filter_map(|(user_id, outdated)| outdated.then_some(user_id)).collect())
3046 }
3047
3048 fn enqueue_keys_query(&mut self) -> Result<(), MessengerError> {
3050 let request_id = self.next_request_id()?;
3051 if let Some(request) = DeviceTracker::keys_query_request(&self.store, request_id)? {
3052 self.enqueue_request(request, Lane::Other)?;
3053 }
3054 Ok(())
3055 }
3056
3057 fn retry_missing_room_keys(&mut self) {
3067 let work: Vec<(RoomId, EventId)> = self
3068 .pending_decrypt
3069 .iter()
3070 .flat_map(|(room, by_event)| by_event.keys().map(move |event| (room.clone(), event.clone())))
3071 .collect();
3072 for (room_id, event_id) in work {
3073 let _ = self.retry_decrypt_one(&room_id, &event_id);
3074 }
3075 }
3076
3077 fn room_admins(&self, room_id: &RoomId) -> BTreeSet<UserId> {
3082 let Some(state) = self.room_state(room_id) else { return BTreeSet::new() };
3083 self.encrypted_room_member_ids(room_id)
3084 .into_iter()
3085 .filter(|u| state.power_levels.users.get(u).copied().unwrap_or(state.power_levels.users_default) >= 100)
3086 .collect()
3087 }
3088
3089 fn history_forwarding_allowed(&self, room_id: &RoomId) -> bool {
3090 !matches!(
3091 self.rooms.get(room_id).and_then(|room| room.history_visibility.clone()),
3092 Some(crate::wire::events::HistoryVisibility::Joined) | Some(crate::wire::events::HistoryVisibility::Invited)
3093 )
3094 }
3095
3096 fn maybe_enqueue_cross_signing(&mut self) -> Result<(), MessengerError> {
3099 if self.xsign_inflight || self.xsign_attempts >= 4 {
3100 return Ok(());
3101 }
3102 if self.xsign.is_none() {
3103 self.xsign = Some(CrossSigningState::load_or_create(&mut self.store)?);
3104 }
3105 let Some(state) = self.xsign.clone() else { return Ok(()) };
3106 let user_id = self.config.user_id.clone();
3107 let request_id;
3108 let request = if !state.uploaded {
3109 request_id = self.next_request_id()?;
3110 OutgoingRequest::signing_keys_upload(request_id, state.upload_body(&user_id)?)
3111 } else if !state.device_signed {
3112 let device_id = self.config.device_id.clone();
3113 let device_keys = self.account.device_keys_json(&user_id, &device_id)?;
3114 request_id = self.next_request_id()?;
3115 OutgoingRequest::signatures_upload(request_id, state.device_signature_body(&user_id, &device_id, device_keys)?)
3116 } else {
3117 return Ok(());
3118 };
3119 self.xsign_attempts += 1;
3120 self.xsign_inflight = true;
3121 self.enqueue_request(request, Lane::Other)
3122 }
3123
3124 fn track_joined_members<'a>(&mut self, room_id: &RoomId, events: impl Iterator<Item = &'a RawEvent>) -> Result<(), MessengerError> {
3125 if self.rooms.get(room_id).is_none_or(|room| room.encryption.is_none()) {
3126 return Ok(());
3127 }
3128 let joined: Vec<UserId> = events
3129 .filter(|event| event.event_type == "m.room.member")
3130 .filter(|event| event.content.get("membership").and_then(serde_json::Value::as_str) == Some("join"))
3131 .filter_map(|event| event.state_key.as_deref().and_then(|state_key| UserId::parse(state_key).ok()))
3132 .filter(|user_id| *user_id != self.config.user_id)
3133 .collect();
3134 if joined.is_empty() {
3135 return Ok(());
3136 }
3137 DeviceTracker::on_device_lists(&mut self.store, &joined, &[])
3138 }
3139
3140 fn ingest_joined_room(&mut self, room_id: &RoomId, joined: &JoinedRoom) -> Result<(), MessengerError> {
3141 for event in joined.state.events.iter().chain(joined.timeline.events.iter()) {
3142 self.rooms.entry(room_id.clone()).or_default().apply_state_event(event)?;
3143 }
3144
3145 self.track_joined_members(room_id, joined.state.events.iter().chain(joined.timeline.events.iter()))?;
3146
3147 let unread_before = self.rooms.get(room_id).map(RoomState::unread_count);
3148 self.rooms.entry(room_id.clone()).or_default().apply_summary(&joined.summary, &joined.unread_notifications);
3149 let unread_after = self.rooms.get(room_id).map(RoomState::unread_count);
3150 if unread_before != unread_after {
3151 self.emit(MessengerEvent::UnreadChanged { room_id: room_id.clone() });
3152 }
3153
3154 for event in &joined.account_data.events {
3155 self.apply_synced_account_data(Some(room_id), &event.event_type, event.content.clone());
3156 }
3157
3158 if let Some(typing_event) = joined.ephemeral.events.iter().find(|event| event.event_type == "m.typing") {
3159 if let Ok(typing) = serde_json::from_value::<TypingContent>(typing_event.content.clone()) {
3160 self.typing.insert(room_id.clone(), typing.user_ids);
3161 self.emit(MessengerEvent::TypingChanged { room_id: room_id.clone() });
3162 }
3163 }
3164 if let Some(receipt_event) = joined.ephemeral.events.iter().find(|event| event.event_type == "m.receipt") {
3165 if let Ok(receipts) = serde_json::from_value::<ReceiptContent>(receipt_event.content.clone()) {
3166 self.receipts.insert(room_id.clone(), receipts);
3167 self.emit(MessengerEvent::ReceiptsChanged { room_id: room_id.clone() });
3168 }
3169 }
3170
3171 if !joined.timeline.events.is_empty() {
3172 self.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(
3173 &joined.timeline.events,
3174 joined.timeline.limited,
3175 joined.timeline.prev_batch.clone(),
3176 );
3177 self.decrypt_new_timeline_events(room_id, &joined.timeline.events);
3178 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3179 }
3180
3181 self.persist_room_state(room_id)
3182 }
3183
3184 fn ingest_invited_room(&mut self, room_id: &RoomId, invited: &InvitedRoom) -> Result<(), MessengerError> {
3185 for stripped in &invited.invite_state.events {
3186 let synthetic = stripped_state_to_raw_event(stripped);
3187 self.rooms.entry(room_id.clone()).or_default().apply_state_event(&synthetic)?;
3188 }
3189 self.persist_room_state(room_id)
3190 }
3191
3192 fn ingest_left_room(&mut self, room_id: &RoomId, left: &LeftRoom) -> Result<(), MessengerError> {
3193 for event in left.state.events.iter().chain(left.timeline.events.iter()) {
3194 self.rooms.entry(room_id.clone()).or_default().apply_state_event(event)?;
3195 }
3196 for event in &left.account_data.events {
3197 self.apply_synced_account_data(Some(room_id), &event.event_type, event.content.clone());
3198 }
3199 if !left.timeline.events.is_empty() {
3200 self.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(
3201 &left.timeline.events,
3202 left.timeline.limited,
3203 left.timeline.prev_batch.clone(),
3204 );
3205 self.decrypt_new_timeline_events(room_id, &left.timeline.events);
3206 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3207 }
3208 self.persist_room_state(room_id)
3209 }
3210
3211 fn persist_room_state(&mut self, room_id: &RoomId) -> Result<(), MessengerError> {
3212 let Some(state) = self.rooms.get(room_id) else { return Ok(()) };
3213 let bytes = serde_json::to_vec(state)
3214 .map_err(|source| MessengerError::Crypto(format!("encode room state for {room_id}: {source}")))?;
3215 self.store.save_room_state(room_id, bytes)?;
3216 Ok(())
3217 }
3218
3219 fn decrypt_new_timeline_events(&mut self, room_id: &RoomId, events: &[RawEvent]) {
3227 for event in events {
3228 if event.event_type != "m.room.encrypted" || event.state_key.is_some() || event.unsigned.redacted_because.is_some() {
3229 continue;
3230 }
3231 let Ok(RoomEncryptedContent::Megolm(content)) = serde_json::from_value::<RoomEncryptedContent>(event.content.clone())
3232 else {
3233 continue;
3234 };
3235 match GroupSessionManager::decrypt_event(
3236 &mut self.store,
3237 room_id,
3238 &event.event_id,
3239 event.origin_server_ts,
3240 &event.sender,
3241 &content,
3242 ) {
3243 Ok(plaintext) => {
3244 self.apply_decrypted_plaintext(room_id, &event.event_id, &event.sender, event.origin_server_ts, &plaintext);
3245 if let Some(by_event) = self.pending_decrypt.get_mut(room_id) {
3246 by_event.remove(&event.event_id);
3247 }
3248 }
3249 Err(err) => {
3250 if matches!(err, GroupDecryptError::MissingSession { .. }) {
3251 let _ = self.request_missing_room_key(room_id, &event.sender, &content);
3252 }
3253 let reason = format!("{:?}", err.utd_reason());
3262 if let Some(timeline) = self.timelines.get_mut(room_id) {
3263 timeline.set_decrypted(&event.event_id, ItemContent::Undecryptable { reason });
3264 }
3265 self.pending_decrypt.entry(room_id.clone()).or_default().insert(
3266 event.event_id.clone(),
3267 PendingDecryptItem {
3268 session_id: content.session_id.clone(),
3269 sender: event.sender.clone(),
3270 origin_server_ts: event.origin_server_ts,
3271 content,
3272 },
3273 );
3274 }
3275 }
3276 }
3277 }
3278
3279 fn ingest_room_messages_response(&mut self, room_id: &RoomId, body: &[u8]) -> Result<(), MessengerError> {
3280 let parsed: RoomMessagesResponseBody = serde_json::from_slice(body)?;
3281 let mut events = parsed.chunk;
3282 events.reverse();
3283 self.timelines.entry(room_id.clone()).or_default().prepend_back_page(&events, parsed.end);
3284 self.decrypt_new_timeline_events(room_id, &events);
3285 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3286 self.persist_room_state(room_id)
3287 }
3288}
3289
3290impl MessengerCore<SealedRecordCodec> {
3291 pub fn open_sealed(
3302 records: impl IntoIterator<Item = SealedRecord>,
3303 secrets: CoreSecrets,
3304 config: CoreConfig,
3305 now_ms: i64,
3306 jitter: Box<dyn Jitter>,
3307 ) -> Result<Self, MessengerError> {
3308 let seal_key = secrets.store_seal_key.clone().ok_or_else(|| {
3309 MessengerError::Crypto(
3310 "MessengerCore::open_sealed requires CoreSecrets::store_seal_key"
3311 .to_string(),
3312 )
3313 })?;
3314 let codec = SealedRecordCodec::new(seal_key);
3315 Self::open(records, codec, config, secrets, now_ms, jitter)
3316 }
3317}
3318
3319fn stripped_placeholder_event_id() -> EventId {
3325 EventId::parse("$stripped-preview").expect("this literal is a well-formed, hardcoded $-prefixed opaque id")
3326}
3327
3328fn stripped_state_to_raw_event(stripped: &StrippedStateEvent) -> RawEvent {
3333 RawEvent {
3334 event_id: stripped_placeholder_event_id(),
3335 event_type: stripped.event_type.clone(),
3336 sender: stripped.sender.clone(),
3337 origin_server_ts: 0,
3338 state_key: Some(stripped.state_key.clone()),
3339 content: stripped.content.clone(),
3340 unsigned: Unsigned::default(),
3341 }
3342}
3343
3344fn forwardable_content(mut content: serde_json::Value, marker_key: &str, marker: serde_json::Value) -> serde_json::Value {
3351 if let Some(object) = content.as_object_mut() {
3352 for key in ["m.relates_to", "m.mentions", "m.new_content", KEY_FORWARDED, KEY_FORWARDED_FROM] {
3353 object.remove(key);
3354 }
3355 object.insert(marker_key.to_string(), marker);
3356 }
3357 content
3358}
3359
3360fn message_local_echo_content(message: &OutgoingMessage) -> ItemContent {
3367 let body = TextLikeMessageContent { body: message.body.clone(), format: None, formatted_body: None };
3368 match message.kind {
3369 MessageKind::Text => ItemContent::Text(body),
3370 MessageKind::Notice => ItemContent::Notice(body),
3371 MessageKind::Emote => ItemContent::Emote(body),
3372 }
3373}
3374
3375fn message_wire_content(message: &OutgoingMessage) -> serde_json::Value {
3384 let msgtype = message.kind.msgtype();
3385 let new_content = serde_json::json!({ "msgtype": msgtype, "body": message.body });
3386 let mut content = new_content.clone();
3387 if let Some(edit_of) = &message.edit_of {
3388 content["body"] = serde_json::Value::String(format!("* {}", message.body));
3389 content["m.new_content"] = new_content;
3390 content["m.relates_to"] = serde_json::json!({ "rel_type": "m.replace", "event_id": edit_of.as_str() });
3391 } else if let Some(reply_to) = &message.reply_to {
3392 content["m.relates_to"] = serde_json::json!({ "m.in_reply_to": { "event_id": reply_to.as_str() } });
3393 }
3394 content
3395}
3396
3397fn message_relates_to(message: &OutgoingMessage) -> Option<RelatesTo> {
3403 if let Some(edit_of) = &message.edit_of {
3404 Some(RelatesTo::Replace { event_id: edit_of.clone() })
3405 } else {
3406 message.reply_to.clone().map(|event_id| RelatesTo::InReplyTo(InReplyTo { event_id }))
3407 }
3408}
3409
3410#[cfg(test)]
3411mod tests {
3412 use super::*;
3413 use crate::room::timeline::Forwarded;
3414 use crate::store::InsecurePlainCodecForTests;
3415 use crate::wire::HttpMethod;
3416
3417 struct FixedJitter(f64);
3418
3419 impl Jitter for FixedJitter {
3420 fn next_unit(&mut self) -> f64 {
3421 self.0
3422 }
3423 }
3424
3425 fn device_id() -> DeviceId {
3426 DeviceId::parse("DEV1").expect("valid device id")
3427 }
3428
3429 fn user_id() -> UserId {
3430 UserId::parse("@alice:example.org").expect("valid user id")
3431 }
3432
3433 fn test_config() -> CoreConfig {
3434 CoreConfig { user_id: user_id(), device_id: device_id(), server_name: "example.org".to_string() }
3435 }
3436
3437 fn open_fresh_core() -> MessengerCore<InsecurePlainCodecForTests> {
3438 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, test_config(), CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3439 .expect("open succeeds on a brand-new device")
3440 }
3441
3442 fn flush_and_ack(core: &mut MessengerCore<InsecurePlainCodecForTests>) {
3450 while let Some(batch) = core.take_flush_batch() {
3451 core.ack_flush(batch.id);
3452 }
3453 }
3454
3455 #[test]
3456 fn counters_survive_restart_and_never_repeat_txn_ids() {
3457 let device = device_id();
3458 let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
3459 store.save_counters(Counters { next_request_id: 3, next_txn_id: 7 }).expect("save");
3460 let batch = store.take_flush_batch().expect("counters dirtied the store");
3461 store.ack_flush(batch.id);
3462
3463 let reloaded = Store::load(batch.records, InsecurePlainCodecForTests, device).expect("reload");
3464 let restored = reloaded.counters().expect("no error").expect("counters were persisted");
3465 assert_eq!(restored.next_txn_id, 7, "a restart resumes the txn-id sequence exactly where it left off");
3466 assert_eq!(restored.next_request_id, 3);
3467
3468 assert_eq!(restore_counter(3, Some(10)), 11, "resumes past the highest pending id, never repeating it");
3473 assert_eq!(restore_counter(3, None), 3, "nothing pending: the persisted value alone is authoritative");
3474 }
3475
3476 #[test]
3477 fn open_restores_the_request_counter_past_a_still_pending_requests_id() {
3478 let device = device_id();
3479 let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
3480 let mut queue = OutgoingQueue::new();
3481 let pending = OutgoingRequest::sync(RequestId::next(5), None, None);
3482 queue.enqueue(&mut store, pending, Lane::Sync).expect("enqueue");
3483 store.save_counters(Counters { next_request_id: 2, next_txn_id: 0 }).expect("save");
3486 let batch = store.take_flush_batch().expect("pending");
3487 store.ack_flush(batch.id);
3488
3489 let core = MessengerCore::open(
3490 batch.records,
3491 InsecurePlainCodecForTests,
3492 test_config(),
3493 CoreSecrets::default(),
3494 0,
3495 Box::new(FixedJitter(0.0)),
3496 )
3497 .expect("open succeeds");
3498 assert_eq!(
3499 core.counters.next_request_id, 6,
3500 "resumes past the highest id already in flight, not the stale persisted value"
3501 );
3502 }
3503
3504 #[test]
3505 fn sync_token_persisted_after_everything_it_covers() {
3506 let mut core = open_fresh_core();
3507 flush_and_ack(&mut core);
3508 core.releasable_requests(0);
3513 flush_and_ack(&mut core);
3514 let requests = core.releasable_requests(0);
3515 let sync_request = requests.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("a sync request was enqueued");
3516
3517 assert!(core.store.sync_token().expect("no error").is_none(), "no token before the first response");
3518
3519 let body = serde_json::json!({
3520 "next_batch": "s1",
3521 "device_one_time_keys_count": { "signed_curve25519": 0 },
3522 "device_unused_fallback_key_types": []
3523 })
3524 .to_string();
3525 core.on_response(sync_request.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
3526
3527 assert_eq!(core.store.sync_token().expect("no error"), Some("s1"));
3528 }
3529
3530 #[test]
3531 fn only_one_sync_in_flight() {
3532 let mut core = open_fresh_core();
3533 flush_and_ack(&mut core);
3534 core.releasable_requests(0);
3535 flush_and_ack(&mut core);
3536 let first = core.releasable_requests(0);
3537 assert_eq!(first.iter().filter(|r| r.kind == OutgoingRequestKind::Sync).count(), 1);
3538
3539 let second = core.releasable_requests(0);
3543 assert!(second.iter().all(|r| r.kind != OutgoingRequestKind::Sync), "no second sync request released");
3544 }
3545
3546 #[test]
3547 fn first_sync_of_a_session_never_long_polls_then_the_loop_does() {
3548 let mut core = open_fresh_core();
3549 flush_and_ack(&mut core);
3550 core.releasable_requests(0);
3551 flush_and_ack(&mut core);
3552 let first = core.releasable_requests(0);
3553 let first_sync = first.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("a sync request was enqueued");
3554 assert!(first_sync.query.contains(&("timeout".to_string(), "0".to_string())), "the initial sync must answer at once");
3555 assert!(first_sync.query.iter().all(|(key, _)| key != "since"), "a fresh device has no since token");
3556
3557 let body = serde_json::json!({
3558 "next_batch": "s1",
3559 "device_one_time_keys_count": { "signed_curve25519": 0 },
3560 "device_unused_fallback_key_types": []
3561 })
3562 .to_string();
3563 core.on_response(first_sync.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
3564 flush_and_ack(&mut core);
3565
3566 core.releasable_requests(0);
3569 flush_and_ack(&mut core);
3570 let second = core.releasable_requests(0);
3571 let second_sync = second.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("the follow-up sync was enqueued");
3572 assert!(second_sync.query.contains(&("since".to_string(), "s1".to_string())));
3573 assert!(second_sync.query.contains(&("timeout".to_string(), SYNC_TIMEOUT_MS.to_string())), "once caught up the loop long-polls");
3574 }
3575
3576 #[test]
3577 fn room_key_arrival_retries_undecryptable_items() {
3578 use crate::crypto::group_sessions::GroupSessionManager;
3579 use mail4agent_vodozemac::megolm::{GroupSession, SessionConfig};
3580 use mail4agent_vodozemac::olm::Account;
3581
3582 let mut core = open_fresh_core();
3583 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3584 let sender = UserId::parse("@bob:example.org").expect("valid user id");
3585 let identity = Account::new().identity_keys();
3586
3587 let mut group_session = GroupSession::new(SessionConfig::version_1());
3588 let session_id = group_session.session_id();
3589 let session_key = group_session.session_key();
3594 let plaintext = crate::crypto::group_sessions::RoomEventPlaintext {
3595 event_type: "m.room.message".to_string(),
3596 content: serde_json::json!({ "msgtype": "m.text", "body": "hi" }),
3597 room_id: room_id.clone(),
3598 };
3599 let message = group_session.encrypt(serde_json::to_vec(&plaintext).expect("valid JSON"));
3600
3601 let event: RawEvent = serde_json::from_value(serde_json::json!({
3602 "event_id": "$evt:example.org",
3603 "type": "m.room.encrypted",
3604 "sender": sender.as_str(),
3605 "origin_server_ts": 1,
3606 "content": {
3607 "algorithm": "m.megolm.v1.aes-sha2",
3608 "ciphertext": message.to_base64(),
3609 "session_id": session_id,
3610 }
3611 }))
3612 .expect("valid event");
3613
3614 let mut timeline = Timeline::new();
3620 timeline.apply_timeline_batch(std::slice::from_ref(&event), false, None);
3621 core.timelines.insert(room_id.clone(), timeline);
3622 core.decrypt_new_timeline_events(&room_id, std::slice::from_ref(&event));
3623 {
3624 let timeline = core.timeline(&room_id).expect("timeline exists");
3625 let item = timeline.item_by_event_id(&EventId::parse("$evt:example.org").expect("valid event id")).expect("item present");
3626 assert!(matches!(item.content, ItemContent::Undecryptable { .. }));
3627 }
3628
3629 let content = RoomKeyContent {
3633 algorithm: "m.megolm.v1.aes-sha2".to_string(),
3634 room_id: room_id.clone(),
3635 session_id: session_id.clone(),
3636 session_key: session_key.to_base64(),
3637 };
3638 GroupSessionManager::receive_room_key(&mut core.store, &sender, identity.curve25519, identity.ed25519, &content)
3639 .expect("receive room key");
3640 core.retry_pending_for_session(&room_id, &session_id);
3641
3642 let timeline = core.timeline(&room_id).expect("timeline exists");
3643 let item = timeline.item_by_event_id(&EventId::parse("$evt:example.org").expect("valid event id")).expect("item present");
3644 match &item.content {
3645 ItemContent::Text(text) => assert_eq!(text.body, "hi"),
3646 other => panic!("expected the retried decrypt to succeed, got {other:?}"),
3647 }
3648 }
3649
3650 #[test]
3651 fn redelivered_to_device_after_restart_is_dropped_silently() {
3652 use crate::crypto::olm_sessions::OlmSessionManager;
3653
3654 let alice_user = UserId::parse("@alice:example.org").expect("valid user id");
3656 let alice_device = DeviceId::parse("ALICEDEV").expect("valid device id");
3657 let mut alice_store = Store::new(alice_device.clone(), InsecurePlainCodecForTests);
3658 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
3659 let alice_device_keys =
3662 alice_account.device_keys_json(&alice_user, &alice_device).expect("sign alice's device_keys");
3663
3664 let mut bob_store = Store::new(device_id(), InsecurePlainCodecForTests);
3666 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
3667 bob_account.on_sync_counts(&mut bob_store, 0, &["signed_curve25519".to_string()]).expect("top up bob's OTKs");
3668 let upload = bob_account
3669 .keys_upload_request(RequestId::next(0), &user_id(), &device_id())
3670 .expect("builds a request")
3671 .expect("something to upload");
3672 let one_time_keys = upload.body.expect("upload has a body");
3673 let one_time_keys = one_time_keys.get("one_time_keys").and_then(serde_json::Value::as_object).expect("OTKs present");
3674 let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
3675 let claim_body = serde_json::to_vec(&serde_json::json!({
3676 "one_time_keys": { user_id().as_str(): { device_id().as_str(): { key_id: key_value } } }
3677 }))
3678 .expect("valid JSON");
3679 let bob_device_stored = crate::crypto::device_tracker::StoredDevice {
3680 user_id: user_id(),
3681 device_id: device_id(),
3682 curve25519: bob_account.identity_keys().curve25519,
3683 ed25519: bob_account.identity_keys().ed25519,
3684 algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
3685 display_name: None,
3686 verified: false,
3687 blocked: false,
3688 };
3689 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device_stored], &claim_body)
3690 .expect("alice establishes a session with bob");
3691
3692 DeviceTracker::on_keys_query_response(
3698 &mut bob_store,
3699 &serde_json::to_vec(&serde_json::json!({
3700 "device_keys": { alice_user.as_str(): { alice_device.as_str(): alice_device_keys } }
3701 }))
3702 .expect("valid JSON"),
3703 )
3704 .expect("bob learns alice's device");
3705
3706 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3707 let group_session = mail4agent_vodozemac::megolm::GroupSession::new(mail4agent_vodozemac::megolm::SessionConfig::version_1());
3708 let room_key_content = RoomKeyContent {
3709 algorithm: "m.megolm.v1.aes-sha2".to_string(),
3710 room_id,
3711 session_id: group_session.session_id(),
3712 session_key: group_session.session_key().to_base64(),
3713 };
3714 let encrypted = OlmSessionManager::encrypt_to_device(
3715 &mut alice_store,
3716 &alice_account,
3717 &alice_user,
3718 &alice_device,
3719 &bob_device_stored,
3720 "m.room_key",
3721 serde_json::to_value(&room_key_content).expect("valid JSON"),
3722 )
3723 .expect("encrypt to bob");
3724 let to_device_event = ToDeviceEvent {
3725 sender: alice_user.clone(),
3726 event_type: "m.room.encrypted".to_string(),
3727 content: serde_json::to_value(RoomEncryptedContent::Olm(encrypted)).expect("valid JSON"),
3728 };
3729
3730 let mut core =
3731 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, test_config(), CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3732 .expect("open succeeds");
3733 core.store = bob_store;
3734 core.account = bob_account;
3735
3736 let mut sessions = Vec::new();
3737 core.process_to_device_event(&to_device_event, &mut sessions).expect("first decrypt succeeds");
3738 assert_eq!(
3739 sessions,
3740 vec![(room_key_content.room_id.clone(), room_key_content.session_id.clone())],
3741 "the first, genuine delivery is fully validated and accepted"
3742 );
3743
3744 let batch = core.store.take_flush_batch().expect("decrypting dirtied the store");
3748 core.store.ack_flush(batch.id);
3749 let reloaded_store = Store::load(batch.records, InsecurePlainCodecForTests, device_id()).expect("reload succeeds");
3750 core.store = reloaded_store;
3751 core.account = OlmAccountState::load_or_create(&mut core.store).expect("reload reuses the persisted account");
3752
3753 let mut sessions_after_restart = Vec::new();
3757 let result = core.process_to_device_event(&to_device_event, &mut sessions_after_restart);
3758 assert!(result.is_ok(), "a redelivered to-device event is dropped, not surfaced as an ingestion error");
3759 assert!(sessions_after_restart.is_empty(), "the replay never produces a second accepted room key");
3760 }
3761
3762 #[test]
3763 fn unknown_sender_device_is_retried_after_keys_query() {
3764 use crate::crypto::olm_sessions::OlmSessionManager;
3765
3766 let alice_user = UserId::parse("@alice:example.org").expect("valid user id");
3768 let alice_device = DeviceId::parse("ALICEDEV").expect("valid device id");
3769 let mut alice_store = Store::new(alice_device.clone(), InsecurePlainCodecForTests);
3770 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
3771 let alice_device_keys =
3779 alice_account.device_keys_json(&alice_user, &alice_device).expect("sign alice's device_keys");
3780
3781 let bob_user = UserId::parse("@bob:example.org").expect("valid user id");
3789 let bob_device = DeviceId::parse("BOBDEV").expect("valid device id");
3790 let mut bob_store = Store::new(bob_device.clone(), InsecurePlainCodecForTests);
3791 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
3792 bob_account.on_sync_counts(&mut bob_store, 0, &["signed_curve25519".to_string()]).expect("top up bob's OTKs");
3793 let upload = bob_account
3794 .keys_upload_request(RequestId::next(0), &bob_user, &bob_device)
3795 .expect("builds a request")
3796 .expect("something to upload");
3797 let one_time_keys = upload.body.expect("upload has a body");
3798 let one_time_keys = one_time_keys.get("one_time_keys").and_then(serde_json::Value::as_object).expect("OTKs present");
3799 let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
3800 let claim_body = serde_json::to_vec(&serde_json::json!({
3801 "one_time_keys": { bob_user.as_str(): { bob_device.as_str(): { key_id: key_value } } }
3802 }))
3803 .expect("valid JSON");
3804 let bob_device_stored = crate::crypto::device_tracker::StoredDevice {
3805 user_id: bob_user.clone(),
3806 device_id: bob_device.clone(),
3807 curve25519: bob_account.identity_keys().curve25519,
3808 ed25519: bob_account.identity_keys().ed25519,
3809 algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
3810 display_name: None,
3811 verified: false,
3812 blocked: false,
3813 };
3814 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device_stored], &claim_body)
3815 .expect("alice establishes a session with bob");
3816 let encrypted = OlmSessionManager::encrypt_to_device(
3817 &mut alice_store,
3818 &alice_account,
3819 &alice_user,
3820 &alice_device,
3821 &bob_device_stored,
3822 "m.dummy",
3823 serde_json::json!({}),
3824 )
3825 .expect("encrypt to bob");
3826 let to_device_event = ToDeviceEvent {
3827 sender: alice_user.clone(),
3828 event_type: "m.room.encrypted".to_string(),
3829 content: serde_json::to_value(RoomEncryptedContent::Olm(encrypted)).expect("valid JSON"),
3830 };
3831
3832 let bob_config = CoreConfig { user_id: bob_user.clone(), device_id: bob_device.clone(), server_name: "example.org".to_string() };
3833 let mut core =
3834 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, bob_config, CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3835 .expect("open succeeds");
3836 core.store = bob_store;
3837 core.account = bob_account;
3838
3839 let mut sessions = Vec::new();
3843 core.process_to_device_event(&to_device_event, &mut sessions).expect("queues for retry, does not error");
3844 assert_eq!(core.unknown_sender_retry.len(), 1, "queued for a retry after the next keys/query");
3845 let alice_curve = alice_account.identity_keys().curve25519.to_base64();
3846 assert!(
3847 core.store.olm_sessions_for_device(&alice_curve).expect("no error").is_empty(),
3848 "the refused attempt created no Olm session (it would have consumed the one-time key)"
3849 );
3850
3851 let query_body = serde_json::to_vec(&serde_json::json!({
3856 "device_keys": { alice_user.as_str(): { alice_device.as_str(): alice_device_keys } }
3857 }))
3858 .expect("valid JSON");
3859 DeviceTracker::on_keys_query_response(&mut core.store, &query_body).expect("bob learns alice's device");
3860 core.retry_unknown_sender_queue().expect("no error");
3861 assert!(core.unknown_sender_retry.is_empty(), "the queued entry is drained by the retry attempt");
3862 assert_eq!(
3863 core.store.olm_sessions_for_device(&alice_curve).expect("no error").len(),
3864 1,
3865 "the retry decrypted the very same pre-key message and established the inbound session"
3866 );
3867 }
3868
3869 #[test]
3870 fn dispatch_load_older_produces_an_outgoing_request() {
3871 let mut core = open_fresh_core();
3872 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3873 let mut timeline = Timeline::new();
3874 let event: RawEvent = serde_json::from_value(serde_json::json!({
3875 "event_id": "$1:example.org",
3876 "type": "m.room.message",
3877 "sender": "@alice:example.org",
3878 "origin_server_ts": 1,
3879 "content": { "msgtype": "m.text", "body": "hi" }
3880 }))
3881 .expect("valid event");
3882 timeline.apply_timeline_batch(&[event], true, Some("t1".to_string()));
3883 core.timelines.insert(room_id.clone(), timeline);
3884 flush_and_ack(&mut core);
3885
3886 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3887 flush_and_ack(&mut core);
3888 let requests = core.releasable_requests(0);
3889 let load_older = requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomMessages);
3890 assert!(load_older.is_some(), "a RoomMessages request was enqueued using the room's own gap token");
3891 }
3892
3893 #[test]
3894 fn load_older_on_a_resumed_room_pages_back_from_the_sync_token() {
3895 let mut core = open_fresh_core();
3896 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3897 core.store.save_sync_token("s42_7".to_string()).expect("save the sync token");
3898 flush_and_ack(&mut core);
3899
3900 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3902 flush_and_ack(&mut core);
3903 assert!(core.releasable_requests(0).iter().all(|r| r.kind != OutgoingRequestKind::RoomMessages));
3904
3905 core.rooms.insert(room_id.clone(), RoomState::default());
3907 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3908 flush_and_ack(&mut core);
3909 let requests = core.releasable_requests(0);
3910 let load_older = requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomMessages).expect("a RoomMessages request was enqueued");
3911 assert!(load_older.query.contains(&("from".to_string(), "s42_7".to_string())), "pages back from the sync token: {:?}", load_older.query);
3912 }
3913
3914 fn bob_joins_room_body(encrypted: bool) -> serde_json::Value {
3917 let mut state = Vec::new();
3918 if encrypted {
3919 state.push(serde_json::json!({
3920 "event_id": "$enc:example.org", "type": "m.room.encryption", "sender": "@alice:example.org",
3921 "origin_server_ts": 1, "state_key": "", "content": { "algorithm": "m.megolm.v1.aes-sha2" }
3922 }));
3923 }
3924 serde_json::json!({
3925 "next_batch": "s1",
3926 "rooms": { "join": { "!r:example.org": {
3927 "state": { "events": state },
3928 "timeline": { "events": [{
3929 "event_id": "$join:example.org", "type": "m.room.member", "sender": "@bob:example.org",
3930 "origin_server_ts": 2, "state_key": "@bob:example.org", "content": { "membership": "join" }
3931 }] }
3932 } } }
3933 })
3934 }
3935
3936 #[test]
3937 fn a_member_joining_an_encrypted_room_is_tracked_and_queried_right_away() {
3938 let mut core = open_fresh_core();
3939 let bob = UserId::parse("@bob:example.org").expect("valid user id");
3940 deliver_sync(&mut core, bob_joins_room_body(true));
3941 let tracked = core.store.tracked_users().expect("no error");
3942 assert!(tracked.iter().any(|(user_id, outdated)| *user_id == bob && *outdated), "bob is tracked with an outdated device list: {tracked:?}");
3943
3944 flush_and_ack(&mut core);
3945 let requests = core.releasable_requests(0);
3946 let query = requests.iter().find(|r| r.kind == OutgoingRequestKind::KeysQuery).expect("a /keys/query was enqueued for the new member");
3947 assert!(query.body.as_ref().is_some_and(|body| body["device_keys"].get(bob.as_str()).is_some()), "the query names bob: {:?}", query.body);
3948 }
3949
3950 #[test]
3951 fn a_member_joining_a_plaintext_room_is_not_tracked() {
3952 let mut core = open_fresh_core();
3953 deliver_sync(&mut core, bob_joins_room_body(false));
3954 assert!(core.store.tracked_users().expect("no error").is_empty());
3955 flush_and_ack(&mut core);
3956 assert!(core.releasable_requests(0).iter().all(|r| r.kind != OutgoingRequestKind::KeysQuery));
3957 }
3958
3959 #[test]
3960 fn dispatch_mark_read_and_set_typing_produce_outgoing_requests() {
3961 let mut core = open_fresh_core();
3962 flush_and_ack(&mut core);
3963 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3964 let event_id = EventId::parse("$1:example.org").expect("valid event id");
3965
3966 core.dispatch(MessengerCommand::MarkRead { room_id: room_id.clone(), event_id }, 0).expect("dispatch succeeds");
3967 core.dispatch(MessengerCommand::SetTyping { room_id: room_id.clone(), typing: true }, 0).expect("dispatch succeeds");
3968 flush_and_ack(&mut core);
3969
3970 let requests = core.releasable_requests(0);
3971 assert!(requests.iter().any(|r| r.kind == OutgoingRequestKind::ReadMarkers));
3972 assert!(requests.iter().any(|r| r.kind == OutgoingRequestKind::Typing));
3973 }
3974
3975 #[test]
3976 fn snapshot_reflects_the_latest_applied_sync() {
3977 let mut core = open_fresh_core();
3978 flush_and_ack(&mut core);
3979 core.releasable_requests(0);
3980 flush_and_ack(&mut core);
3981 let requests = core.releasable_requests(0);
3982 let sync_request = requests.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("sync enqueued");
3983
3984 let body = serde_json::json!({
3985 "next_batch": "s1",
3986 "rooms": {
3987 "join": {
3988 "!r:example.org": {
3989 "state": { "events": [] },
3990 "timeline": {
3991 "events": [
3992 {
3993 "event_id": "$1:example.org",
3994 "type": "m.room.message",
3995 "sender": "@alice:example.org",
3996 "origin_server_ts": 1,
3997 "content": { "msgtype": "m.text", "body": "hi" }
3998 }
3999 ]
4000 },
4001 "unread_notifications": { "notification_count": 1, "highlight_count": 0 }
4002 }
4003 }
4004 }
4005 })
4006 .to_string();
4007 let events = core.on_response(sync_request.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
4008
4009 assert!(events.contains(&MessengerEvent::RoomsChanged));
4010 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4011 assert!(events.contains(&MessengerEvent::TimelineChanged { room_id: room_id.clone() }));
4012 assert!(events.contains(&MessengerEvent::UnreadChanged { room_id: room_id.clone() }));
4013
4014 let timeline = core.timeline(&room_id).expect("timeline present");
4015 assert_eq!(timeline.items().len(), 1);
4016 assert_eq!(core.room_state(&room_id).expect("room present").unread_count(), 1);
4017 assert_eq!(core.change_counter(), events.len() as u64);
4018 assert!(core.events().is_empty());
4021 }
4022
4023 #[test]
4024 fn dispatch_set_account_data_updates_optimistically_and_sends_a_put() {
4025 let mut core = open_fresh_core();
4026 flush_and_ack(&mut core);
4027 let content = serde_json::json!({ "note": "keep" });
4028 core.dispatch(
4029 MessengerCommand::SetAccountData { event_type: "com.example.prefs".to_string(), content: content.clone() },
4030 0,
4031 )
4032 .expect("dispatch succeeds");
4033
4034 assert_eq!(
4035 core.global_account_data("com.example.prefs"),
4036 Some(&content),
4037 "SetAccountData updates the core's own cache before any round trip"
4038 );
4039
4040 flush_and_ack(&mut core);
4041 let requests = core.releasable_requests(0);
4042 let request = requests.iter().find(|r| r.kind == OutgoingRequestKind::AccountData).expect("an AccountData request was enqueued");
4043 assert_eq!(request.method, HttpMethod::Put);
4044 assert_eq!(request.path, "/_matrix/client/v3/user/%40alice%3Aexample.org/account_data/com.example.prefs");
4045 assert_eq!(request.body.as_ref(), Some(&content));
4046 }
4047
4048 #[test]
4049 fn dispatch_set_room_account_data_updates_optimistically_and_sends_a_put() {
4050 let mut core = open_fresh_core();
4051 flush_and_ack(&mut core);
4052 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4053 let content = serde_json::json!({ "flag": true });
4054 core.dispatch(
4055 MessengerCommand::SetRoomAccountData {
4056 room_id: room_id.clone(),
4057 event_type: "com.example.flag".to_string(),
4058 content: content.clone(),
4059 },
4060 0,
4061 )
4062 .expect("dispatch succeeds");
4063
4064 assert_eq!(
4065 core.room_account_data(&room_id, "com.example.flag"),
4066 Some(&content),
4067 "SetRoomAccountData updates the core's own cache before any round trip"
4068 );
4069
4070 flush_and_ack(&mut core);
4071 let requests = core.releasable_requests(0);
4072 let request =
4073 requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomAccountData).expect("a RoomAccountData request was enqueued");
4074 assert_eq!(request.method, HttpMethod::Put);
4075 assert_eq!(
4076 request.path,
4077 "/_matrix/client/v3/user/%40alice%3Aexample.org/rooms/%21r%3Aexample.org/account_data/com.example.flag"
4078 );
4079 assert_eq!(request.body.as_ref(), Some(&content));
4080 }
4081
4082 #[test]
4083 fn dispatch_search_public_rooms_sends_the_expected_request() {
4084 let mut core = open_fresh_core();
4085 flush_and_ack(&mut core);
4086 core.dispatch(MessengerCommand::SearchPublicRooms { term: "trading".to_string() }, 0).expect("dispatch succeeds");
4087 flush_and_ack(&mut core);
4088 let requests = core.releasable_requests(0);
4089 let request = requests.iter().find(|r| r.kind == OutgoingRequestKind::PublicRooms).expect("a PublicRooms request was enqueued");
4090 assert_eq!(request.method, HttpMethod::Post);
4091 assert_eq!(request.path, "/_matrix/client/v3/publicRooms");
4092 assert_eq!(request.body, Some(serde_json::json!({ "filter": { "generic_search_term": "trading" }, "limit": 50 })));
4093 }
4094
4095 #[test]
4096 fn dispatch_search_users_sends_the_expected_request() {
4097 let mut core = open_fresh_core();
4098 flush_and_ack(&mut core);
4099 core.dispatch(MessengerCommand::SearchUsers { term: "bob".to_string() }, 0).expect("dispatch succeeds");
4100 flush_and_ack(&mut core);
4101 let requests = core.releasable_requests(0);
4102 let request =
4103 requests.iter().find(|r| r.kind == OutgoingRequestKind::UserDirectorySearch).expect("a UserDirectorySearch request was enqueued");
4104 assert_eq!(request.method, HttpMethod::Post);
4105 assert_eq!(request.path, "/_matrix/client/v3/user_directory/search");
4106 assert_eq!(request.body, Some(serde_json::json!({ "search_term": "bob", "limit": 20 })));
4107 }
4108
4109 #[test]
4110 fn public_rooms_response_superseded_by_a_later_search_is_dropped() {
4111 let mut core = open_fresh_core();
4112 flush_and_ack(&mut core);
4113 core.dispatch(MessengerCommand::SearchPublicRooms { term: "first".to_string() }, 0).expect("dispatch succeeds");
4114 flush_and_ack(&mut core);
4115 let first_request = core
4116 .releasable_requests(0)
4117 .into_iter()
4118 .find(|r| r.kind == OutgoingRequestKind::PublicRooms)
4119 .expect("the first search's own request");
4120
4121 core.dispatch(MessengerCommand::SearchPublicRooms { term: "second".to_string() }, 0).expect("dispatch succeeds");
4124 flush_and_ack(&mut core);
4125 let second_request = core
4126 .releasable_requests(0)
4127 .into_iter()
4128 .find(|r| r.kind == OutgoingRequestKind::PublicRooms)
4129 .expect("the second search's own request");
4130
4131 let stale_body = serde_json::json!({ "chunk": [{ "room_id": "!stale:example.org", "num_joined_members": 1 }] });
4133 core.on_response(first_request.id, HttpResponseDescriptor { status: 200, body: serde_json::to_vec(&stale_body).expect("json") }, 0);
4134 assert!(core.public_rooms_result().is_empty(), "a response to a superseded search is dropped");
4135
4136 let fresh_body = serde_json::json!({ "chunk": [{ "room_id": "!fresh:example.org", "num_joined_members": 2 }] });
4138 core.on_response(second_request.id, HttpResponseDescriptor { status: 200, body: serde_json::to_vec(&fresh_body).expect("json") }, 0);
4139 assert_eq!(core.public_rooms_result().len(), 1);
4140 assert_eq!(core.public_rooms_result()[0].room_id, RoomId::parse("!fresh:example.org").expect("valid room id"));
4141 }
4142
4143 fn is_account_data_write(request: &OutgoingRequest) -> bool {
4144 matches!(request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData)
4145 }
4146
4147 #[test]
4148 fn pin_then_archive_back_to_back_keeps_both_tags() {
4149 let mut core = open_fresh_core();
4150 flush_and_ack(&mut core);
4151 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4152 for tag in ["m.favourite", "u.example.archived"] {
4153 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4154 .expect("dispatch succeeds");
4155 }
4156
4157 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4159 assert!(cached["tags"].get("m.favourite").is_some());
4160 assert!(cached["tags"].get("u.example.archived").is_some());
4161
4162 flush_and_ack(&mut core);
4163 let first_release = core.releasable_requests(0);
4164 let first_writes: Vec<&OutgoingRequest> = first_release.iter().filter(|r| is_account_data_write(r)).collect();
4165 assert_eq!(first_writes.len(), 1, "the second PUT is not released while the first is in flight");
4166 let first_body = first_writes[0].body.clone().expect("a body");
4167 assert!(first_body["tags"].get("m.favourite").is_some());
4168 assert!(first_body["tags"].get("u.example.archived").is_none(), "the first PUT carries only what existed when it was built");
4169
4170 core.on_response(first_writes[0].id.clone(), HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }, 0);
4171 flush_and_ack(&mut core);
4172 let second_release = core.releasable_requests(0);
4173 let second = second_release.iter().find(|r| is_account_data_write(r)).expect("the second PUT is released after the first completed");
4174 let second_body = second.body.clone().expect("a body");
4175 assert!(second_body["tags"].get("m.favourite").is_some(), "the later PUT still carries the earlier tag");
4176 assert!(second_body["tags"].get("u.example.archived").is_some());
4177 }
4178
4179 #[test]
4180 fn account_data_lane_is_fifo_single_flight() {
4181 let mut core = open_fresh_core();
4182 flush_and_ack(&mut core);
4183 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4184 core.dispatch(MessengerCommand::SetAccountData { event_type: "org.t.one".to_string(), content: serde_json::json!({}) }, 0)
4185 .expect("dispatch succeeds");
4186 core.dispatch(
4187 MessengerCommand::SetRoomAccountData {
4188 room_id: room_id.clone(),
4189 event_type: "org.t.two".to_string(),
4190 content: serde_json::json!({}),
4191 },
4192 0,
4193 )
4194 .expect("dispatch succeeds");
4195 core.dispatch(MessengerCommand::SetAccountData { event_type: "org.t.three".to_string(), content: serde_json::json!({}) }, 0)
4196 .expect("dispatch succeeds");
4197 core.dispatch(MessengerCommand::RemoveTag { room_id: room_id.clone(), tag: "m.favourite".to_string() }, 0)
4198 .expect("dispatch succeeds");
4199 flush_and_ack(&mut core);
4200
4201 let mut released_paths = Vec::new();
4202 for _ in 0..4 {
4203 let release = core.releasable_requests(0);
4204 let writes: Vec<&OutgoingRequest> = release.iter().filter(|r| is_account_data_write(r)).collect();
4205 assert_eq!(writes.len(), 1, "exactly one account-data write in flight at a time");
4206 released_paths.push(writes[0].path.clone());
4207 core.on_response(writes[0].id.clone(), HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }, 0);
4208 flush_and_ack(&mut core);
4209 }
4210 assert!(released_paths[0].ends_with("/account_data/org.t.one"));
4211 assert!(released_paths[1].ends_with("/account_data/org.t.two"));
4212 assert!(released_paths[2].ends_with("/account_data/org.t.three"));
4213 assert!(released_paths[3].ends_with("/account_data/m.tag"));
4214 assert!(core.releasable_requests(0).iter().all(|r| !is_account_data_write(r)), "the lane is drained");
4215 }
4216
4217 fn deliver_sync(core: &mut MessengerCore<InsecurePlainCodecForTests>, body: serde_json::Value) -> Vec<OutgoingRequest> {
4222 let mut others = Vec::new();
4223 for _ in 0..3 {
4224 flush_and_ack(core);
4225 let mut sync_request = None;
4226 for request in core.releasable_requests(0) {
4227 if request.kind == OutgoingRequestKind::Sync && sync_request.is_none() {
4228 sync_request = Some(request);
4229 } else {
4230 others.push(request);
4231 }
4232 }
4233 if let Some(sync_request) = sync_request {
4234 let body = serde_json::to_vec(&body).expect("valid JSON");
4235 core.on_response(sync_request.id, HttpResponseDescriptor { status: 200, body }, 0);
4236 return others;
4237 }
4238 }
4239 panic!("no sync request became releasable");
4240 }
4241
4242 fn tag_sync_body(room_id: &str, tags: serde_json::Value, next_batch: &str) -> serde_json::Value {
4243 let mut join = serde_json::Map::new();
4244 join.insert(
4245 room_id.to_string(),
4246 serde_json::json!({ "account_data": { "events": [{ "type": "m.tag", "content": { "tags": tags } }] } }),
4247 );
4248 serde_json::json!({ "next_batch": next_batch, "rooms": { "join": serde_json::Value::Object(join) } })
4249 }
4250
4251 fn ok_response() -> HttpResponseDescriptor {
4252 HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }
4253 }
4254
4255 #[test]
4256 fn sync_echo_of_an_older_tag_write_does_not_clobber_a_newer_optimistic_value() {
4257 let mut core = open_fresh_core();
4258 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4259 for tag in ["m.favourite", "u.example.archived"] {
4260 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4261 .expect("dispatch succeeds");
4262 }
4263
4264 let released = deliver_sync(&mut core, tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {} }), "s1"));
4266 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4267 assert!(cached["tags"].get("u.example.archived").is_some(), "the older echo must not clobber the newer optimistic value");
4268 assert!(cached["tags"].get("m.favourite").is_some());
4269 let key = (Some(room_id.clone()), "m.tag".to_string());
4270 assert_eq!(
4271 core.account_data_guard.get(&key).expect("writes are outstanding").server_value,
4272 Some(serde_json::json!({ "tags": { "m.favourite": {} } })),
4273 "the echo is remembered as the server's value"
4274 );
4275
4276 let first = released.iter().find(|r| r.kind == OutgoingRequestKind::RoomAccountData).expect("first write released");
4279 core.on_response(first.id.clone(), ok_response(), 0);
4280 flush_and_ack(&mut core);
4281 let second = core
4282 .releasable_requests(0)
4283 .into_iter()
4284 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4285 .expect("second write released after the first");
4286 core.on_response(second.id, ok_response(), 0);
4287 assert!(core.account_data_guard.is_empty());
4288 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4289 assert!(cached["tags"].get("m.favourite").is_some() && cached["tags"].get("u.example.archived").is_some());
4290
4291 deliver_sync(
4293 &mut core,
4294 tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {}, "u.example.archived": {} }), "s2"),
4295 );
4296 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4297 assert!(cached["tags"].get("u.example.archived").is_some());
4298 }
4299
4300 #[test]
4301 fn terminal_account_data_failure_restores_the_server_value() {
4302 let mut core = open_fresh_core();
4303 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4304 deliver_sync(&mut core, tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {} }), "s1"));
4305
4306 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: "u.example.archived".to_string(), order: None }, 0)
4307 .expect("dispatch succeeds");
4308 assert!(core.room_account_data(&room_id, "m.tag").expect("cached")["tags"].get("u.example.archived").is_some());
4309 flush_and_ack(&mut core);
4310 let write = core
4311 .releasable_requests(0)
4312 .into_iter()
4313 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4314 .expect("the write is released");
4315
4316 let counter_before = core.change_counter();
4317 let forbidden =
4318 HttpResponseDescriptor { status: 403, body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec() };
4319 let events = core.on_response(write.id, forbidden, 0);
4320 assert!(core.account_data_guard.is_empty());
4321 let restored = core.room_account_data(&room_id, "m.tag").expect("the server's value is back");
4322 assert!(restored["tags"].get("u.example.archived").is_none(), "the failed write's optimistic value is gone");
4323 assert!(restored["tags"].get("m.favourite").is_some());
4324 assert!(events.contains(&MessengerEvent::RoomsChanged));
4325 assert!(core.change_counter() > counter_before, "the UI is told to re-render");
4326
4327 for tag in ["u.a", "u.b"] {
4331 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4332 .expect("dispatch succeeds");
4333 }
4334 flush_and_ack(&mut core);
4335 let first = core
4336 .releasable_requests(0)
4337 .into_iter()
4338 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4339 .expect("first write released");
4340 let forbidden =
4341 HttpResponseDescriptor { status: 403, body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec() };
4342 core.on_response(first.id, forbidden, 0);
4343 let cached = core.room_account_data(&room_id, "m.tag").expect("cached");
4344 assert!(cached["tags"].get("u.a").is_some() && cached["tags"].get("u.b").is_some(), "cache untouched while a write is pending");
4345 flush_and_ack(&mut core);
4346 let second = core
4347 .releasable_requests(0)
4348 .into_iter()
4349 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4350 .expect("second write released");
4351 core.on_response(second.id, ok_response(), 0);
4352 let cached = core.room_account_data(&room_id, "m.tag").expect("cached");
4353 assert!(cached["tags"].get("u.b").is_some(), "the last write succeeded: its optimistic value stands");
4354 }
4355
4356 #[test]
4357 fn pending_account_data_writes_rebuild_their_guard_after_a_restart() {
4358 let mut core = open_fresh_core();
4359 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4360 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: "m.favourite".to_string(), order: None }, 0)
4361 .expect("dispatch succeeds");
4362 core.dispatch(
4363 MessengerCommand::SetAccountData { event_type: "org.t.one".to_string(), content: serde_json::json!({ "n": 1 }) },
4364 0,
4365 )
4366 .expect("dispatch succeeds");
4367
4368 let mut records = Vec::new();
4370 while let Some(batch) = core.take_flush_batch() {
4371 records.extend(batch.records.clone());
4372 core.ack_flush(batch.id);
4373 }
4374 let reopened = MessengerCore::open(
4375 records,
4376 InsecurePlainCodecForTests,
4377 test_config(),
4378 CoreSecrets::default(),
4379 0,
4380 Box::new(FixedJitter(0.0)),
4381 )
4382 .expect("reopen succeeds");
4383
4384 assert_eq!(reopened.account_data_guard.len(), 2, "one guard per key with a pending write");
4385 assert!(reopened.account_data_guard.values().all(|guard| guard.pending == 1 && guard.server_value.is_none()));
4386 assert_eq!(reopened.global_account_data("org.t.one"), Some(&serde_json::json!({ "n": 1 })));
4387 assert!(reopened.room_account_data(&room_id, "m.tag").expect("cached")["tags"].get("m.favourite").is_some());
4388 }
4389
4390 type TestCore = MessengerCore<InsecurePlainCodecForTests>;
4395
4396 fn room(id: &str) -> RoomId {
4397 RoomId::parse(id).expect("valid room id")
4398 }
4399
4400 fn event(id: &str) -> EventId {
4401 EventId::parse(id).expect("valid event id")
4402 }
4403
4404 fn add_room(core: &mut TestCore, room_id: &RoomId, encrypted: bool, name: Option<&str>) {
4407 let mut state = RoomState::new();
4408 if encrypted {
4409 state.encryption = Some(crate::wire::events::RoomEncryptionContent {
4410 algorithm: "m.megolm.v1.aes-sha2".to_string(),
4411 rotation_period_ms: 604_800_000,
4412 rotation_period_msgs: 100,
4413 });
4414 }
4415 state.name = name.map(str::to_string);
4416 core.rooms.insert(room_id.clone(), state);
4417 core.timelines.entry(room_id.clone()).or_default();
4418 }
4419
4420 fn mark_as_channel(core: &mut TestCore, room_id: &RoomId) {
4423 let Some(state) = core.rooms.get_mut(room_id) else { return };
4424 state
4425 .apply_state_event(&state_event_raw(
4426 "m.room.join_rules",
4427 "",
4428 "@alice:example.org",
4429 serde_json::json!({ "join_rule": "public" }),
4430 ))
4431 .expect("join_rules");
4432 state
4433 .apply_state_event(&state_event_raw(
4434 "m.room.power_levels",
4435 "",
4436 "@alice:example.org",
4437 serde_json::json!({ "events_default": 50, "users_default": 0 }),
4438 ))
4439 .expect("power_levels");
4440 }
4441
4442 fn state_event_raw(event_type: &str, state_key: &str, sender: &str, content: serde_json::Value) -> RawEvent {
4443 serde_json::from_value(serde_json::json!({
4444 "event_id": format!("${event_type}:example.org"),
4445 "type": event_type,
4446 "sender": sender,
4447 "origin_server_ts": 1,
4448 "state_key": state_key,
4449 "content": content,
4450 }))
4451 .expect("valid raw event")
4452 }
4453
4454 fn raw_event(event_id: &str, event_type: &str, sender: &str, content: serde_json::Value) -> RawEvent {
4455 serde_json::from_value(serde_json::json!({
4456 "event_id": event_id,
4457 "type": event_type,
4458 "sender": sender,
4459 "origin_server_ts": 1,
4460 "content": content,
4461 }))
4462 .expect("valid raw event")
4463 }
4464
4465 fn seed_plain(core: &mut TestCore, room_id: &RoomId, event_id: &str, sender: &str, event_type: &str, content: serde_json::Value) {
4467 let raw = raw_event(event_id, event_type, sender, content);
4468 core.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(&[raw], false, None);
4469 }
4470
4471 fn seed_sealed(core: &mut TestCore, room_id: &RoomId, event_id: &str, sender: &str) {
4473 let content = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2", "ciphertext": "AAAA", "session_id": "s1" });
4474 seed_plain(core, room_id, event_id, sender, "m.room.encrypted", content);
4475 }
4476
4477 fn seed_decrypted(
4480 core: &mut TestCore,
4481 room_id: &RoomId,
4482 event_id_str: &str,
4483 sender: &str,
4484 event_type: &str,
4485 content: serde_json::Value,
4486 ) {
4487 seed_sealed(core, room_id, event_id_str, sender);
4488 if let Some(timeline) = core.timelines.get_mut(room_id) {
4489 timeline.set_decrypted_event(&event(event_id_str), event_type, &content);
4490 }
4491 }
4492
4493 fn sent_room_event(core: &mut TestCore) -> (String, serde_json::Value) {
4497 flush_and_ack(core);
4498 let request = core
4499 .releasable_requests(0)
4500 .into_iter()
4501 .find(|request| request.kind == OutgoingRequestKind::RoomSend)
4502 .expect("a room send is releasable");
4503 let confirmed = format!("$sent{}:example.org", core.counters.next_request_id);
4504 let body = serde_json::json!({ "event_id": confirmed }).to_string().into_bytes();
4505 core.on_response(request.id.clone(), HttpResponseDescriptor { status: 200, body }, 0);
4506 (request.path, request.body.expect("a room send has a body"))
4507 }
4508
4509 fn forward(core: &mut TestCore, from: &RoomId, event_id: &str, to: &RoomId) -> Result<(), MessengerError> {
4510 core.dispatch(
4511 MessengerCommand::Forward { from_room: from.clone(), event_id: event(event_id), to_room: to.clone(), txn_id: None },
4512 0,
4513 )
4514 }
4515
4516 fn last_item(core: &TestCore, room_id: &RoomId) -> crate::room::timeline::TimelineItem {
4517 core.timeline(room_id).and_then(|timeline| timeline.items().last()).expect("the room has an item").clone()
4518 }
4519
4520 #[test]
4521 fn forward_from_encrypted_room_carries_only_the_forwarded_flag() {
4522 let mut core = open_fresh_core();
4523 let dm = room("!dm:example.org");
4524 let target = room("!target:example.org");
4525 add_room(&mut core, &dm, true, Some("Secret DM name"));
4526 add_room(&mut core, &target, false, None);
4527 seed_decrypted(
4528 &mut core,
4529 &dm,
4530 "$src:example.org",
4531 "@bob:example.org",
4532 "m.room.message",
4533 serde_json::json!({
4534 "msgtype": "m.text",
4535 "body": "quarterly numbers",
4536 "m.relates_to": { "m.in_reply_to": { "event_id": "$parent:example.org" } },
4537 "m.mentions": { "user_ids": ["@carol:example.org"] },
4538 }),
4539 );
4540
4541 forward(&mut core, &dm, "$src:example.org", &target).expect("a decrypted message is forwardable");
4542
4543 let (path, body) = sent_room_event(&mut core);
4544 assert!(path.contains("/send/m.room.message/"), "the event type is kept: {path}");
4545 assert_eq!(
4546 body,
4547 serde_json::json!({ "msgtype": "m.text", "body": "quarterly numbers", "forwarded": true }),
4548 "the text plus the bare flag; the reply pointer and the mentions are stripped"
4549 );
4550 let wire = body.to_string();
4551 for leaked in ["bob", "carol", "!dm", "Secret DM name", "$parent"] {
4552 assert!(!wire.contains(leaked), "{leaked:?} must not travel with a forward out of an encrypted room: {wire}");
4553 }
4554 let echo = last_item(&core, &target);
4555 assert_eq!(echo.forwarded, Some(Forwarded::Hidden), "the forwarder's own echo shows the same marker");
4556 }
4557
4558 #[test]
4559 fn forward_from_public_channel_carries_room_id_and_name_no_sender() {
4560 let mut core = open_fresh_core();
4561 let channel = room("!chan:example.org");
4562 let target = room("!target:example.org");
4563 add_room(&mut core, &channel, true, Some("Announcements"));
4564 mark_as_channel(&mut core, &channel);
4565 add_room(&mut core, &target, false, None);
4566 seed_plain(
4567 &mut core,
4568 &channel,
4569 "$post:example.org",
4570 "@dave:example.org",
4571 "m.room.message",
4572 serde_json::json!({
4573 "msgtype": "m.text",
4574 "body": "BTC breaks out",
4575 "m.mentions": { "room": true },
4576 "m.relates_to": { "m.in_reply_to": { "event_id": "$older:example.org" } },
4577 }),
4578 );
4579
4580 forward(&mut core, &channel, "$post:example.org", &target).expect("a channel post is forwardable");
4581
4582 let (_, body) = sent_room_event(&mut core);
4583 assert_eq!(
4584 body,
4585 serde_json::json!({
4586 "msgtype": "m.text",
4587 "body": "BTC breaks out",
4588 "forwarded_from": { "room_id": "!chan:example.org", "room_name": "Announcements" },
4589 })
4590 );
4591 assert!(!body.to_string().contains("dave"), "the original sender never travels");
4592 assert_eq!(
4593 last_item(&core, &target).forwarded,
4594 Some(Forwarded::Channel { room_id: channel.clone(), room_name: "Announcements".to_string() })
4595 );
4596 }
4597
4598 #[test]
4599 fn forward_from_an_unnamed_room_never_lists_member_names() {
4600 let mut core = open_fresh_core();
4601 let channel = room("!chan:example.org");
4602 let target = room("!target:example.org");
4603 add_room(&mut core, &channel, true, None);
4604 mark_as_channel(&mut core, &channel);
4605 if let Some(state) = core.rooms.get_mut(&channel) {
4606 state.members.insert(
4607 UserId::parse("@erin:example.org").expect("valid user id"),
4608 crate::room::state::MemberState {
4609 membership: Membership::Join,
4610 displayname: Some("Erin Private".to_string()),
4611 is_direct: false,
4612 invited_by: None,
4613 },
4614 );
4615 }
4616 add_room(&mut core, &target, false, None);
4617 seed_plain(&mut core, &channel, "$post:example.org", "@erin:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "hi" }));
4618
4619 forward(&mut core, &channel, "$post:example.org", &target).expect("forwardable");
4620
4621 let (_, body) = sent_room_event(&mut core);
4622 let name = body["forwarded_from"]["room_name"].as_str().expect("a name is sent");
4623 assert!(!name.contains("Erin"), "an unnamed room is attributed by a generic label, not by its members: {name}");
4624 }
4625
4626 #[test]
4627 fn forward_replaces_the_sources_own_forward_marker() {
4628 let mut core = open_fresh_core();
4629 let dm = room("!dm:example.org");
4630 let target = room("!target:example.org");
4631 add_room(&mut core, &dm, true, None);
4632 add_room(&mut core, &target, false, None);
4633 seed_decrypted(
4634 &mut core,
4635 &dm,
4636 "$src:example.org",
4637 "@bob:example.org",
4638 "m.room.message",
4639 serde_json::json!({
4640 "msgtype": "m.text",
4641 "body": "second hop",
4642 "forwarded_from": { "room_id": "!older:example.org", "room_name": "Older channel" },
4643 }),
4644 );
4645
4646 forward(&mut core, &dm, "$src:example.org", &target).expect("forwardable");
4647
4648 let (_, body) = sent_room_event(&mut core);
4649 assert_eq!(body, serde_json::json!({ "msgtype": "m.text", "body": "second hop", "forwarded": true }));
4650 }
4651
4652 #[test]
4653 fn forward_of_edited_item_sends_the_latest_text() {
4654 let mut core = open_fresh_core();
4655 let channel = room("!chan:example.org");
4656 let dm = room("!dm:example.org");
4657 let target = room("!target:example.org");
4658 add_room(&mut core, &channel, false, Some("Announcements"));
4659 add_room(&mut core, &dm, true, None);
4660 add_room(&mut core, &target, false, None);
4661
4662 seed_plain(&mut core, &channel, "$orig:example.org", "@dave:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "typo text" }));
4664 seed_plain(
4665 &mut core,
4666 &channel,
4667 "$edit:example.org",
4668 "@dave:example.org",
4669 "m.room.message",
4670 serde_json::json!({
4671 "msgtype": "m.text",
4672 "body": "* fixed text",
4673 "m.new_content": { "msgtype": "m.text", "body": "fixed text" },
4674 "m.relates_to": { "rel_type": "m.replace", "event_id": "$orig:example.org" },
4675 }),
4676 );
4677 forward(&mut core, &channel, "$orig:example.org", &target).expect("forwardable");
4678 let (_, body) = sent_room_event(&mut core);
4679 assert_eq!(body["body"], "fixed text", "the latest edit's text, not the original and not the `* ` fallback");
4680 assert!(body.get("m.new_content").is_none() && body.get("m.relates_to").is_none());
4681
4682 seed_decrypted(&mut core, &dm, "$secret:example.org", "@bob:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "typo secret" }));
4684 seed_sealed(&mut core, &dm, "$secret-edit:example.org", "@bob:example.org");
4685 let applied = core.timelines.get_mut(&dm).expect("timeline").apply_decrypted_relation(
4686 &event("$secret-edit:example.org"),
4687 UserId::parse("@bob:example.org").expect("valid user id"),
4688 2,
4689 RelatesTo::Replace { event_id: event("$secret:example.org") },
4690 Some(serde_json::json!({ "msgtype": "m.text", "body": "fixed secret" })),
4691 );
4692 assert!(applied, "the decrypted edit folds onto its target");
4693 forward(&mut core, &dm, "$secret:example.org", &target).expect("forwardable");
4694 let (_, body) = sent_room_event(&mut core);
4695 assert_eq!(body, serde_json::json!({ "msgtype": "m.text", "body": "fixed secret", "forwarded": true }));
4696 }
4697
4698 #[test]
4699 fn forward_of_unknown_msgtype_keeps_its_fields() {
4700 let mut core = open_fresh_core();
4701 let dm = room("!dm:example.org");
4702 let target = room("!target:example.org");
4703 add_room(&mut core, &dm, true, None);
4704 add_room(&mut core, &target, false, None);
4705 let card = serde_json::json!({
4706 "msgtype": "com.example.card",
4707 "body": "opaque",
4708 "note": "kept raw",
4709 });
4710 seed_decrypted(&mut core, &dm, "$card:example.org", "@bob:example.org", "m.room.message", card.clone());
4711
4712 forward(&mut core, &dm, "$card:example.org", &target).expect("an unrecognized msgtype is still an m.room.message");
4713
4714 let (path, body) = sent_room_event(&mut core);
4715 assert!(path.contains("/send/m.room.message/"));
4716 let mut expected = card;
4717 expected["forwarded"] = serde_json::Value::Bool(true);
4718 assert_eq!(body, expected, "unrecognized msgtype fields pass through plus the marker");
4719 assert!(matches!(last_item(&core, &target).content, ItemContent::Unknown), "the echo does not invent a card type");
4720 }
4721
4722 #[test]
4723 fn forward_refuses_redacted_sealed_unknown_and_missing() {
4724 let mut core = open_fresh_core();
4725 let dm = room("!dm:example.org");
4726 let target = room("!target:example.org");
4727 add_room(&mut core, &dm, true, None);
4728 add_room(&mut core, &target, false, None);
4729
4730 seed_decrypted(&mut core, &dm, "$redacted:example.org", "@bob:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "gone" }));
4731 core.timelines.get_mut(&dm).expect("timeline").apply_redaction(&event("$redacted:example.org"));
4732 seed_sealed(&mut core, &dm, "$sealed:example.org", "@bob:example.org");
4733 seed_sealed(&mut core, &dm, "$utd:example.org", "@bob:example.org");
4734 core.timelines
4735 .get_mut(&dm)
4736 .expect("timeline")
4737 .set_decrypted(&event("$utd:example.org"), ItemContent::Undecryptable { reason: "MissingSession".to_string() });
4738 seed_plain(&mut core, &dm, "$call:example.org", "@bob:example.org", "m.call.invite", serde_json::json!({ "call_id": "c1" }));
4739
4740 for id in ["$redacted:example.org", "$sealed:example.org", "$utd:example.org", "$call:example.org", "$missing:example.org"] {
4741 match forward(&mut core, &dm, id, &target) {
4742 Err(MessengerError::IntentRefused(reason)) => {
4743 assert!(reason.contains(id), "the refusal names the event {id}: {reason}");
4744 }
4745 other => panic!("forwarding {id} must be refused, got {other:?}"),
4746 }
4747 }
4748 match forward(&mut core, &room("!nowhere:example.org"), "$redacted:example.org", &target) {
4749 Err(MessengerError::IntentRefused(reason)) => assert!(reason.contains("$redacted:example.org"), "{reason}"),
4750 other => panic!("an unknown source room means a missing event, got {other:?}"),
4751 }
4752 match forward(&mut core, &dm, "$sealed:example.org", &room("!nowhere:example.org")) {
4753 Err(MessengerError::IntentRefused(reason)) => assert!(reason.contains("unknown"), "{reason}"),
4754 other => panic!("an unknown target room is refused, got {other:?}"),
4755 }
4756
4757 assert!(core.send_state.is_empty(), "a refused forward queues nothing");
4758 assert!(core.timeline(&target).is_some_and(|timeline| timeline.items().is_empty()), "and leaves no echo");
4759 }
4760}