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 {
319 name: String,
321 topic: Option<String>,
323 },
324}
325
326#[derive(Clone, Debug, PartialEq)]
332pub enum MessengerCommand {
333 SendMessage {
339 room_id: RoomId,
341 message: OutgoingMessage,
343 txn_id: Option<TxnId>,
346 },
347 Forward {
373 from_room: RoomId,
375 event_id: EventId,
377 to_room: RoomId,
379 txn_id: Option<TxnId>,
381 },
382 React {
386 room_id: RoomId,
388 target: EventId,
390 key: String,
392 },
393 Redact {
395 room_id: RoomId,
397 target: EventId,
399 reason: Option<String>,
401 },
402 RetrySend {
408 room_id: RoomId,
410 txn_id: TxnId,
412 },
413 CreateRoom {
415 kind: CreateRoomKind,
417 },
418 JoinRoom {
421 room_id: RoomId,
423 },
424 LeaveRoom {
426 room_id: RoomId,
428 },
429 Invite {
431 room_id: RoomId,
433 user_id: UserId,
435 },
436 Kick {
441 room_id: RoomId,
443 user_id: UserId,
445 reason: Option<String>,
447 },
448 SetPowerLevel {
451 room_id: RoomId,
453 user_id: UserId,
455 level: i64,
457 },
458 SetEventsDefault {
462 room_id: RoomId,
464 level: i64,
466 },
467 SetTag {
469 room_id: RoomId,
471 tag: String,
473 order: Option<f64>,
475 },
476 RemoveTag {
478 room_id: RoomId,
480 tag: String,
482 },
483 MarkRead {
486 room_id: RoomId,
488 event_id: EventId,
490 },
491 SetTyping {
494 room_id: RoomId,
496 typing: bool,
498 },
499 RetryDecryption {
504 room_id: RoomId,
506 event_id: EventId,
508 },
509 LoadOlder {
514 room_id: RoomId,
516 },
517 SetAccountData {
523 event_type: String,
525 content: serde_json::Value,
527 },
528 SetRoomAccountData {
537 room_id: RoomId,
539 event_type: String,
541 content: serde_json::Value,
543 },
544 SearchPublicRooms {
549 term: String,
551 },
552 SearchUsers {
557 term: String,
559 },
560}
561
562#[derive(Clone, Debug, PartialEq, Eq)]
567pub enum MessengerEvent {
568 RoomsChanged,
571 TimelineChanged {
573 room_id: RoomId,
575 },
576 TypingChanged {
578 room_id: RoomId,
580 },
581 ReceiptsChanged {
583 room_id: RoomId,
585 },
586 UnreadChanged {
588 room_id: RoomId,
590 },
591 DeviceKeyChanged {
595 user_id: UserId,
597 device_id: DeviceId,
599 },
600 PublicRoomsChanged,
604 UserSearchChanged,
608}
609
610#[derive(Clone)]
615struct PendingDecryptItem {
616 session_id: String,
617 sender: UserId,
618 origin_server_ts: i64,
619 content: MegolmEncryptedContent,
620}
621
622#[derive(Deserialize)]
628struct RoomMessagesResponseBody {
629 #[serde(default)]
630 chunk: Vec<RawEvent>,
631 #[serde(default)]
632 end: Option<String>,
633}
634
635#[derive(Deserialize)]
637struct SendResponseBody {
638 event_id: EventId,
639}
640
641#[derive(Deserialize)]
643struct CreateRoomResponseBody {
644 room_id: RoomId,
645}
646
647#[derive(Clone, Debug, PartialEq)]
650pub struct PublicRoomsResultEntry {
651 pub room_id: RoomId,
653 pub name: Option<String>,
655 pub topic: Option<String>,
657 pub num_joined_members: u64,
659}
660
661#[derive(Deserialize)]
663struct PublicRoomsResponseBody {
664 #[serde(default)]
665 chunk: Vec<PublicRoomsChunkEntry>,
666}
667
668#[derive(Deserialize)]
669struct PublicRoomsChunkEntry {
670 room_id: RoomId,
671 #[serde(default)]
672 name: Option<String>,
673 #[serde(default)]
674 topic: Option<String>,
675 #[serde(default)]
676 num_joined_members: u64,
677}
678
679#[derive(Clone, Debug, PartialEq)]
682pub struct UserDirectoryResultEntry {
683 pub user_id: UserId,
685 pub display_name: Option<String>,
687}
688
689#[derive(Deserialize)]
692struct UserDirectorySearchResponseBody {
693 #[serde(default)]
694 results: Vec<UserDirectorySearchResultEntry>,
695}
696
697#[derive(Deserialize)]
698struct UserDirectorySearchResultEntry {
699 user_id: UserId,
700 #[serde(default)]
701 display_name: Option<String>,
702}
703
704#[derive(Clone)]
709enum SendPayload {
710 Message(OutgoingMessage),
713 Event {
717 event_type: String,
719 content: serde_json::Value,
721 },
722 Reaction {
724 target: EventId,
726 key: String,
728 },
729 Redaction {
731 target: EventId,
733 reason: Option<String>,
735 },
736}
737
738struct RoomEventWire {
742 event_type: String,
743 content: serde_json::Value,
744 relates_to: Option<RelatesTo>,
747}
748
749impl SendPayload {
750 fn room_event(&self) -> Option<RoomEventWire> {
753 match self {
754 SendPayload::Message(message) => Some(RoomEventWire {
755 event_type: "m.room.message".to_string(),
756 content: message_wire_content(message),
757 relates_to: message_relates_to(message),
758 }),
759 SendPayload::Event { event_type, content } => {
760 Some(RoomEventWire { event_type: event_type.clone(), content: content.clone(), relates_to: None })
761 }
762 SendPayload::Reaction { .. } | SendPayload::Redaction { .. } => None,
763 }
764 }
765
766 fn is_room_event(&self) -> bool {
767 matches!(self, SendPayload::Message(_) | SendPayload::Event { .. })
768 }
769}
770
771#[derive(Clone)]
777enum WireSend {
778 Event {
780 event_type: String,
783 content: serde_json::Value,
785 },
786 Redact {
788 target: EventId,
790 reason: Option<String>,
792 },
793}
794
795#[derive(Clone)]
800enum SendPhase {
801 Queued,
805 AwaitingKeysQuery,
811 AwaitingKeysClaim {
814 devices: Vec<StoredDevice>,
818 },
819 AwaitingKeyShare {
824 pending: BTreeSet<RequestId>,
826 },
827 AwaitingSend,
829 Failed {
834 errcode: String,
836 },
837}
838
839#[derive(Clone)]
845struct PendingSend {
846 room_id: RoomId,
849 payload: SendPayload,
851 phase: SendPhase,
853 wire: Option<WireSend>,
855}
856
857type AccountDataKey = (Option<RoomId>, String);
860
861struct AccountDataGuard {
873 pending: u32,
875 server_value: Option<serde_json::Value>,
879}
880
881fn account_data_key_from_path(path: &str) -> Option<AccountDataKey> {
884 let segments: Vec<&str> = path.split('/').collect();
885 let n = segments.len();
886 if n < 4 || segments[n - 2] != "account_data" {
887 return None;
888 }
889 let event_type = percent_decode_segment(segments[n - 1])?;
890 if segments[n - 4] == "user" {
891 return Some((None, event_type));
892 }
893 if n >= 6 && segments[n - 4] == "rooms" && segments[n - 6] == "user" {
894 let room_id = RoomId::parse(percent_decode_segment(segments[n - 3])?).ok()?;
895 return Some((Some(room_id), event_type));
896 }
897 None
898}
899
900pub struct MessengerCore<C: RecordCodec> {
902 config: CoreConfig,
903 store: Store<C>,
904 account: OlmAccountState,
905 xsign: Option<CrossSigningState>,
907 xsign_inflight: bool,
908 xsign_attempts: u8,
909 outgoing: OutgoingQueue,
910 counters: Counters,
911 jitter: Box<dyn Jitter>,
912
913 rooms: BTreeMap<RoomId, RoomState>,
914 timelines: BTreeMap<RoomId, Timeline>,
915 direct_account_data: Option<DirectContent>,
916 global_account_data: BTreeMap<String, serde_json::Value>,
917 room_account_data: BTreeMap<RoomId, BTreeMap<String, serde_json::Value>>,
918 typing: BTreeMap<RoomId, Vec<UserId>>,
919 receipts: BTreeMap<RoomId, ReceiptContent>,
920
921 public_rooms_result: Vec<PublicRoomsResultEntry>,
924 latest_public_rooms_request: Option<RequestId>,
929 user_search_result: Vec<UserDirectoryResultEntry>,
932 latest_user_search_request: Option<RequestId>,
935
936 account_data_guard: BTreeMap<AccountDataKey, AccountDataGuard>,
940 account_data_write_keys: BTreeMap<RequestId, AccountDataKey>,
942
943 pending_decrypt: BTreeMap<RoomId, BTreeMap<EventId, PendingDecryptItem>>,
944 key_requests_sent: BTreeSet<(RoomId, String)>,
948 history_claims: BTreeMap<RequestId, Vec<StoredDevice>>,
951 history_claim_tried: BTreeSet<(UserId, DeviceId)>,
953 withheld_sessions: BTreeSet<(RoomId, String)>,
955 key_requests_answered: BTreeSet<(UserId, DeviceId, String)>,
957 unknown_sender_retry: VecDeque<ToDeviceEvent>,
958
959 typing_debounce: BTreeMap<RoomId, i64>,
964 pending_sends: BTreeMap<RoomId, VecDeque<TxnId>>,
970 send_state: BTreeMap<TxnId, PendingSend>,
975 send_request_owner: BTreeMap<RequestId, TxnId>,
979 pending_create_room: BTreeMap<RequestId, CreateRoomKind>,
983
984 pending_kinds: BTreeMap<RequestId, OutgoingRequestKind>,
990 pending_room_messages: BTreeMap<RequestId, RoomId>,
993 sync_request_id: Option<RequestId>,
996 sync_caught_up: bool,
1001 pending_flush_batches: VecDeque<FlushBatch>,
1008
1009 events: Vec<MessengerEvent>,
1010 change_counter: u64,
1011 ingest_error: Option<String>,
1016}
1017
1018impl<C: RecordCodec> MessengerCore<C> {
1019 pub fn sign_scoped(&self, domain: &str, payload: &[u8]) -> Result<(String, String), MessengerError> {
1022 self.account.sign_scoped(domain, payload)
1023 }
1024
1025 pub fn open(
1038 records: impl IntoIterator<Item = SealedRecord>,
1039 codec: C,
1040 config: CoreConfig,
1041 secrets: CoreSecrets,
1042 _now_ms: i64,
1043 jitter: Box<dyn Jitter>,
1044 ) -> Result<Self, MessengerError> {
1045 let _ = secrets;
1049 let mut store = Store::load(records, codec, config.device_id.clone())?;
1050 let mut outgoing = OutgoingQueue::load(&store)?;
1051 let account = OlmAccountState::load_or_create(&mut store)?;
1052
1053 let mut rooms = BTreeMap::new();
1054 let mut timelines = BTreeMap::new();
1055 for room_id in store.room_ids()? {
1056 if let Some(bytes) = store.room_state(&room_id)? {
1057 let state: RoomState = serde_json::from_slice(bytes)
1058 .map_err(|source| MessengerError::Crypto(format!("decode room state for {room_id}: {source}")))?;
1059 rooms.insert(room_id.clone(), state);
1060 timelines.insert(room_id, Timeline::new());
1061 }
1062 }
1063
1064 let pending_requests = store.pending_requests()?;
1065 let max_pending_request_seed = pending_requests.iter().filter_map(|req| req.request.id.as_seed()).max();
1066 outgoing.discard_kind(&mut store, OutgoingRequestKind::Sync)?;
1073 let persisted_counters = store.counters()?.unwrap_or_default();
1074 let mut counters = Counters {
1075 next_request_id: restore_counter(persisted_counters.next_request_id, max_pending_request_seed),
1076 next_txn_id: restore_counter(persisted_counters.next_txn_id, None),
1077 };
1078 if counters != persisted_counters {
1079 store.save_counters(counters)?;
1085 }
1086 counters.next_txn_id = counters.next_txn_id.max(fresh_txn_seed());
1092
1093 let mut core = Self {
1094 config,
1095 store,
1096 account,
1097 outgoing,
1098 counters,
1099 jitter,
1100 rooms,
1101 timelines,
1102 direct_account_data: None,
1103 global_account_data: BTreeMap::new(),
1104 room_account_data: BTreeMap::new(),
1105 typing: BTreeMap::new(),
1106 receipts: BTreeMap::new(),
1107 public_rooms_result: Vec::new(),
1108 latest_public_rooms_request: None,
1109 user_search_result: Vec::new(),
1110 latest_user_search_request: None,
1111 account_data_guard: BTreeMap::new(),
1112 account_data_write_keys: BTreeMap::new(),
1113 pending_decrypt: BTreeMap::new(),
1114 key_requests_sent: BTreeSet::new(),
1115 history_claims: BTreeMap::new(),
1116 history_claim_tried: BTreeSet::new(),
1117 withheld_sessions: BTreeSet::new(),
1118 key_requests_answered: BTreeSet::new(),
1119 unknown_sender_retry: VecDeque::new(),
1120 typing_debounce: BTreeMap::new(),
1121 pending_sends: BTreeMap::new(),
1122 send_state: BTreeMap::new(),
1123 send_request_owner: BTreeMap::new(),
1124 pending_create_room: BTreeMap::new(),
1125 pending_kinds: BTreeMap::new(),
1126 pending_room_messages: BTreeMap::new(),
1127 sync_request_id: None,
1128 sync_caught_up: false,
1129 pending_flush_batches: VecDeque::new(),
1130 events: Vec::new(),
1131 change_counter: 0,
1132 ingest_error: None,
1133 xsign: None,
1134 xsign_inflight: false,
1135 xsign_attempts: 0,
1136 };
1137 core.rebuild_account_data_guard(&pending_requests);
1138 Ok(core)
1139 }
1140
1141 fn rebuild_account_data_guard(&mut self, pending_requests: &[PendingRequest]) {
1149 let mut writes: Vec<&PendingRequest> = pending_requests
1150 .iter()
1151 .filter(|record| matches!(record.request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData))
1152 .collect();
1153 writes.sort_by(|a, b| a.request.id.cmp(&b.request.id));
1154 for record in writes {
1155 let Some(key) = account_data_key_from_path(&record.request.path) else { continue };
1156 let Some(body) = record.request.body.clone() else { continue };
1157 let guard = self.account_data_guard.entry(key.clone()).or_insert(AccountDataGuard { pending: 0, server_value: None });
1158 guard.pending += 1;
1159 self.store_account_data_cache(&key, Some(body));
1160 self.account_data_write_keys.insert(record.request.id.clone(), key);
1161 }
1162 }
1163
1164 fn next_request_id(&mut self) -> Result<RequestId, MessengerError> {
1168 let seed = self.counters.next_request_id;
1169 self.counters.next_request_id = seed.saturating_add(1);
1170 self.store.save_counters(self.counters)?;
1171 Ok(RequestId::next(seed))
1172 }
1173
1174 fn next_txn_id(&mut self) -> Result<TxnId, MessengerError> {
1179 let seed = self.counters.next_txn_id;
1180 self.counters.next_txn_id = seed.saturating_add(1);
1181 self.store.save_counters(self.counters)?;
1182 Ok(TxnId::new(seed))
1183 }
1184
1185 fn enqueue_request(&mut self, request: OutgoingRequest, lane: Lane) -> Result<(), MessengerError> {
1200 let id = request.id.clone();
1201 let kind = request.kind;
1202 if let Some(batch) = self.outgoing.enqueue(&mut self.store, request, lane)? {
1203 self.pending_flush_batches.push_back(batch);
1204 }
1205 self.pending_kinds.insert(id, kind);
1206 Ok(())
1207 }
1208
1209 fn emit(&mut self, event: MessengerEvent) {
1210 self.change_counter = self.change_counter.wrapping_add(1);
1211 self.events.push(event);
1212 }
1213
1214 fn drain_events(&mut self) -> Vec<MessengerEvent> {
1215 std::mem::take(&mut self.events)
1216 }
1217
1218 fn start_new_send(
1234 &mut self,
1235 room_id: RoomId,
1236 payload: SendPayload,
1237 caller_txn_id: Option<TxnId>,
1238 now_ms: i64,
1239 ) -> Result<(), MessengerError> {
1240 let txn_id = match caller_txn_id {
1241 Some(txn_id) => txn_id,
1242 None => self.next_txn_id()?,
1243 };
1244
1245 if let Some(wire) = payload.room_event() {
1246 if self.typing_debounce.remove(&room_id).is_some() {
1247 if let Ok(request_id) = self.next_request_id() {
1248 let request = OutgoingRequest::typing(request_id, &room_id, &self.config.user_id, false, None);
1249 let _ = self.enqueue_request(request, Lane::Other);
1250 }
1251 }
1252 let content = match &payload {
1253 SendPayload::Message(message) => message_local_echo_content(message),
1254 _ => interpret_content(&wire.event_type, None, &wire.content),
1255 };
1256 let timeline = self.timelines.entry(room_id.clone()).or_default();
1257 timeline.push_local_echo(
1258 txn_id.clone(),
1259 self.config.user_id.clone(),
1260 now_ms,
1261 &wire.event_type,
1262 content,
1263 wire.content,
1264 );
1265 timeline.mark_send_state(&txn_id, SendState::Sending);
1266 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1267 }
1268
1269 self.send_state.insert(txn_id.clone(), PendingSend { room_id: room_id.clone(), payload, phase: SendPhase::Queued, wire: None });
1270 let queue = self.pending_sends.entry(room_id).or_default();
1271 let was_empty = queue.is_empty();
1272 queue.push_back(txn_id.clone());
1273 if was_empty {
1274 self.start_send_pipeline(&txn_id, now_ms)?;
1275 }
1276 Ok(())
1277 }
1278
1279 fn retry_send(&mut self, room_id: &RoomId, txn_id: &TxnId, now_ms: i64) -> Result<(), MessengerError> {
1284 let Some(pending) = self.send_state.get(txn_id) else { return Ok(()) };
1285 if pending.room_id != *room_id || !matches!(pending.phase, SendPhase::Failed { .. }) {
1286 return Ok(());
1287 }
1288 self.set_send_phase(txn_id, SendPhase::Queued);
1289 let queue = self.pending_sends.entry(room_id.clone()).or_default();
1290 let was_empty = queue.is_empty();
1291 queue.push_back(txn_id.clone());
1292 if was_empty {
1293 self.start_send_pipeline(txn_id, now_ms)?;
1294 }
1295 Ok(())
1296 }
1297
1298 fn set_send_phase(&mut self, txn_id: &TxnId, phase: SendPhase) {
1299 if let Some(entry) = self.send_state.get_mut(txn_id) {
1300 entry.phase = phase;
1301 }
1302 }
1303
1304 fn start_send_pipeline(&mut self, txn_id: &TxnId, now_ms: i64) -> Result<(), MessengerError> {
1313 let Some(pending) = self.send_state.get(txn_id) else { return Ok(()) };
1314 let room_id = pending.room_id.clone();
1315
1316 if pending.wire.is_some() {
1317 return self.begin_awaiting_send(txn_id, &room_id);
1318 }
1319
1320 let is_room_event = pending.payload.is_room_event();
1321 let encrypted_room = self.rooms.get(&room_id).is_some_and(|room| room.encryption.is_some());
1322
1323 if is_room_event && encrypted_room {
1324 self.begin_key_pipeline(txn_id, &room_id, now_ms)
1325 } else {
1326 self.build_plaintext_wire_body(txn_id);
1327 self.begin_awaiting_send(txn_id, &room_id)
1328 }
1329 }
1330
1331 fn encrypted_room_member_ids(&self, room_id: &RoomId) -> BTreeSet<UserId> {
1336 let mut ids: BTreeSet<UserId> = self
1337 .rooms
1338 .get(room_id)
1339 .map(|state| {
1340 state
1341 .members
1342 .iter()
1343 .filter(|(_, member)| matches!(member.membership, Membership::Join | Membership::Invite))
1344 .map(|(user_id, _)| user_id.clone())
1345 .collect()
1346 })
1347 .unwrap_or_default();
1348 ids.insert(self.config.user_id.clone());
1349 ids
1350 }
1351
1352 fn mark_outdated_or_untracked(&mut self, member_ids: &BTreeSet<UserId>) -> Result<Vec<UserId>, MessengerError> {
1358 let tracked: BTreeMap<UserId, bool> = self.store.tracked_users()?.into_iter().collect();
1359 let mut outdated = Vec::new();
1360 let mut newly_tracked = Vec::new();
1361 for user_id in member_ids {
1362 match tracked.get(user_id) {
1363 Some(true) => outdated.push(user_id.clone()),
1364 Some(false) => {}
1365 None => newly_tracked.push(user_id.clone()),
1366 }
1367 }
1368 if !newly_tracked.is_empty() {
1369 DeviceTracker::on_device_lists(&mut self.store, &newly_tracked, &[])?;
1370 outdated.extend(newly_tracked);
1371 }
1372 Ok(outdated)
1373 }
1374
1375 fn known_devices_for(&self, member_ids: &BTreeSet<UserId>) -> Result<Vec<StoredDevice>, MessengerError> {
1378 let mut all = Vec::new();
1379 for user_id in member_ids {
1380 all.extend(DeviceTracker::devices_for_user(&self.store, user_id)?);
1381 }
1382 Ok(all)
1383 }
1384
1385 fn candidate_recipient_devices(&self, member_ids: &BTreeSet<UserId>, known_devices: &[StoredDevice]) -> Vec<StoredDevice> {
1389 known_devices
1390 .iter()
1391 .filter(|device| member_ids.contains(&device.user_id) && !device.blocked)
1392 .filter(|device| !(device.user_id == self.config.user_id && device.device_id == self.config.device_id))
1393 .cloned()
1394 .collect()
1395 }
1396
1397 fn begin_key_pipeline(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1402 let members = self.encrypted_room_member_ids(room_id);
1403 let outdated = self.mark_outdated_or_untracked(&members)?;
1404 if !outdated.is_empty() {
1405 let request_id = self.next_request_id()?;
1406 if let Some(request) = DeviceTracker::keys_query_request(&self.store, request_id.clone())? {
1407 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1408 self.enqueue_request(request, Lane::Other)?;
1409 self.set_send_phase(txn_id, SendPhase::AwaitingKeysQuery);
1410 return Ok(());
1411 }
1412 }
1413 self.begin_keys_claim_or_share(txn_id, room_id, now_ms)
1414 }
1415
1416 fn begin_keys_claim_or_share(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1420 let members = self.encrypted_room_member_ids(room_id);
1421 let known_devices = self.known_devices_for(&members)?;
1422 let candidates = self.candidate_recipient_devices(&members, &known_devices);
1423 let missing: Vec<StoredDevice> =
1424 OlmSessionManager::sessions_missing_for(&self.store, &candidates)?.into_iter().cloned().collect();
1425 if !missing.is_empty() {
1426 let request_id = self.next_request_id()?;
1427 let refs: Vec<&StoredDevice> = missing.iter().collect();
1428 if let Some(request) = OlmSessionManager::keys_claim_request(request_id.clone(), &refs) {
1429 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1430 self.enqueue_request(request, Lane::Other)?;
1431 self.set_send_phase(txn_id, SendPhase::AwaitingKeysClaim { devices: missing });
1432 return Ok(());
1433 }
1434 }
1435 self.begin_share_or_encrypt(txn_id, room_id, now_ms)
1436 }
1437
1438 fn begin_share_or_encrypt(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1451 let Some(encryption) = self.rooms.get(room_id).and_then(|room| room.encryption.clone()) else {
1452 self.build_plaintext_wire_body(txn_id);
1453 return self.begin_awaiting_send(txn_id, room_id);
1454 };
1455 let members = self.encrypted_room_member_ids(room_id);
1456 let known_devices = self.known_devices_for(&members)?;
1457 let update = GroupSessionManager::ensure_outbound_session(
1458 &mut self.store,
1459 room_id,
1460 &self.config.user_id,
1461 &self.config.device_id,
1462 &encryption,
1463 &members,
1464 &known_devices,
1465 now_ms,
1466 )?;
1467 let own_keys = self.account.identity_keys();
1468 GroupSessionManager::adopt_own_outbound_session(
1469 &mut self.store,
1470 room_id,
1471 &self.config.user_id,
1472 own_keys.curve25519,
1473 own_keys.ed25519,
1474 &update,
1475 )?;
1476
1477 if let Some(room_key_content) = update.room_key_content.clone() {
1478 let missing_ids: BTreeSet<(UserId, DeviceId)> = OlmSessionManager::sessions_missing_for(&self.store, &update.new_recipients)?
1479 .into_iter()
1480 .map(|device| (device.user_id.clone(), device.device_id.clone()))
1481 .collect();
1482 let sessioned: Vec<StoredDevice> = update
1483 .new_recipients
1484 .into_iter()
1485 .filter(|device| !missing_ids.contains(&(device.user_id.clone(), device.device_id.clone())))
1486 .collect();
1487
1488 let mut pending_ids = BTreeSet::new();
1489 for chunk in GroupSessionManager::chunk_recipients_for_send_to_device(&sessioned) {
1490 let body = GroupSessionManager::build_room_key_send_to_device_body(
1491 &mut self.store,
1492 &self.account,
1493 &self.config.user_id,
1494 &self.config.device_id,
1495 chunk,
1496 &room_key_content,
1497 )?;
1498 let request_id = self.next_request_id()?;
1499 let share_txn = self.next_txn_id()?;
1500 let request = OutgoingRequest::send_to_device(request_id.clone(), ROOM_KEY_SHARE_EVENT_TYPE, &share_txn, body);
1501 self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1502 self.enqueue_request(request, Lane::ToDevice)?;
1503 pending_ids.insert(request_id);
1504 }
1505 if !pending_ids.is_empty() {
1506 self.set_send_phase(txn_id, SendPhase::AwaitingKeyShare { pending: pending_ids });
1507 return Ok(());
1508 }
1509 }
1510
1511 self.finish_encrypt_and_send(txn_id, room_id)?;
1512 self.begin_awaiting_send(txn_id, room_id)
1513 }
1514
1515 fn finish_encrypt_and_send(&mut self, txn_id: &TxnId, room_id: &RoomId) -> Result<(), MessengerError> {
1522 let Some(wire) = self.send_state.get(txn_id).and_then(|pending| pending.payload.room_event()) else {
1523 return Ok(());
1524 };
1525 let sender_curve = self.account.identity_keys().curve25519.to_base64();
1526 let encrypted = GroupSessionManager::encrypt_event(
1527 &mut self.store,
1528 room_id,
1529 &sender_curve,
1530 &self.config.device_id,
1531 &wire.event_type,
1532 wire.content,
1533 wire.relates_to,
1534 )?;
1535 let wire_content = serde_json::to_value(RoomEncryptedContent::Megolm(encrypted))?;
1536 if let Some(entry) = self.send_state.get_mut(txn_id) {
1537 entry.wire = Some(WireSend::Event { event_type: "m.room.encrypted".to_string(), content: wire_content });
1538 }
1539 Ok(())
1540 }
1541
1542 fn build_plaintext_wire_body(&mut self, txn_id: &TxnId) {
1548 let Some(pending) = self.send_state.get(txn_id) else { return };
1549 let wire = match &pending.payload {
1550 SendPayload::Message(_) | SendPayload::Event { .. } => match pending.payload.room_event() {
1551 Some(room_event) => WireSend::Event { event_type: room_event.event_type, content: room_event.content },
1552 None => return,
1553 },
1554 SendPayload::Reaction { target, key } => WireSend::Event {
1555 event_type: "m.reaction".to_string(),
1556 content: serde_json::json!({
1557 "m.relates_to": { "rel_type": "m.annotation", "event_id": target.as_str(), "key": key }
1558 }),
1559 },
1560 SendPayload::Redaction { target, reason } => WireSend::Redact { target: target.clone(), reason: reason.clone() },
1561 };
1562 if let Some(entry) = self.send_state.get_mut(txn_id) {
1563 entry.wire = Some(wire);
1564 }
1565 }
1566
1567 fn begin_awaiting_send(&mut self, txn_id: &TxnId, room_id: &RoomId) -> Result<(), MessengerError> {
1572 let Some(wire) = self.send_state.get(txn_id).and_then(|pending| pending.wire.clone()) else { return Ok(()) };
1573 let request_id = self.next_request_id()?;
1574 let request = match &wire {
1575 WireSend::Event { event_type, content } => {
1576 OutgoingRequest::room_send(request_id.clone(), room_id, event_type, txn_id, content.clone())
1577 }
1578 WireSend::Redact { target, reason } => {
1579 let body = match reason {
1580 Some(reason) => serde_json::json!({ "reason": reason }),
1581 None => serde_json::json!({}),
1582 };
1583 OutgoingRequest::room_redact(request_id.clone(), room_id, target, txn_id, body)
1584 }
1585 };
1586 self.send_request_owner.insert(request_id, txn_id.clone());
1587 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
1588 self.set_send_phase(txn_id, SendPhase::AwaitingSend);
1589 Ok(())
1590 }
1591
1592 fn advance_send_on_success(
1598 &mut self,
1599 txn_id: &TxnId,
1600 request_id: &RequestId,
1601 kind: Option<OutgoingRequestKind>,
1602 response: &HttpResponseDescriptor,
1603 now_ms: i64,
1604 ) {
1605 match kind {
1606 Some(OutgoingRequestKind::KeysQuery) => {
1607 let Some(room_id) = self.send_state.get(txn_id).map(|pending| pending.room_id.clone()) else { return };
1608 let _ = self.begin_keys_claim_or_share(txn_id, &room_id, now_ms);
1609 }
1610 Some(OutgoingRequestKind::KeysClaim) => {
1611 let Some(pending) = self.send_state.get(txn_id) else { return };
1612 let room_id = pending.room_id.clone();
1613 if let SendPhase::AwaitingKeysClaim { devices, .. } = &pending.phase {
1614 let devices = devices.clone();
1615 let refs: Vec<&StoredDevice> = devices.iter().collect();
1616 let _ = OlmSessionManager::on_keys_claim_response(&mut self.store, &self.account, &refs, &response.body);
1617 }
1618 let _ = self.begin_share_or_encrypt(txn_id, &room_id, now_ms);
1619 }
1620 Some(OutgoingRequestKind::SendToDevice) => {
1621 let mut remaining = None;
1622 if let Some(entry) = self.send_state.get_mut(txn_id) {
1623 if let SendPhase::AwaitingKeyShare { pending } = &mut entry.phase {
1624 pending.remove(request_id);
1625 remaining = Some(pending.len());
1626 }
1627 }
1628 if remaining == Some(0) {
1629 let Some(room_id) = self.send_state.get(txn_id).map(|pending| pending.room_id.clone()) else { return };
1630 let _ = self.finish_encrypt_and_send(txn_id, &room_id);
1631 let _ = self.begin_awaiting_send(txn_id, &room_id);
1632 }
1633 }
1634 Some(OutgoingRequestKind::RoomSend) | Some(OutgoingRequestKind::RoomRedact) => {
1635 self.complete_send_success(txn_id, response, now_ms);
1636 }
1637 _ => {}
1638 }
1639 }
1640
1641 fn complete_send_success(&mut self, txn_id: &TxnId, response: &HttpResponseDescriptor, now_ms: i64) {
1647 let Some(pending) = self.send_state.get(txn_id) else { return };
1648 let room_id = pending.room_id.clone();
1649 if pending.payload.is_room_event() {
1650 if let Ok(body) = serde_json::from_slice::<SendResponseBody>(&response.body) {
1651 if let Some(timeline) = self.timelines.get_mut(&room_id) {
1652 timeline.set_echo_event_id(txn_id, body.event_id);
1653 }
1654 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1655 }
1656 }
1657 self.send_state.remove(txn_id);
1658 self.finish_active_send(&room_id, txn_id, now_ms);
1659 }
1660
1661 fn fail_send(&mut self, txn_id: &TxnId, errcode: String, now_ms: i64) {
1667 let Some(pending) = self.send_state.get(txn_id) else { return };
1668 let room_id = pending.room_id.clone();
1669 if pending.payload.is_room_event() {
1670 if let Some(timeline) = self.timelines.get_mut(&room_id) {
1671 timeline.mark_send_state(txn_id, SendState::Failed { reason: errcode.clone() });
1672 }
1673 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1674 }
1675 self.set_send_phase(txn_id, SendPhase::Failed { errcode });
1676 self.finish_active_send(&room_id, txn_id, now_ms);
1677 }
1678
1679 fn finish_active_send(&mut self, room_id: &RoomId, txn_id: &TxnId, now_ms: i64) {
1685 if let Some(queue) = self.pending_sends.get_mut(room_id) {
1686 if queue.front() == Some(txn_id) {
1687 queue.pop_front();
1688 } else {
1689 queue.retain(|id| id != txn_id);
1690 }
1691 }
1692 let next = self.pending_sends.get(room_id).and_then(|queue| queue.front().cloned());
1693 if let Some(next_txn) = next {
1694 let _ = self.start_send_pipeline(&next_txn, now_ms);
1695 }
1696 }
1697
1698 pub fn releasable_requests(&mut self, now_ms: i64) -> Vec<OutgoingRequest> {
1718 self.ensure_sync_enqueued();
1719 self.outgoing.releasable(&self.store, now_ms)
1720 }
1721
1722 fn ensure_sync_enqueued(&mut self) {
1723 if self.sync_request_id.is_some() {
1724 return;
1725 }
1726 let Ok(request_id) = self.next_request_id() else { return };
1727 let since = self.store.sync_token().ok().flatten().map(str::to_string);
1728 let timeout_ms = if since.is_some() && self.sync_caught_up { SYNC_TIMEOUT_MS } else { 0 };
1738 let request = OutgoingRequest::sync(request_id.clone(), since.as_deref(), Some(timeout_ms));
1739 if self.enqueue_request(request, Lane::Sync).is_ok() {
1740 self.sync_request_id = Some(request_id);
1741 }
1742 }
1743
1744 pub fn on_response(&mut self, request_id: RequestId, resp: HttpResponseDescriptor, now_ms: i64) -> Vec<MessengerEvent> {
1750 let kind = self.pending_kinds.get(&request_id).copied();
1751 let room_messages_room = self.pending_room_messages.get(&request_id).cloned();
1752 let create_room_kind = self.pending_create_room.get(&request_id).cloned();
1753 let send_owner = self.send_request_owner.get(&request_id).cloned();
1754
1755 let outcome = self.outgoing.on_response(&mut self.store, &request_id, resp, now_ms, self.jitter.as_mut());
1756 let (is_terminal, terminal_response, failed_errcode) = match outcome {
1757 Ok(Some(ResponseOutcome::Done(response))) => (true, Some(response), None),
1758 Ok(Some(ResponseOutcome::Failed { errcode, .. })) => (true, None, Some(errcode)),
1759 _ => (false, None, None),
1760 };
1761
1762 if is_terminal {
1763 self.pending_kinds.remove(&request_id);
1764 if matches!(kind, Some(OutgoingRequestKind::Sync)) {
1765 self.sync_request_id = None;
1766 if terminal_response.is_some() {
1767 self.sync_caught_up = true;
1768 }
1769 }
1770 if room_messages_room.is_some() {
1771 self.pending_room_messages.remove(&request_id);
1772 }
1773 self.pending_create_room.remove(&request_id);
1774 self.send_request_owner.remove(&request_id);
1775 if let Some(key) = self.account_data_write_keys.remove(&request_id) {
1776 self.settle_account_data_write(&key, terminal_response.is_some());
1777 }
1778 }
1779
1780 if let Some(response) = &terminal_response {
1781 if let Err(error) = self.handle_terminal_success(&request_id, kind, room_messages_room, create_room_kind, response) {
1788 self.ingest_error = Some(match kind {
1789 Some(kind) => format!("{kind:?} response not ingested: {error}"),
1790 None => format!("response not ingested: {error}"),
1791 });
1792 }
1793 }
1794
1795 if is_terminal
1796 && terminal_response.is_none()
1797 && matches!(kind, Some(OutgoingRequestKind::SigningKeysUpload) | Some(OutgoingRequestKind::SignaturesUpload))
1798 {
1799 self.xsign_inflight = false;
1800 }
1801
1802 if let Some(txn_id) = send_owner {
1803 if let Some(response) = &terminal_response {
1804 self.advance_send_on_success(&txn_id, &request_id, kind, response, now_ms);
1805 } else if let Some(errcode) = failed_errcode.clone() {
1806 self.fail_send(&txn_id, errcode, now_ms);
1807 }
1808 }
1809
1810 if matches!(kind, Some(OutgoingRequestKind::KeysUpload)) {
1814 if let Some(errcode) = failed_errcode {
1815 self.ingest_error = Some(format!("keys/upload failed: {errcode}"));
1816 }
1817 }
1818
1819 self.drain_events()
1820 }
1821
1822 pub fn on_transport_error(&mut self, request_id: &RequestId, now_ms: i64) {
1827 let _ = self.outgoing.on_transport_error(request_id, now_ms, self.jitter.as_mut());
1828 }
1829
1830 fn handle_terminal_success(
1831 &mut self,
1832 request_id: &RequestId,
1833 kind: Option<OutgoingRequestKind>,
1834 room_messages_room: Option<RoomId>,
1835 create_room_kind: Option<CreateRoomKind>,
1836 response: &HttpResponseDescriptor,
1837 ) -> Result<(), MessengerError> {
1838 match kind {
1839 Some(OutgoingRequestKind::Sync) => {
1840 let parsed = parse_sync_response(&response.body)?;
1841 self.ingest_sync(&parsed)?;
1842 }
1843 Some(OutgoingRequestKind::KeysUpload) => {
1844 self.account.on_keys_upload_response(&mut self.store)?;
1845 }
1846 Some(OutgoingRequestKind::KeysClaim) if self.history_claims.contains_key(request_id) => {
1847 if let Some(devices) = self.history_claims.remove(request_id) {
1848 let refs: Vec<&StoredDevice> = devices.iter().collect();
1849 let _ = OlmSessionManager::on_keys_claim_response(&mut self.store, &self.account, &refs, &response.body);
1850 }
1851 self.retry_missing_room_keys();
1852 }
1853 Some(OutgoingRequestKind::SigningKeysUpload) => {
1854 self.xsign_inflight = false;
1855 if let Some(state) = self.xsign.as_mut() {
1856 state.uploaded = true;
1857 let snapshot = state.clone();
1858 snapshot.save(&mut self.store)?;
1859 }
1860 self.maybe_enqueue_cross_signing()?;
1861 }
1862 Some(OutgoingRequestKind::SignaturesUpload) => {
1863 self.xsign_inflight = false;
1864 let failures_empty = serde_json::from_slice::<serde_json::Value>(&response.body)
1865 .ok()
1866 .and_then(|v| v.get("failures").and_then(|f| f.as_object()).map(|f| f.is_empty()))
1867 .unwrap_or(true);
1868 if failures_empty {
1869 if let Some(state) = self.xsign.as_mut() {
1870 state.device_signed = true;
1871 let snapshot = state.clone();
1872 snapshot.save(&mut self.store)?;
1873 }
1874 }
1875 }
1876 Some(OutgoingRequestKind::KeysQuery) => {
1877 let outcome = DeviceTracker::on_keys_query_response(&mut self.store, &response.body)?;
1878 if !outcome.key_changes.is_empty() {
1879 let room_ids: Vec<RoomId> = self.rooms.keys().cloned().collect();
1880 for change in &outcome.key_changes {
1881 let _ = GroupSessionManager::forget_shared_device(
1882 &mut self.store,
1883 &room_ids,
1884 &change.user_id,
1885 &change.device_id,
1886 );
1887 self.emit(MessengerEvent::DeviceKeyChanged {
1888 user_id: change.user_id.clone(),
1889 device_id: change.device_id.clone(),
1890 });
1891 }
1892 }
1893 self.retry_unknown_sender_queue()?;
1894 }
1895 Some(OutgoingRequestKind::RoomMessages) => {
1896 if let Some(room_id) = room_messages_room {
1897 self.ingest_room_messages_response(&room_id, &response.body)?;
1898 }
1899 }
1900 Some(OutgoingRequestKind::CreateRoom) => {
1901 if let Some(kind) = create_room_kind {
1902 self.on_create_room_response(kind, response)?;
1903 }
1904 }
1905 Some(OutgoingRequestKind::PublicRooms) if self.latest_public_rooms_request.as_ref() == Some(request_id) => {
1910 let body: PublicRoomsResponseBody = serde_json::from_slice(&response.body)?;
1911 self.public_rooms_result = body
1912 .chunk
1913 .into_iter()
1914 .map(|entry| PublicRoomsResultEntry {
1915 room_id: entry.room_id,
1916 name: entry.name,
1917 topic: entry.topic,
1918 num_joined_members: entry.num_joined_members,
1919 })
1920 .collect();
1921 self.emit(MessengerEvent::PublicRoomsChanged);
1922 }
1923 Some(OutgoingRequestKind::UserDirectorySearch) if self.latest_user_search_request.as_ref() == Some(request_id) => {
1925 let body: UserDirectorySearchResponseBody = serde_json::from_slice(&response.body)?;
1926 self.user_search_result = body
1927 .results
1928 .into_iter()
1929 .map(|entry| UserDirectoryResultEntry { user_id: entry.user_id, display_name: entry.display_name })
1930 .collect();
1931 self.emit(MessengerEvent::UserSearchChanged);
1932 }
1933 _ => {}
1934 }
1935 Ok(())
1936 }
1937
1938 fn on_create_room_response(&mut self, kind: CreateRoomKind, response: &HttpResponseDescriptor) -> Result<(), MessengerError> {
1944 let CreateRoomKind::Dm { peer } = kind else { return Ok(()) };
1945 let body: CreateRoomResponseBody = serde_json::from_slice(&response.body)?;
1946 self.merge_direct_account_data(body.room_id, peer)
1947 }
1948
1949 fn merge_direct_account_data(&mut self, room_id: RoomId, peer: UserId) -> Result<(), MessengerError> {
1961 let mut direct = self.direct_account_data.clone().unwrap_or_default();
1962 let rooms_for_peer = direct.0.entry(peer).or_default();
1963 if !rooms_for_peer.contains(&room_id) {
1964 rooms_for_peer.push(room_id);
1965 }
1966 let value = serde_json::to_value(&direct)?;
1967 self.write_account_data(None, "m.direct".to_string(), value)
1968 }
1969
1970 fn cached_account_data(&self, key: &AccountDataKey) -> Option<&serde_json::Value> {
1971 match &key.0 {
1972 Some(room_id) => self.room_account_data(room_id, &key.1),
1973 None => self.global_account_data(&key.1),
1974 }
1975 }
1976
1977 fn store_account_data_cache(&mut self, key: &AccountDataKey, value: Option<serde_json::Value>) {
1980 if key.0.is_none() && key.1 == "m.direct" {
1981 self.direct_account_data = value.as_ref().and_then(|v| serde_json::from_value::<DirectContent>(v.clone()).ok());
1982 }
1983 match (&key.0, value) {
1984 (None, Some(v)) => {
1985 self.global_account_data.insert(key.1.clone(), v);
1986 }
1987 (None, None) => {
1988 self.global_account_data.remove(&key.1);
1989 }
1990 (Some(room_id), Some(v)) => {
1991 self.room_account_data.entry(room_id.clone()).or_default().insert(key.1.clone(), v);
1992 }
1993 (Some(room_id), None) => {
1994 if let Some(by_type) = self.room_account_data.get_mut(room_id) {
1995 by_type.remove(&key.1);
1996 }
1997 }
1998 }
1999 }
2000
2001 fn write_account_data(&mut self, room_id: Option<RoomId>, event_type: String, value: serde_json::Value) -> Result<(), MessengerError> {
2007 let request_id = self.next_request_id()?;
2008 let request = match &room_id {
2009 Some(room_id) => OutgoingRequest::room_account_data(request_id.clone(), &self.config.user_id, room_id, &event_type, value.clone()),
2010 None => OutgoingRequest::account_data(request_id.clone(), &self.config.user_id, &event_type, value.clone()),
2011 };
2012 let key: AccountDataKey = (room_id, event_type);
2013 let known = self.cached_account_data(&key).cloned();
2014 let guard = self.account_data_guard.entry(key.clone()).or_insert(AccountDataGuard { pending: 0, server_value: known });
2015 guard.pending += 1;
2016 self.store_account_data_cache(&key, Some(value));
2017 match self.enqueue_request(request, Lane::AccountData) {
2018 Ok(()) => {
2019 self.account_data_write_keys.insert(request_id, key);
2020 Ok(())
2021 }
2022 Err(error) => {
2023 self.settle_account_data_write(&key, false);
2024 Err(error)
2025 }
2026 }
2027 }
2028
2029 fn settle_account_data_write(&mut self, key: &AccountDataKey, succeeded: bool) {
2034 let Some(guard) = self.account_data_guard.get_mut(key) else { return };
2035 guard.pending = guard.pending.saturating_sub(1);
2036 if guard.pending > 0 {
2037 return;
2038 }
2039 let Some(guard) = self.account_data_guard.remove(key) else { return };
2040 if !succeeded {
2041 self.store_account_data_cache(key, guard.server_value);
2042 self.emit(MessengerEvent::RoomsChanged);
2043 }
2044 }
2045
2046 fn apply_synced_account_data(&mut self, room_id: Option<&RoomId>, event_type: &str, content: serde_json::Value) {
2050 let key: AccountDataKey = (room_id.cloned(), event_type.to_string());
2051 if let Some(guard) = self.account_data_guard.get_mut(&key) {
2052 guard.server_value = Some(content);
2053 return;
2054 }
2055 self.store_account_data_cache(&key, Some(content));
2056 }
2057
2058 pub fn take_flush_batch(&mut self) -> Option<FlushBatch> {
2061 if let Some(batch) = self.pending_flush_batches.pop_front() {
2062 return Some(batch);
2063 }
2064 self.store.take_flush_batch()
2065 }
2066
2067 pub fn ack_flush(&mut self, id: u64) {
2069 self.store.ack_flush(id);
2070 }
2071
2072 pub fn events(&mut self) -> Vec<MessengerEvent> {
2075 self.drain_events()
2076 }
2077
2078 pub fn take_ingest_error(&mut self) -> Option<String> {
2081 self.ingest_error.take()
2082 }
2083
2084 pub fn change_counter(&self) -> u64 {
2089 self.change_counter
2090 }
2091
2092 pub fn room_ids(&self) -> impl Iterator<Item = &RoomId> {
2094 self.rooms.keys()
2095 }
2096
2097 pub fn room_state(&self, room_id: &RoomId) -> Option<&RoomState> {
2099 self.rooms.get(room_id)
2100 }
2101
2102 pub fn timeline(&self, room_id: &RoomId) -> Option<&Timeline> {
2104 self.timelines.get(room_id)
2105 }
2106
2107 pub fn room_kind(&self, room_id: &RoomId) -> Option<RoomKind> {
2111 self.rooms.get(room_id).map(|state| state.derive_room_kind(room_id, self.direct_account_data.as_ref()))
2112 }
2113
2114 pub fn typing_users(&self, room_id: &RoomId) -> &[UserId] {
2118 self.typing.get(room_id).map(Vec::as_slice).unwrap_or(&[])
2119 }
2120
2121 pub fn receipts(&self, room_id: &RoomId) -> Option<&ReceiptContent> {
2123 self.receipts.get(room_id)
2124 }
2125
2126 pub fn room_account_data(&self, room_id: &RoomId, event_type: &str) -> Option<&serde_json::Value> {
2129 self.room_account_data.get(room_id).and_then(|by_type| by_type.get(event_type))
2130 }
2131
2132 pub fn global_account_data(&self, event_type: &str) -> Option<&serde_json::Value> {
2135 self.global_account_data.get(event_type)
2136 }
2137
2138 pub fn key_request_count(&self) -> usize {
2140 self.key_requests_sent.len()
2141 }
2142
2143 pub fn user_id(&self) -> &UserId {
2148 &self.config.user_id
2149 }
2150
2151 pub fn has_tracked_device(&self, user_id: &UserId) -> bool {
2157 DeviceTracker::devices_for_user(&self.store, user_id).map(|devices| !devices.is_empty()).unwrap_or(false)
2158 }
2159
2160 pub fn public_rooms_result(&self) -> &[PublicRoomsResultEntry] {
2163 &self.public_rooms_result
2164 }
2165
2166 pub fn user_search_result(&self) -> &[UserDirectoryResultEntry] {
2169 &self.user_search_result
2170 }
2171
2172 pub fn send_failure_reason(&self, txn_id: &TxnId) -> Option<&str> {
2177 match self.send_state.get(txn_id).map(|pending| &pending.phase) {
2178 Some(SendPhase::Failed { errcode }) => Some(errcode.as_str()),
2179 _ => None,
2180 }
2181 }
2182
2183 pub fn dispatch(&mut self, cmd: MessengerCommand, now_ms: i64) -> Result<(), MessengerError> {
2190 match cmd {
2191 MessengerCommand::SendMessage { room_id, message, txn_id } => {
2192 if let Some(edit_of) = &message.edit_of {
2193 let owned = self
2194 .timelines
2195 .get(&room_id)
2196 .and_then(|timeline| timeline.item_by_event_id(edit_of))
2197 .is_some_and(|item| item.sender == self.config.user_id);
2198 if !owned {
2199 return Err(MessengerError::EditNotOwned { event_id: edit_of.clone() });
2200 }
2201 }
2202 self.start_new_send(room_id, SendPayload::Message(message), txn_id, now_ms)?;
2203 }
2204 MessengerCommand::Forward { from_room, event_id, to_room, txn_id } => {
2205 let payload = self.build_forward_payload(&from_room, &event_id, &to_room)?;
2206 self.start_new_send(to_room, payload, txn_id, now_ms)?;
2207 }
2208 MessengerCommand::React { room_id, target, key } => {
2209 self.start_new_send(room_id, SendPayload::Reaction { target, key }, None, now_ms)?;
2210 }
2211 MessengerCommand::Redact { room_id, target, reason } => {
2212 self.start_new_send(room_id, SendPayload::Redaction { target, reason }, None, now_ms)?;
2213 }
2214 MessengerCommand::RetrySend { room_id, txn_id } => {
2215 self.retry_send(&room_id, &txn_id, now_ms)?;
2216 }
2217 MessengerCommand::CreateRoom { kind } => {
2218 self.dispatch_create_room(kind)?;
2219 }
2220 MessengerCommand::JoinRoom { room_id } => {
2221 let request_id = self.next_request_id()?;
2222 let request = OutgoingRequest::join_room(request_id, room_id.as_str());
2223 self.enqueue_request(request, Lane::Other)?;
2224
2225 let dm_peer = self.rooms.get(&room_id).and_then(|room| {
2232 let invited_as_direct = room.members.get(&self.config.user_id).is_some_and(|member| member.is_direct);
2233 if !invited_as_direct {
2234 return None;
2235 }
2236 room.members.keys().find(|user_id| **user_id != self.config.user_id).cloned()
2237 });
2238 if let Some(peer) = dm_peer {
2239 self.merge_direct_account_data(room_id, peer)?;
2240 }
2241 }
2242 MessengerCommand::LeaveRoom { room_id } => {
2243 let request_id = self.next_request_id()?;
2244 let request = OutgoingRequest::leave_room(request_id, &room_id);
2245 self.enqueue_request(request, Lane::Other)?;
2246 }
2247 MessengerCommand::Invite { room_id, user_id } => {
2248 let request_id = self.next_request_id()?;
2249 let request = OutgoingRequest::invite(request_id, &room_id, &user_id);
2250 self.enqueue_request(request, Lane::Other)?;
2251 }
2252 MessengerCommand::Kick { room_id, user_id, reason } => {
2253 let request_id = self.next_request_id()?;
2254 let request = OutgoingRequest::kick(request_id, &room_id, &user_id, reason.as_deref());
2255 self.enqueue_request(request, Lane::Other)?;
2256 }
2257 MessengerCommand::SetPowerLevel { room_id, user_id, level } => {
2258 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())))?;
2259 content.users.insert(user_id, level);
2260 let body = serde_json::to_value(&content)?;
2261 let request_id = self.next_request_id()?;
2262 let request = OutgoingRequest::set_state(request_id, &room_id, "m.room.power_levels", "", body);
2263 self.enqueue_request(request, Lane::Other)?;
2264 }
2265 MessengerCommand::SetEventsDefault { room_id, level } => {
2266 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())))?;
2267 content.events_default = level;
2268 let body = serde_json::to_value(&content)?;
2269 let request_id = self.next_request_id()?;
2270 let request = OutgoingRequest::set_state(request_id, &room_id, "m.room.power_levels", "", body);
2271 self.enqueue_request(request, Lane::Other)?;
2272 }
2273 MessengerCommand::SetTag { room_id, tag, order } => {
2274 let mut content = self.current_tag_content(&room_id);
2275 content.tags.insert(tag, TagInfo { order });
2276 self.enqueue_tag_update(room_id, content)?;
2277 }
2278 MessengerCommand::RemoveTag { room_id, tag } => {
2279 let mut content = self.current_tag_content(&room_id);
2280 content.tags.remove(&tag);
2281 self.enqueue_tag_update(room_id, content)?;
2282 }
2283 MessengerCommand::MarkRead { room_id, event_id } => {
2284 let request_id = self.next_request_id()?;
2285 let body = serde_json::json!({ "m.fully_read": event_id.as_str(), "m.read": event_id.as_str() });
2286 let request = OutgoingRequest::read_markers(request_id, &room_id, body);
2287 self.enqueue_request(request, Lane::Other)?;
2288 }
2289 MessengerCommand::SetTyping { room_id, typing } => {
2290 if typing {
2291 let should_send = match self.typing_debounce.get(&room_id) {
2292 Some(&last_sent_ms) => now_ms.saturating_sub(last_sent_ms) >= TYPING_DEBOUNCE_MS,
2293 None => true,
2294 };
2295 if !should_send {
2296 return Ok(());
2297 }
2298 self.typing_debounce.insert(room_id.clone(), now_ms);
2299 } else {
2300 self.typing_debounce.remove(&room_id);
2301 }
2302 let request_id = self.next_request_id()?;
2303 let timeout_ms = typing.then_some(TYPING_TIMEOUT_MS);
2304 let request = OutgoingRequest::typing(request_id, &room_id, &self.config.user_id, typing, timeout_ms);
2305 self.enqueue_request(request, Lane::Other)?;
2306 }
2307 MessengerCommand::RetryDecryption { room_id, event_id } => {
2308 if self.retry_decrypt_one(&room_id, &event_id) {
2309 self.emit(MessengerEvent::TimelineChanged { room_id });
2310 }
2311 }
2312 MessengerCommand::LoadOlder { room_id } => {
2313 let from = self
2314 .timelines
2315 .get(&room_id)
2316 .and_then(|timeline| {
2317 timeline
2318 .older_token()
2319 .map(str::to_string)
2320 .or_else(|| timeline.gap().and_then(|gap| gap.prev_batch.clone()))
2321 })
2322 .or_else(|| {
2328 if self.rooms.contains_key(&room_id) {
2329 self.store.sync_token().ok().flatten().map(str::to_string)
2330 } else {
2331 None
2332 }
2333 });
2334 let Some(from) = from else { return Ok(()) };
2335 let request_id = self.next_request_id()?;
2336 let request =
2337 OutgoingRequest::room_messages(request_id.clone(), &room_id, &from, "b", Some(LOAD_OLDER_PAGE_SIZE));
2338 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
2339 self.pending_room_messages.insert(request_id, room_id);
2340 }
2341 MessengerCommand::SetAccountData { event_type, content } => {
2342 self.write_account_data(None, event_type, content)?;
2343 }
2344 MessengerCommand::SetRoomAccountData { room_id, event_type, content } => {
2345 self.write_account_data(Some(room_id), event_type, content)?;
2346 }
2347 MessengerCommand::SearchPublicRooms { term } => {
2348 let request_id = self.next_request_id()?;
2349 let body = serde_json::json!({ "filter": { "generic_search_term": term }, "limit": 50 });
2350 let request = OutgoingRequest::public_rooms(request_id.clone(), body);
2351 self.latest_public_rooms_request = Some(request_id);
2352 self.enqueue_request(request, Lane::Other)?;
2353 }
2354 MessengerCommand::SearchUsers { term } => {
2355 let request_id = self.next_request_id()?;
2356 let body = serde_json::json!({ "search_term": term, "limit": 20 });
2357 let request = OutgoingRequest::user_directory_search(request_id.clone(), body);
2358 self.latest_user_search_request = Some(request_id);
2359 self.enqueue_request(request, Lane::Other)?;
2360 }
2361 }
2362 Ok(())
2363 }
2364
2365 fn dispatch_create_room(&mut self, kind: CreateRoomKind) -> Result<(), MessengerError> {
2370 let body = match &kind {
2371 CreateRoomKind::Dm { peer } => serde_json::json!({
2372 "is_direct": true,
2373 "invite": [peer.as_str()],
2374 }),
2375 CreateRoomKind::Group { name, invite, members_can_invite } => serde_json::json!({
2376 "visibility": "private",
2377 "is_direct": false,
2378 "name": name,
2379 "invite": invite.iter().map(UserId::as_str).collect::<Vec<_>>(),
2380 "members_can_invite": members_can_invite,
2381 }),
2382 CreateRoomKind::Channel { name, topic } => {
2383 let mut value = serde_json::json!({
2384 "visibility": "public",
2385 "is_direct": false,
2386 "name": name,
2387 });
2388 if let Some(topic) = topic {
2389 value["topic"] = serde_json::Value::String(topic.clone());
2390 }
2391 value
2392 }
2393 };
2394 let request_id = self.next_request_id()?;
2395 let request = OutgoingRequest::create_room(request_id.clone(), body);
2396 self.pending_create_room.insert(request_id.clone(), kind);
2397 self.enqueue_request(request, Lane::Other)?;
2398 Ok(())
2399 }
2400
2401 fn current_tag_content(&self, room_id: &RoomId) -> TagContent {
2406 self.room_account_data(room_id, "m.tag")
2407 .and_then(|value| serde_json::from_value(value.clone()).ok())
2408 .unwrap_or_default()
2409 }
2410
2411 fn enqueue_tag_update(&mut self, room_id: RoomId, content: TagContent) -> Result<(), MessengerError> {
2417 let value = serde_json::to_value(&content)?;
2418 self.write_account_data(Some(room_id), "m.tag".to_string(), value)
2419 }
2420
2421 fn build_forward_payload(
2426 &self,
2427 from_room: &RoomId,
2428 event_id: &EventId,
2429 to_room: &RoomId,
2430 ) -> Result<SendPayload, MessengerError> {
2431 let refuse = |reason: &dyn std::fmt::Display| {
2432 MessengerError::IntentRefused(format!("cannot forward {event_id} from {from_room}: {reason}"))
2433 };
2434 if !self.rooms.contains_key(to_room) {
2435 return Err(refuse(&format!("the target room {to_room} is unknown")));
2436 }
2437 let source = match self.timelines.get(from_room) {
2438 Some(timeline) => timeline.forward_source(event_id),
2439 None => Err(ForwardRefusal::Missing),
2440 }
2441 .map_err(|reason| refuse(&reason))?;
2442 let (marker_key, marker) = self.forward_marker(from_room);
2443 Ok(SendPayload::Event {
2444 event_type: source.event_type,
2445 content: forwardable_content(source.content, marker_key, marker),
2446 })
2447 }
2448
2449 fn forward_marker(&self, from_room: &RoomId) -> (&'static str, serde_json::Value) {
2454 let public_channel = self.room_kind(from_room) == Some(RoomKind::Channel);
2455 if public_channel {
2456 let room_name = self.forward_room_name(from_room);
2457 (KEY_FORWARDED_FROM, serde_json::json!({ "room_id": from_room.as_str(), "room_name": room_name }))
2458 } else {
2459 (KEY_FORWARDED, serde_json::Value::Bool(true))
2460 }
2461 }
2462
2463 fn forward_room_name(&self, room_id: &RoomId) -> String {
2467 if let Some(name) = self.rooms.get(room_id).and_then(|room| room.name.clone()) {
2468 return name;
2469 }
2470 match self.room_kind(room_id) {
2471 Some(RoomKind::Dm) => "Direct",
2472 Some(RoomKind::Channel) => "Channel",
2473 Some(RoomKind::Group) | None => "Group",
2474 }
2475 .to_string()
2476 }
2477
2478 fn retry_decrypt_one(&mut self, room_id: &RoomId, event_id: &EventId) -> bool {
2484 let Some(item) = self.pending_decrypt.get(room_id).and_then(|by_event| by_event.get(event_id)).cloned() else {
2485 return false;
2486 };
2487 match GroupSessionManager::decrypt_event(
2488 &mut self.store,
2489 room_id,
2490 event_id,
2491 item.origin_server_ts,
2492 &item.sender,
2493 &item.content,
2494 ) {
2495 Ok(plaintext) => {
2496 self.apply_decrypted_plaintext(room_id, event_id, &item.sender, item.origin_server_ts, &plaintext);
2497 }
2498 Err(GroupDecryptError::MissingSession { .. }) => {
2499 let _ = self.request_missing_room_key(room_id, &item.sender, &item.content);
2500 return false;
2501 }
2502 Err(_) => return false,
2503 }
2504 if let Some(by_event) = self.pending_decrypt.get_mut(room_id) {
2505 by_event.remove(event_id);
2506 }
2507 true
2508 }
2509
2510 pub fn pull_latest_page(&mut self, room_id: RoomId) -> Result<(), MessengerError> {
2513 let request_id = self.next_request_id()?;
2514 let request = OutgoingRequest::room_messages_latest(request_id.clone(), &room_id, 20);
2515 self.enqueue_request(request, Lane::Room(room_id.clone()))?;
2516 self.pending_room_messages.insert(request_id, room_id);
2517 Ok(())
2518 }
2519
2520 pub fn decrypt_loaded_timeline(&mut self) {
2524 let pending: Vec<(RoomId, Vec<EventId>)> = self
2525 .pending_decrypt
2526 .iter()
2527 .map(|(room_id, by_event)| (room_id.clone(), by_event.keys().cloned().collect()))
2528 .collect();
2529 for (room_id, event_ids) in pending {
2530 for event_id in event_ids {
2531 self.retry_decrypt_one(&room_id, &event_id);
2532 }
2533 }
2534 let rooms: Vec<RoomId> = self.timelines.keys().cloned().collect();
2535 for room_id in rooms {
2536 let sealed: Vec<(EventId, i64, UserId, MegolmEncryptedContent)> = {
2537 let Some(timeline) = self.timelines.get(&room_id) else {
2538 continue;
2539 };
2540 timeline
2541 .items()
2542 .iter()
2543 .filter_map(|item| {
2544 let event_id = item.event_id.clone()?;
2545 let (session_id, ciphertext) = match &item.content {
2546 ItemContent::Encrypted { session_id, ciphertext_b64 } => {
2547 (session_id.clone(), ciphertext_b64.clone())
2548 }
2549 _ => return None,
2550 };
2551 Some((
2552 event_id,
2553 item.origin_server_ts,
2554 item.sender.clone(),
2555 MegolmEncryptedContent {
2556 ciphertext,
2557 session_id,
2558 sender_key: None,
2559 device_id: None,
2560 relates_to: None,
2561 },
2562 ))
2563 })
2564 .collect()
2565 };
2566 for (event_id, origin_server_ts, sender, content) in sealed {
2567 let plaintext = match GroupSessionManager::decrypt_event(
2568 &mut self.store,
2569 &room_id,
2570 &event_id,
2571 origin_server_ts,
2572 &sender,
2573 &content,
2574 ) {
2575 Ok(plaintext) => plaintext,
2576 Err(GroupDecryptError::MissingSession { .. }) => {
2577 let _ = self.request_missing_room_key(&room_id, &sender, &content);
2578 continue;
2579 }
2580 Err(_) => continue,
2581 };
2582 self.apply_decrypted_plaintext(
2583 &room_id,
2584 &event_id,
2585 &sender,
2586 origin_server_ts,
2587 &plaintext,
2588 );
2589 }
2590 }
2591 }
2592
2593 fn apply_decrypted_plaintext(
2606 &mut self,
2607 room_id: &RoomId,
2608 event_id: &EventId,
2609 sender: &UserId,
2610 origin_server_ts: i64,
2611 plaintext: &RoomEventPlaintext,
2612 ) {
2613 let Some(timeline) = self.timelines.get_mut(room_id) else { return };
2614 if let Some(relation) = RelatesTo::from_content(&plaintext.content) {
2615 if matches!(relation, RelatesTo::Annotation { .. } | RelatesTo::Replace { .. }) {
2616 let new_content = plaintext.content.get("m.new_content").cloned();
2617 timeline.apply_decrypted_relation(event_id, sender.clone(), origin_server_ts, relation, new_content);
2618 return;
2619 }
2620 }
2621 timeline.set_decrypted_event(event_id, &plaintext.event_type, &plaintext.content);
2622 }
2623
2624 fn retry_pending_for_session(&mut self, room_id: &RoomId, session_id: &str) {
2625 let Some(by_event) = self.pending_decrypt.get(room_id) else { return };
2626 let candidates: Vec<EventId> =
2627 by_event.iter().filter(|(_, item)| item.session_id == session_id).map(|(event_id, _)| event_id.clone()).collect();
2628 let mut changed = false;
2629 for event_id in candidates {
2630 if self.retry_decrypt_one(room_id, &event_id) {
2631 changed = true;
2632 }
2633 }
2634 if changed {
2635 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
2636 }
2637 }
2638
2639 pub fn republish_device_keys(&mut self) {
2642 self.account.forget_published_device_keys();
2643 }
2644
2645 fn ingest_sync(&mut self, response: &crate::wire::sync::SyncResponse) -> Result<(), MessengerError> {
2646 let mut newly_received_sessions: Vec<(RoomId, String)> = Vec::new();
2647
2648 for event in &response.to_device.events {
2650 self.process_to_device_event(event, &mut newly_received_sessions)?;
2651 }
2652
2653 DeviceTracker::on_device_lists(&mut self.store, &response.device_lists.changed, &response.device_lists.left)?;
2655
2656 let published_otk_count = response.device_one_time_keys_count.get("signed_curve25519").copied().unwrap_or(0);
2658 self.account.on_sync_counts(&mut self.store, published_otk_count, &response.device_unused_fallback_key_types)?;
2659 let keys_upload_request_id = self.next_request_id()?;
2660 if let Some(request) =
2661 self.account.keys_upload_request(keys_upload_request_id, &self.config.user_id, &self.config.device_id)?
2662 {
2663 self.enqueue_request(request, Lane::Other)?;
2664 }
2665 self.maybe_enqueue_cross_signing()?;
2666
2667 let outdated_before_rooms = self.outdated_tracked_users()?;
2669 if !outdated_before_rooms.is_empty() {
2670 self.enqueue_keys_query()?;
2671 }
2672
2673 let rooms_touched =
2675 !response.rooms.join.is_empty() || !response.rooms.invite.is_empty() || !response.rooms.leave.is_empty();
2676 for (room_id, joined) in &response.rooms.join {
2677 self.ingest_joined_room(room_id, joined)?;
2678 }
2679 for (room_id, invited) in &response.rooms.invite {
2680 self.ingest_invited_room(room_id, invited)?;
2681 }
2682 for (room_id, left) in &response.rooms.leave {
2683 self.ingest_left_room(room_id, left)?;
2684 }
2685 if rooms_touched {
2686 self.emit(MessengerEvent::RoomsChanged);
2687 }
2688 if !self.outdated_tracked_users()?.is_subset(&outdated_before_rooms) {
2692 self.enqueue_keys_query()?;
2693 }
2694
2695 for event in &response.account_data.events {
2697 self.apply_synced_account_data(None, &event.event_type, event.content.clone());
2698 }
2699
2700 for (room_id, session_id) in &newly_received_sessions {
2701 self.retry_pending_for_session(room_id, session_id);
2702 }
2703
2704 self.store.save_sync_token(response.next_batch.clone())?;
2707 Ok(())
2708 }
2709
2710 fn process_to_device_event(
2711 &mut self,
2712 event: &ToDeviceEvent,
2713 newly_received_sessions: &mut Vec<(RoomId, String)>,
2714 ) -> Result<(), MessengerError> {
2715 if event.event_type != "m.room.encrypted" {
2716 return Ok(());
2717 }
2718 let Ok(RoomEncryptedContent::Olm(content)) = serde_json::from_value::<RoomEncryptedContent>(event.content.clone())
2719 else {
2720 return Ok(());
2721 };
2722 match OlmSessionManager::decrypt_to_device(&mut self.store, &mut self.account, &self.config.user_id, &event.sender, &content) {
2723 Ok(decrypted) => self.route_decrypted_to_device(decrypted, newly_received_sessions),
2724 Err(OlmDecryptError::UnknownSenderDevice { .. }) => {
2725 self.queue_unknown_sender_retry(event.clone());
2726 Ok(())
2727 }
2728 Err(_) => Ok(()),
2735 }
2736 }
2737
2738 fn route_decrypted_to_device(
2739 &mut self,
2740 decrypted: DecryptedToDevice,
2741 newly_received_sessions: &mut Vec<(RoomId, String)>,
2742 ) -> Result<(), MessengerError> {
2743 match decrypted.event_type.as_str() {
2744 "m.room_key" => {
2745 if let Ok(content) = serde_json::from_value::<RoomKeyContent>(decrypted.content.clone()) {
2746 if content.algorithm == "m.megolm.v1.aes-sha2" {
2747 GroupSessionManager::receive_room_key(
2748 &mut self.store,
2749 &decrypted.sender,
2750 decrypted.sender_device_curve25519,
2751 decrypted.sender_ed25519,
2752 &content,
2753 )?;
2754 newly_received_sessions.push((content.room_id, content.session_id));
2755 }
2756 }
2757 }
2758 "m.forwarded_room_key" => {
2759 if let Ok(content) = serde_json::from_value::<ForwardedRoomKeyContent>(decrypted.content.clone()) {
2760 if let Some((user_id, curve, ed)) = self.forward_binding(&decrypted, &content)? {
2761 let stored = GroupSessionManager::receive_forwarded_room_key(
2762 &mut self.store,
2763 &user_id,
2764 curve,
2765 ed,
2766 &content,
2767 )?;
2768 if stored {
2769 newly_received_sessions.push((content.room_id, content.session_id));
2770 }
2771 }
2772 }
2773 }
2774 "m.room_key_request" => {
2775 if let Ok(content) = serde_json::from_value::<RoomKeyRequestContent>(decrypted.content.clone()) {
2776 self.answer_room_key_request(&decrypted, &content)?;
2777 }
2778 }
2779 "m.room_key.withheld" => {
2780 if let Ok(content) = serde_json::from_value::<RoomKeyWithheldContent>(decrypted.content.clone()) {
2781 if let (Some(room_id), Some(session_id)) = (content.room_id, content.session_id) {
2782 self.withheld_sessions.insert((room_id, session_id));
2783 }
2784 }
2785 }
2786 _ => {}
2787 }
2788 Ok(())
2789 }
2790
2791 fn forward_binding(
2797 &self,
2798 decrypted: &DecryptedToDevice,
2799 content: &ForwardedRoomKeyContent,
2800 ) -> Result<Option<(UserId, mail4agent_vodozemac::Curve25519PublicKey, mail4agent_vodozemac::Ed25519PublicKey)>, MessengerError>
2801 {
2802 let members = self.encrypted_room_member_ids(&content.room_id);
2803 let forwarder_is_us = decrypted.sender == self.config.user_id
2806 || (self.history_forwarding_allowed(&content.room_id) && self.room_admins(&content.room_id).contains(&decrypted.sender));
2807 if !forwarder_is_us && !members.contains(&decrypted.sender) {
2808 return Ok(None);
2809 }
2810 for user_id in &members {
2811 for device in DeviceTracker::devices_for_user(&self.store, user_id)? {
2812 if device.blocked || device.curve25519.to_base64() != content.sender_key {
2813 continue;
2814 }
2815 let same_user = decrypted.sender == device.user_id;
2816 if same_user || forwarder_is_us {
2817 return Ok(Some((device.user_id, device.curve25519, device.ed25519)));
2818 }
2819 }
2820 }
2821 if decrypted.sender_device_curve25519.to_base64() == content.sender_key {
2822 return Ok(Some((
2823 decrypted.sender.clone(),
2824 decrypted.sender_device_curve25519,
2825 decrypted.sender_ed25519,
2826 )));
2827 }
2828 Ok(None)
2829 }
2830
2831 fn answer_room_key_request(
2836 &mut self,
2837 decrypted: &DecryptedToDevice,
2838 content: &RoomKeyRequestContent,
2839 ) -> Result<(), MessengerError> {
2840 if matches!(content.action, RoomKeyRequestAction::RequestCancellation) {
2841 self.key_requests_answered.remove(&(
2842 decrypted.sender.clone(),
2843 content.requesting_device_id.clone(),
2844 content.request_id.clone(),
2845 ));
2846 return Ok(());
2847 }
2848 if !matches!(content.action, RoomKeyRequestAction::Request) {
2849 return Ok(());
2850 }
2851 let Some(body) = content.body.as_ref() else { return Ok(()) };
2852 if body.algorithm != "m.megolm.v1.aes-sha2" {
2853 return Ok(());
2854 }
2855 let members = self.encrypted_room_member_ids(&body.room_id);
2856 if !members.contains(&decrypted.sender) {
2857 return Ok(());
2858 }
2859 if decrypted.sender != self.config.user_id && !self.history_forwarding_allowed(&body.room_id) {
2860 return Ok(());
2861 }
2862 if decrypted.sender == self.config.user_id && content.requesting_device_id == self.config.device_id {
2863 return Ok(());
2864 }
2865 let dedup = (decrypted.sender.clone(), content.requesting_device_id.clone(), content.request_id.clone());
2866 if self.key_requests_answered.contains(&dedup) {
2867 return Ok(());
2868 }
2869 let Some(target) = self.requester_device(decrypted, &content.requesting_device_id)? else {
2870 return Ok(());
2871 };
2872 let our_curve = self.account.identity_keys().curve25519.to_base64();
2873 let (inner_type, payload) =
2874 if let Some(forwarded) = GroupSessionManager::forwarded_room_key_content(
2875 &self.store,
2876 &body.room_id,
2877 &body.session_id,
2878 &our_curve,
2879 )? {
2880 ("m.forwarded_room_key", serde_json::to_value(&forwarded)?)
2881 } else if let Some(room_key) =
2882 GroupSessionManager::outbound_room_key_content(&self.store, &body.room_id, &body.session_id)?
2883 {
2884 ("m.room_key", serde_json::to_value(&room_key)?)
2885 } else {
2886 return Ok(());
2887 };
2888 let sent = self.enqueue_olm_to_devices(std::slice::from_ref(&target), inner_type, payload)?;
2889 if sent > 0 {
2890 self.key_requests_answered.insert(dedup);
2891 }
2892 Ok(())
2893 }
2894
2895 fn requester_device(
2899 &self,
2900 decrypted: &DecryptedToDevice,
2901 requesting_device_id: &DeviceId,
2902 ) -> Result<Option<StoredDevice>, MessengerError> {
2903 let devices = DeviceTracker::devices_for_user(&self.store, &decrypted.sender)?;
2904 if let Some(device) = devices.iter().find(|device| &device.device_id == requesting_device_id) {
2905 return Ok((!device.blocked).then(|| device.clone()));
2906 }
2907 let curve = decrypted.sender_device_curve25519.to_base64();
2908 if let Some(device) = devices.iter().find(|device| device.curve25519.to_base64() == curve) {
2909 return Ok((!device.blocked).then(|| device.clone()));
2910 }
2911 Ok(None)
2912 }
2913
2914 fn request_missing_room_key(
2919 &mut self,
2920 room_id: &RoomId,
2921 sender: &UserId,
2922 content: &MegolmEncryptedContent,
2923 ) -> Result<(), MessengerError> {
2924 let key = (room_id.clone(), content.session_id.clone());
2925 if self.key_requests_sent.contains(&key) || self.withheld_sessions.contains(&key) {
2926 return Ok(());
2927 }
2928 let sender_key = match content.sender_key.clone() {
2929 Some(sender_key) => sender_key,
2930 None => {
2931 let devices = DeviceTracker::devices_for_user(&self.store, sender)?;
2932 let found = content
2933 .device_id
2934 .as_ref()
2935 .and_then(|device_id| devices.iter().find(|device| &device.device_id == device_id))
2936 .or_else(|| devices.iter().find(|device| !device.blocked));
2937 match found {
2938 Some(device) => device.curve25519.to_base64(),
2939 None => return Ok(()),
2940 }
2941 }
2942 };
2943 let mut targets = DeviceTracker::devices_for_user(&self.store, sender)?;
2944 targets.extend(DeviceTracker::devices_for_user(&self.store, &self.config.user_id)?);
2945 if self.history_forwarding_allowed(room_id) {
2948 for admin in self.room_admins(room_id) {
2949 if &admin != sender && admin != self.config.user_id {
2950 targets.extend(DeviceTracker::devices_for_user(&self.store, &admin)?);
2951 }
2952 }
2953 }
2954 if targets.is_empty() {
2955 return Ok(());
2956 }
2957 let missing: Vec<StoredDevice> = OlmSessionManager::sessions_missing_for(&self.store, &targets)?
2961 .into_iter()
2962 .filter(|device| {
2963 !device.blocked
2964 && !(device.user_id == self.config.user_id && device.device_id == self.config.device_id)
2965 && !self.history_claim_tried.contains(&(device.user_id.clone(), device.device_id.clone()))
2966 })
2967 .cloned()
2968 .collect();
2969 if !missing.is_empty() {
2970 let claim_id = self.next_request_id()?;
2971 let refs: Vec<&StoredDevice> = missing.iter().collect();
2972 if let Some(request) = OlmSessionManager::keys_claim_request(claim_id.clone(), &refs) {
2973 for device in &missing {
2974 self.history_claim_tried.insert((device.user_id.clone(), device.device_id.clone()));
2975 }
2976 self.history_claims.insert(claim_id, missing);
2977 self.enqueue_request(request, Lane::Other)?;
2978 return Ok(());
2979 }
2980 }
2981 let request_id = self.next_txn_id()?;
2982 let body = RoomKeyRequestContent {
2983 action: RoomKeyRequestAction::Request,
2984 body: Some(RoomKeyRequestBody {
2985 algorithm: "m.megolm.v1.aes-sha2".to_string(),
2986 room_id: room_id.clone(),
2987 session_id: content.session_id.clone(),
2988 sender_key,
2989 }),
2990 request_id: request_id.as_str().to_string(),
2991 requesting_device_id: self.config.device_id.clone(),
2992 };
2993 let sent = self.enqueue_olm_to_devices(&targets, "m.room_key_request", serde_json::to_value(&body)?)?;
2994 if sent > 0 {
2995 self.key_requests_sent.insert(key);
2996 }
2997 Ok(())
2998 }
2999
3000 fn enqueue_olm_to_devices(
3005 &mut self,
3006 devices: &[StoredDevice],
3007 inner_type: &str,
3008 content: serde_json::Value,
3009 ) -> Result<usize, MessengerError> {
3010 let mut unique = Vec::new();
3011 for device in devices {
3012 if device.blocked || (device.user_id == self.config.user_id && device.device_id == self.config.device_id) {
3013 continue;
3014 }
3015 if unique.iter().any(|have: &StoredDevice| have.user_id == device.user_id && have.device_id == device.device_id)
3016 {
3017 continue;
3018 }
3019 unique.push(device.clone());
3020 }
3021 let missing = OlmSessionManager::sessions_missing_for(&self.store, &unique)?;
3022 let missing_ids: BTreeSet<(UserId, DeviceId)> =
3023 missing.into_iter().map(|device| (device.user_id.clone(), device.device_id.clone())).collect();
3024 let ready: Vec<StoredDevice> = unique
3025 .into_iter()
3026 .filter(|device| !missing_ids.contains(&(device.user_id.clone(), device.device_id.clone())))
3027 .collect();
3028 let mut sent = 0;
3029 for chunk in GroupSessionManager::chunk_recipients_for_send_to_device(&ready) {
3030 let body = GroupSessionManager::build_encrypted_to_device_body(
3031 &mut self.store,
3032 &self.account,
3033 &self.config.user_id,
3034 &self.config.device_id,
3035 chunk,
3036 inner_type,
3037 content.clone(),
3038 )?;
3039 let request_id = self.next_request_id()?;
3040 let share_txn = self.next_txn_id()?;
3041 let request = OutgoingRequest::send_to_device(request_id, ROOM_KEY_SHARE_EVENT_TYPE, &share_txn, body);
3042 self.enqueue_request(request, Lane::ToDevice)?;
3043 sent += chunk.len();
3044 }
3045 Ok(sent)
3046 }
3047
3048 fn queue_unknown_sender_retry(&mut self, event: ToDeviceEvent) {
3049 self.unknown_sender_retry.push_back(event);
3050 while self.unknown_sender_retry.len() > UNKNOWN_SENDER_RETRY_CAPACITY {
3051 self.unknown_sender_retry.pop_front();
3052 }
3053 }
3054
3055 fn retry_unknown_sender_queue(&mut self) -> Result<(), MessengerError> {
3056 let pending: Vec<ToDeviceEvent> = std::mem::take(&mut self.unknown_sender_retry).into_iter().collect();
3057 let mut newly_received_sessions = Vec::new();
3058 for event in pending {
3059 self.process_to_device_event(&event, &mut newly_received_sessions)?;
3060 }
3061 for (room_id, session_id) in &newly_received_sessions {
3062 self.retry_pending_for_session(room_id, session_id);
3063 }
3064 Ok(())
3065 }
3066
3067 fn outdated_tracked_users(&self) -> Result<BTreeSet<UserId>, MessengerError> {
3069 Ok(self.store.tracked_users()?.into_iter().filter_map(|(user_id, outdated)| outdated.then_some(user_id)).collect())
3070 }
3071
3072 fn enqueue_keys_query(&mut self) -> Result<(), MessengerError> {
3074 let request_id = self.next_request_id()?;
3075 if let Some(request) = DeviceTracker::keys_query_request(&self.store, request_id)? {
3076 self.enqueue_request(request, Lane::Other)?;
3077 }
3078 Ok(())
3079 }
3080
3081 fn retry_missing_room_keys(&mut self) {
3091 let work: Vec<(RoomId, EventId)> = self
3092 .pending_decrypt
3093 .iter()
3094 .flat_map(|(room, by_event)| by_event.keys().map(move |event| (room.clone(), event.clone())))
3095 .collect();
3096 for (room_id, event_id) in work {
3097 let _ = self.retry_decrypt_one(&room_id, &event_id);
3098 }
3099 }
3100
3101 fn room_admins(&self, room_id: &RoomId) -> BTreeSet<UserId> {
3106 let Some(state) = self.room_state(room_id) else { return BTreeSet::new() };
3107 self.encrypted_room_member_ids(room_id)
3108 .into_iter()
3109 .filter(|u| state.power_levels.users.get(u).copied().unwrap_or(state.power_levels.users_default) >= 100)
3110 .collect()
3111 }
3112
3113 fn history_forwarding_allowed(&self, room_id: &RoomId) -> bool {
3114 !matches!(
3115 self.rooms.get(room_id).and_then(|room| room.history_visibility.clone()),
3116 Some(crate::wire::events::HistoryVisibility::Joined) | Some(crate::wire::events::HistoryVisibility::Invited)
3117 )
3118 }
3119
3120 fn maybe_enqueue_cross_signing(&mut self) -> Result<(), MessengerError> {
3123 if self.xsign_inflight || self.xsign_attempts >= 4 {
3124 return Ok(());
3125 }
3126 if self.xsign.is_none() {
3127 self.xsign = Some(CrossSigningState::load_or_create(&mut self.store)?);
3128 }
3129 let Some(state) = self.xsign.clone() else { return Ok(()) };
3130 let user_id = self.config.user_id.clone();
3131 let request_id;
3132 let request = if !state.uploaded {
3133 request_id = self.next_request_id()?;
3134 OutgoingRequest::signing_keys_upload(request_id, state.upload_body(&user_id)?)
3135 } else if !state.device_signed {
3136 let device_id = self.config.device_id.clone();
3137 let device_keys = self.account.device_keys_json(&user_id, &device_id)?;
3138 request_id = self.next_request_id()?;
3139 OutgoingRequest::signatures_upload(request_id, state.device_signature_body(&user_id, &device_id, device_keys)?)
3140 } else {
3141 return Ok(());
3142 };
3143 self.xsign_attempts += 1;
3144 self.xsign_inflight = true;
3145 self.enqueue_request(request, Lane::Other)
3146 }
3147
3148 fn track_joined_members<'a>(&mut self, room_id: &RoomId, events: impl Iterator<Item = &'a RawEvent>) -> Result<(), MessengerError> {
3149 if self.rooms.get(room_id).is_none_or(|room| room.encryption.is_none()) {
3150 return Ok(());
3151 }
3152 let joined: Vec<UserId> = events
3153 .filter(|event| event.event_type == "m.room.member")
3154 .filter(|event| event.content.get("membership").and_then(serde_json::Value::as_str) == Some("join"))
3155 .filter_map(|event| event.state_key.as_deref().and_then(|state_key| UserId::parse(state_key).ok()))
3156 .filter(|user_id| *user_id != self.config.user_id)
3157 .collect();
3158 if joined.is_empty() {
3159 return Ok(());
3160 }
3161 DeviceTracker::on_device_lists(&mut self.store, &joined, &[])
3162 }
3163
3164 fn ingest_joined_room(&mut self, room_id: &RoomId, joined: &JoinedRoom) -> Result<(), MessengerError> {
3165 for event in joined.state.events.iter().chain(joined.timeline.events.iter()) {
3166 self.rooms.entry(room_id.clone()).or_default().apply_state_event(event)?;
3167 }
3168
3169 self.track_joined_members(room_id, joined.state.events.iter().chain(joined.timeline.events.iter()))?;
3170
3171 let unread_before = self.rooms.get(room_id).map(RoomState::unread_count);
3172 self.rooms.entry(room_id.clone()).or_default().apply_summary(&joined.summary, &joined.unread_notifications);
3173 let unread_after = self.rooms.get(room_id).map(RoomState::unread_count);
3174 if unread_before != unread_after {
3175 self.emit(MessengerEvent::UnreadChanged { room_id: room_id.clone() });
3176 }
3177
3178 for event in &joined.account_data.events {
3179 self.apply_synced_account_data(Some(room_id), &event.event_type, event.content.clone());
3180 }
3181
3182 if let Some(typing_event) = joined.ephemeral.events.iter().find(|event| event.event_type == "m.typing") {
3183 if let Ok(typing) = serde_json::from_value::<TypingContent>(typing_event.content.clone()) {
3184 self.typing.insert(room_id.clone(), typing.user_ids);
3185 self.emit(MessengerEvent::TypingChanged { room_id: room_id.clone() });
3186 }
3187 }
3188 if let Some(receipt_event) = joined.ephemeral.events.iter().find(|event| event.event_type == "m.receipt") {
3189 if let Ok(receipts) = serde_json::from_value::<ReceiptContent>(receipt_event.content.clone()) {
3190 self.receipts.insert(room_id.clone(), receipts);
3191 self.emit(MessengerEvent::ReceiptsChanged { room_id: room_id.clone() });
3192 }
3193 }
3194
3195 if !joined.timeline.events.is_empty() {
3196 self.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(
3197 &joined.timeline.events,
3198 joined.timeline.limited,
3199 joined.timeline.prev_batch.clone(),
3200 );
3201 self.decrypt_new_timeline_events(room_id, &joined.timeline.events);
3202 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3203 }
3204
3205 self.persist_room_state(room_id)
3206 }
3207
3208 fn ingest_invited_room(&mut self, room_id: &RoomId, invited: &InvitedRoom) -> Result<(), MessengerError> {
3209 for stripped in &invited.invite_state.events {
3210 let synthetic = stripped_state_to_raw_event(stripped);
3211 self.rooms.entry(room_id.clone()).or_default().apply_state_event(&synthetic)?;
3212 }
3213 self.persist_room_state(room_id)
3214 }
3215
3216 fn ingest_left_room(&mut self, room_id: &RoomId, left: &LeftRoom) -> Result<(), MessengerError> {
3217 for event in left.state.events.iter().chain(left.timeline.events.iter()) {
3218 self.rooms.entry(room_id.clone()).or_default().apply_state_event(event)?;
3219 }
3220 for event in &left.account_data.events {
3221 self.apply_synced_account_data(Some(room_id), &event.event_type, event.content.clone());
3222 }
3223 if !left.timeline.events.is_empty() {
3224 self.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(
3225 &left.timeline.events,
3226 left.timeline.limited,
3227 left.timeline.prev_batch.clone(),
3228 );
3229 self.decrypt_new_timeline_events(room_id, &left.timeline.events);
3230 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3231 }
3232 self.persist_room_state(room_id)
3233 }
3234
3235 fn persist_room_state(&mut self, room_id: &RoomId) -> Result<(), MessengerError> {
3236 let Some(state) = self.rooms.get(room_id) else { return Ok(()) };
3237 let bytes = serde_json::to_vec(state)
3238 .map_err(|source| MessengerError::Crypto(format!("encode room state for {room_id}: {source}")))?;
3239 self.store.save_room_state(room_id, bytes)?;
3240 Ok(())
3241 }
3242
3243 fn decrypt_new_timeline_events(&mut self, room_id: &RoomId, events: &[RawEvent]) {
3251 for event in events {
3252 if event.event_type != "m.room.encrypted" || event.state_key.is_some() || event.unsigned.redacted_because.is_some() {
3253 continue;
3254 }
3255 let Ok(RoomEncryptedContent::Megolm(content)) = serde_json::from_value::<RoomEncryptedContent>(event.content.clone())
3256 else {
3257 continue;
3258 };
3259 match GroupSessionManager::decrypt_event(
3260 &mut self.store,
3261 room_id,
3262 &event.event_id,
3263 event.origin_server_ts,
3264 &event.sender,
3265 &content,
3266 ) {
3267 Ok(plaintext) => {
3268 self.apply_decrypted_plaintext(room_id, &event.event_id, &event.sender, event.origin_server_ts, &plaintext);
3269 if let Some(by_event) = self.pending_decrypt.get_mut(room_id) {
3270 by_event.remove(&event.event_id);
3271 }
3272 }
3273 Err(err) => {
3274 if matches!(err, GroupDecryptError::MissingSession { .. }) {
3275 let _ = self.request_missing_room_key(room_id, &event.sender, &content);
3276 }
3277 let reason = format!("{:?}", err.utd_reason());
3286 if let Some(timeline) = self.timelines.get_mut(room_id) {
3287 timeline.set_decrypted(&event.event_id, ItemContent::Undecryptable { reason });
3288 }
3289 self.pending_decrypt.entry(room_id.clone()).or_default().insert(
3290 event.event_id.clone(),
3291 PendingDecryptItem {
3292 session_id: content.session_id.clone(),
3293 sender: event.sender.clone(),
3294 origin_server_ts: event.origin_server_ts,
3295 content,
3296 },
3297 );
3298 }
3299 }
3300 }
3301 }
3302
3303 fn ingest_room_messages_response(&mut self, room_id: &RoomId, body: &[u8]) -> Result<(), MessengerError> {
3304 let parsed: RoomMessagesResponseBody = serde_json::from_slice(body)?;
3305 let mut events = parsed.chunk;
3306 events.reverse();
3307 self.timelines.entry(room_id.clone()).or_default().prepend_back_page(&events, parsed.end);
3308 self.decrypt_new_timeline_events(room_id, &events);
3309 self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3310 self.persist_room_state(room_id)
3311 }
3312}
3313
3314impl MessengerCore<SealedRecordCodec> {
3315 pub fn open_sealed(
3326 records: impl IntoIterator<Item = SealedRecord>,
3327 secrets: CoreSecrets,
3328 config: CoreConfig,
3329 now_ms: i64,
3330 jitter: Box<dyn Jitter>,
3331 ) -> Result<Self, MessengerError> {
3332 let seal_key = secrets.store_seal_key.clone().ok_or_else(|| {
3333 MessengerError::Crypto(
3334 "MessengerCore::open_sealed requires CoreSecrets::store_seal_key"
3335 .to_string(),
3336 )
3337 })?;
3338 let codec = SealedRecordCodec::new(seal_key);
3339 Self::open(records, codec, config, secrets, now_ms, jitter)
3340 }
3341}
3342
3343fn stripped_placeholder_event_id() -> EventId {
3349 EventId::parse("$stripped-preview").expect("this literal is a well-formed, hardcoded $-prefixed opaque id")
3350}
3351
3352fn stripped_state_to_raw_event(stripped: &StrippedStateEvent) -> RawEvent {
3357 RawEvent {
3358 event_id: stripped_placeholder_event_id(),
3359 event_type: stripped.event_type.clone(),
3360 sender: stripped.sender.clone(),
3361 origin_server_ts: 0,
3362 state_key: Some(stripped.state_key.clone()),
3363 content: stripped.content.clone(),
3364 unsigned: Unsigned::default(),
3365 }
3366}
3367
3368fn forwardable_content(mut content: serde_json::Value, marker_key: &str, marker: serde_json::Value) -> serde_json::Value {
3375 if let Some(object) = content.as_object_mut() {
3376 for key in ["m.relates_to", "m.mentions", "m.new_content", KEY_FORWARDED, KEY_FORWARDED_FROM] {
3377 object.remove(key);
3378 }
3379 object.insert(marker_key.to_string(), marker);
3380 }
3381 content
3382}
3383
3384fn message_local_echo_content(message: &OutgoingMessage) -> ItemContent {
3391 let body = TextLikeMessageContent { body: message.body.clone(), format: None, formatted_body: None };
3392 match message.kind {
3393 MessageKind::Text => ItemContent::Text(body),
3394 MessageKind::Notice => ItemContent::Notice(body),
3395 MessageKind::Emote => ItemContent::Emote(body),
3396 }
3397}
3398
3399fn message_wire_content(message: &OutgoingMessage) -> serde_json::Value {
3408 let msgtype = message.kind.msgtype();
3409 let new_content = serde_json::json!({ "msgtype": msgtype, "body": message.body });
3410 let mut content = new_content.clone();
3411 if let Some(edit_of) = &message.edit_of {
3412 content["body"] = serde_json::Value::String(format!("* {}", message.body));
3413 content["m.new_content"] = new_content;
3414 content["m.relates_to"] = serde_json::json!({ "rel_type": "m.replace", "event_id": edit_of.as_str() });
3415 } else if let Some(reply_to) = &message.reply_to {
3416 content["m.relates_to"] = serde_json::json!({ "m.in_reply_to": { "event_id": reply_to.as_str() } });
3417 }
3418 content
3419}
3420
3421fn message_relates_to(message: &OutgoingMessage) -> Option<RelatesTo> {
3427 if let Some(edit_of) = &message.edit_of {
3428 Some(RelatesTo::Replace { event_id: edit_of.clone() })
3429 } else {
3430 message.reply_to.clone().map(|event_id| RelatesTo::InReplyTo(InReplyTo { event_id }))
3431 }
3432}
3433
3434#[cfg(test)]
3435mod tests {
3436 use super::*;
3437 use crate::room::timeline::Forwarded;
3438 use crate::store::InsecurePlainCodecForTests;
3439 use crate::wire::HttpMethod;
3440
3441 struct FixedJitter(f64);
3442
3443 impl Jitter for FixedJitter {
3444 fn next_unit(&mut self) -> f64 {
3445 self.0
3446 }
3447 }
3448
3449 fn device_id() -> DeviceId {
3450 DeviceId::parse("DEV1").expect("valid device id")
3451 }
3452
3453 fn user_id() -> UserId {
3454 UserId::parse("@alice:example.org").expect("valid user id")
3455 }
3456
3457 fn test_config() -> CoreConfig {
3458 CoreConfig { user_id: user_id(), device_id: device_id(), server_name: "example.org".to_string() }
3459 }
3460
3461 fn open_fresh_core() -> MessengerCore<InsecurePlainCodecForTests> {
3462 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, test_config(), CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3463 .expect("open succeeds on a brand-new device")
3464 }
3465
3466 fn flush_and_ack(core: &mut MessengerCore<InsecurePlainCodecForTests>) {
3474 while let Some(batch) = core.take_flush_batch() {
3475 core.ack_flush(batch.id);
3476 }
3477 }
3478
3479 #[test]
3480 fn counters_survive_restart_and_never_repeat_txn_ids() {
3481 let device = device_id();
3482 let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
3483 store.save_counters(Counters { next_request_id: 3, next_txn_id: 7 }).expect("save");
3484 let batch = store.take_flush_batch().expect("counters dirtied the store");
3485 store.ack_flush(batch.id);
3486
3487 let reloaded = Store::load(batch.records, InsecurePlainCodecForTests, device).expect("reload");
3488 let restored = reloaded.counters().expect("no error").expect("counters were persisted");
3489 assert_eq!(restored.next_txn_id, 7, "a restart resumes the txn-id sequence exactly where it left off");
3490 assert_eq!(restored.next_request_id, 3);
3491
3492 assert_eq!(restore_counter(3, Some(10)), 11, "resumes past the highest pending id, never repeating it");
3497 assert_eq!(restore_counter(3, None), 3, "nothing pending: the persisted value alone is authoritative");
3498 }
3499
3500 #[test]
3501 fn open_restores_the_request_counter_past_a_still_pending_requests_id() {
3502 let device = device_id();
3503 let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
3504 let mut queue = OutgoingQueue::new();
3505 let pending = OutgoingRequest::sync(RequestId::next(5), None, None);
3506 queue.enqueue(&mut store, pending, Lane::Sync).expect("enqueue");
3507 store.save_counters(Counters { next_request_id: 2, next_txn_id: 0 }).expect("save");
3510 let batch = store.take_flush_batch().expect("pending");
3511 store.ack_flush(batch.id);
3512
3513 let core = MessengerCore::open(
3514 batch.records,
3515 InsecurePlainCodecForTests,
3516 test_config(),
3517 CoreSecrets::default(),
3518 0,
3519 Box::new(FixedJitter(0.0)),
3520 )
3521 .expect("open succeeds");
3522 assert_eq!(
3523 core.counters.next_request_id, 6,
3524 "resumes past the highest id already in flight, not the stale persisted value"
3525 );
3526 }
3527
3528 #[test]
3529 fn sync_token_persisted_after_everything_it_covers() {
3530 let mut core = open_fresh_core();
3531 flush_and_ack(&mut core);
3532 core.releasable_requests(0);
3537 flush_and_ack(&mut core);
3538 let requests = core.releasable_requests(0);
3539 let sync_request = requests.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("a sync request was enqueued");
3540
3541 assert!(core.store.sync_token().expect("no error").is_none(), "no token before the first response");
3542
3543 let body = serde_json::json!({
3544 "next_batch": "s1",
3545 "device_one_time_keys_count": { "signed_curve25519": 0 },
3546 "device_unused_fallback_key_types": []
3547 })
3548 .to_string();
3549 core.on_response(sync_request.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
3550
3551 assert_eq!(core.store.sync_token().expect("no error"), Some("s1"));
3552 }
3553
3554 #[test]
3555 fn only_one_sync_in_flight() {
3556 let mut core = open_fresh_core();
3557 flush_and_ack(&mut core);
3558 core.releasable_requests(0);
3559 flush_and_ack(&mut core);
3560 let first = core.releasable_requests(0);
3561 assert_eq!(first.iter().filter(|r| r.kind == OutgoingRequestKind::Sync).count(), 1);
3562
3563 let second = core.releasable_requests(0);
3567 assert!(second.iter().all(|r| r.kind != OutgoingRequestKind::Sync), "no second sync request released");
3568 }
3569
3570 #[test]
3571 fn first_sync_of_a_session_never_long_polls_then_the_loop_does() {
3572 let mut core = open_fresh_core();
3573 flush_and_ack(&mut core);
3574 core.releasable_requests(0);
3575 flush_and_ack(&mut core);
3576 let first = core.releasable_requests(0);
3577 let first_sync = first.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("a sync request was enqueued");
3578 assert!(first_sync.query.contains(&("timeout".to_string(), "0".to_string())), "the initial sync must answer at once");
3579 assert!(first_sync.query.iter().all(|(key, _)| key != "since"), "a fresh device has no since token");
3580
3581 let body = serde_json::json!({
3582 "next_batch": "s1",
3583 "device_one_time_keys_count": { "signed_curve25519": 0 },
3584 "device_unused_fallback_key_types": []
3585 })
3586 .to_string();
3587 core.on_response(first_sync.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
3588 flush_and_ack(&mut core);
3589
3590 core.releasable_requests(0);
3593 flush_and_ack(&mut core);
3594 let second = core.releasable_requests(0);
3595 let second_sync = second.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("the follow-up sync was enqueued");
3596 assert!(second_sync.query.contains(&("since".to_string(), "s1".to_string())));
3597 assert!(second_sync.query.contains(&("timeout".to_string(), SYNC_TIMEOUT_MS.to_string())), "once caught up the loop long-polls");
3598 }
3599
3600 #[test]
3601 fn room_key_arrival_retries_undecryptable_items() {
3602 use crate::crypto::group_sessions::GroupSessionManager;
3603 use mail4agent_vodozemac::megolm::{GroupSession, SessionConfig};
3604 use mail4agent_vodozemac::olm::Account;
3605
3606 let mut core = open_fresh_core();
3607 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3608 let sender = UserId::parse("@bob:example.org").expect("valid user id");
3609 let identity = Account::new().identity_keys();
3610
3611 let mut group_session = GroupSession::new(SessionConfig::version_1());
3612 let session_id = group_session.session_id();
3613 let session_key = group_session.session_key();
3618 let plaintext = crate::crypto::group_sessions::RoomEventPlaintext {
3619 event_type: "m.room.message".to_string(),
3620 content: serde_json::json!({ "msgtype": "m.text", "body": "hi" }),
3621 room_id: room_id.clone(),
3622 };
3623 let message = group_session.encrypt(serde_json::to_vec(&plaintext).expect("valid JSON"));
3624
3625 let event: RawEvent = serde_json::from_value(serde_json::json!({
3626 "event_id": "$evt:example.org",
3627 "type": "m.room.encrypted",
3628 "sender": sender.as_str(),
3629 "origin_server_ts": 1,
3630 "content": {
3631 "algorithm": "m.megolm.v1.aes-sha2",
3632 "ciphertext": message.to_base64(),
3633 "session_id": session_id,
3634 }
3635 }))
3636 .expect("valid event");
3637
3638 let mut timeline = Timeline::new();
3644 timeline.apply_timeline_batch(std::slice::from_ref(&event), false, None);
3645 core.timelines.insert(room_id.clone(), timeline);
3646 core.decrypt_new_timeline_events(&room_id, std::slice::from_ref(&event));
3647 {
3648 let timeline = core.timeline(&room_id).expect("timeline exists");
3649 let item = timeline.item_by_event_id(&EventId::parse("$evt:example.org").expect("valid event id")).expect("item present");
3650 assert!(matches!(item.content, ItemContent::Undecryptable { .. }));
3651 }
3652
3653 let content = RoomKeyContent {
3657 algorithm: "m.megolm.v1.aes-sha2".to_string(),
3658 room_id: room_id.clone(),
3659 session_id: session_id.clone(),
3660 session_key: session_key.to_base64(),
3661 };
3662 GroupSessionManager::receive_room_key(&mut core.store, &sender, identity.curve25519, identity.ed25519, &content)
3663 .expect("receive room key");
3664 core.retry_pending_for_session(&room_id, &session_id);
3665
3666 let timeline = core.timeline(&room_id).expect("timeline exists");
3667 let item = timeline.item_by_event_id(&EventId::parse("$evt:example.org").expect("valid event id")).expect("item present");
3668 match &item.content {
3669 ItemContent::Text(text) => assert_eq!(text.body, "hi"),
3670 other => panic!("expected the retried decrypt to succeed, got {other:?}"),
3671 }
3672 }
3673
3674 #[test]
3675 fn redelivered_to_device_after_restart_is_dropped_silently() {
3676 use crate::crypto::olm_sessions::OlmSessionManager;
3677
3678 let alice_user = UserId::parse("@alice:example.org").expect("valid user id");
3680 let alice_device = DeviceId::parse("ALICEDEV").expect("valid device id");
3681 let mut alice_store = Store::new(alice_device.clone(), InsecurePlainCodecForTests);
3682 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
3683 let alice_device_keys =
3686 alice_account.device_keys_json(&alice_user, &alice_device).expect("sign alice's device_keys");
3687
3688 let mut bob_store = Store::new(device_id(), InsecurePlainCodecForTests);
3690 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
3691 bob_account.on_sync_counts(&mut bob_store, 0, &["signed_curve25519".to_string()]).expect("top up bob's OTKs");
3692 let upload = bob_account
3693 .keys_upload_request(RequestId::next(0), &user_id(), &device_id())
3694 .expect("builds a request")
3695 .expect("something to upload");
3696 let one_time_keys = upload.body.expect("upload has a body");
3697 let one_time_keys = one_time_keys.get("one_time_keys").and_then(serde_json::Value::as_object).expect("OTKs present");
3698 let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
3699 let claim_body = serde_json::to_vec(&serde_json::json!({
3700 "one_time_keys": { user_id().as_str(): { device_id().as_str(): { key_id: key_value } } }
3701 }))
3702 .expect("valid JSON");
3703 let bob_device_stored = crate::crypto::device_tracker::StoredDevice {
3704 user_id: user_id(),
3705 device_id: device_id(),
3706 curve25519: bob_account.identity_keys().curve25519,
3707 ed25519: bob_account.identity_keys().ed25519,
3708 algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
3709 display_name: None,
3710 verified: false,
3711 blocked: false,
3712 };
3713 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device_stored], &claim_body)
3714 .expect("alice establishes a session with bob");
3715
3716 DeviceTracker::on_keys_query_response(
3722 &mut bob_store,
3723 &serde_json::to_vec(&serde_json::json!({
3724 "device_keys": { alice_user.as_str(): { alice_device.as_str(): alice_device_keys } }
3725 }))
3726 .expect("valid JSON"),
3727 )
3728 .expect("bob learns alice's device");
3729
3730 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3731 let group_session = mail4agent_vodozemac::megolm::GroupSession::new(mail4agent_vodozemac::megolm::SessionConfig::version_1());
3732 let room_key_content = RoomKeyContent {
3733 algorithm: "m.megolm.v1.aes-sha2".to_string(),
3734 room_id,
3735 session_id: group_session.session_id(),
3736 session_key: group_session.session_key().to_base64(),
3737 };
3738 let encrypted = OlmSessionManager::encrypt_to_device(
3739 &mut alice_store,
3740 &alice_account,
3741 &alice_user,
3742 &alice_device,
3743 &bob_device_stored,
3744 "m.room_key",
3745 serde_json::to_value(&room_key_content).expect("valid JSON"),
3746 )
3747 .expect("encrypt to bob");
3748 let to_device_event = ToDeviceEvent {
3749 sender: alice_user.clone(),
3750 event_type: "m.room.encrypted".to_string(),
3751 content: serde_json::to_value(RoomEncryptedContent::Olm(encrypted)).expect("valid JSON"),
3752 };
3753
3754 let mut core =
3755 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, test_config(), CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3756 .expect("open succeeds");
3757 core.store = bob_store;
3758 core.account = bob_account;
3759
3760 let mut sessions = Vec::new();
3761 core.process_to_device_event(&to_device_event, &mut sessions).expect("first decrypt succeeds");
3762 assert_eq!(
3763 sessions,
3764 vec![(room_key_content.room_id.clone(), room_key_content.session_id.clone())],
3765 "the first, genuine delivery is fully validated and accepted"
3766 );
3767
3768 let batch = core.store.take_flush_batch().expect("decrypting dirtied the store");
3772 core.store.ack_flush(batch.id);
3773 let reloaded_store = Store::load(batch.records, InsecurePlainCodecForTests, device_id()).expect("reload succeeds");
3774 core.store = reloaded_store;
3775 core.account = OlmAccountState::load_or_create(&mut core.store).expect("reload reuses the persisted account");
3776
3777 let mut sessions_after_restart = Vec::new();
3781 let result = core.process_to_device_event(&to_device_event, &mut sessions_after_restart);
3782 assert!(result.is_ok(), "a redelivered to-device event is dropped, not surfaced as an ingestion error");
3783 assert!(sessions_after_restart.is_empty(), "the replay never produces a second accepted room key");
3784 }
3785
3786 #[test]
3787 fn unknown_sender_device_is_retried_after_keys_query() {
3788 use crate::crypto::olm_sessions::OlmSessionManager;
3789
3790 let alice_user = UserId::parse("@alice:example.org").expect("valid user id");
3792 let alice_device = DeviceId::parse("ALICEDEV").expect("valid device id");
3793 let mut alice_store = Store::new(alice_device.clone(), InsecurePlainCodecForTests);
3794 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
3795 let alice_device_keys =
3803 alice_account.device_keys_json(&alice_user, &alice_device).expect("sign alice's device_keys");
3804
3805 let bob_user = UserId::parse("@bob:example.org").expect("valid user id");
3813 let bob_device = DeviceId::parse("BOBDEV").expect("valid device id");
3814 let mut bob_store = Store::new(bob_device.clone(), InsecurePlainCodecForTests);
3815 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
3816 bob_account.on_sync_counts(&mut bob_store, 0, &["signed_curve25519".to_string()]).expect("top up bob's OTKs");
3817 let upload = bob_account
3818 .keys_upload_request(RequestId::next(0), &bob_user, &bob_device)
3819 .expect("builds a request")
3820 .expect("something to upload");
3821 let one_time_keys = upload.body.expect("upload has a body");
3822 let one_time_keys = one_time_keys.get("one_time_keys").and_then(serde_json::Value::as_object).expect("OTKs present");
3823 let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
3824 let claim_body = serde_json::to_vec(&serde_json::json!({
3825 "one_time_keys": { bob_user.as_str(): { bob_device.as_str(): { key_id: key_value } } }
3826 }))
3827 .expect("valid JSON");
3828 let bob_device_stored = crate::crypto::device_tracker::StoredDevice {
3829 user_id: bob_user.clone(),
3830 device_id: bob_device.clone(),
3831 curve25519: bob_account.identity_keys().curve25519,
3832 ed25519: bob_account.identity_keys().ed25519,
3833 algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
3834 display_name: None,
3835 verified: false,
3836 blocked: false,
3837 };
3838 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device_stored], &claim_body)
3839 .expect("alice establishes a session with bob");
3840 let encrypted = OlmSessionManager::encrypt_to_device(
3841 &mut alice_store,
3842 &alice_account,
3843 &alice_user,
3844 &alice_device,
3845 &bob_device_stored,
3846 "m.dummy",
3847 serde_json::json!({}),
3848 )
3849 .expect("encrypt to bob");
3850 let to_device_event = ToDeviceEvent {
3851 sender: alice_user.clone(),
3852 event_type: "m.room.encrypted".to_string(),
3853 content: serde_json::to_value(RoomEncryptedContent::Olm(encrypted)).expect("valid JSON"),
3854 };
3855
3856 let bob_config = CoreConfig { user_id: bob_user.clone(), device_id: bob_device.clone(), server_name: "example.org".to_string() };
3857 let mut core =
3858 MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, bob_config, CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3859 .expect("open succeeds");
3860 core.store = bob_store;
3861 core.account = bob_account;
3862
3863 let mut sessions = Vec::new();
3867 core.process_to_device_event(&to_device_event, &mut sessions).expect("queues for retry, does not error");
3868 assert_eq!(core.unknown_sender_retry.len(), 1, "queued for a retry after the next keys/query");
3869 let alice_curve = alice_account.identity_keys().curve25519.to_base64();
3870 assert!(
3871 core.store.olm_sessions_for_device(&alice_curve).expect("no error").is_empty(),
3872 "the refused attempt created no Olm session (it would have consumed the one-time key)"
3873 );
3874
3875 let query_body = serde_json::to_vec(&serde_json::json!({
3880 "device_keys": { alice_user.as_str(): { alice_device.as_str(): alice_device_keys } }
3881 }))
3882 .expect("valid JSON");
3883 DeviceTracker::on_keys_query_response(&mut core.store, &query_body).expect("bob learns alice's device");
3884 core.retry_unknown_sender_queue().expect("no error");
3885 assert!(core.unknown_sender_retry.is_empty(), "the queued entry is drained by the retry attempt");
3886 assert_eq!(
3887 core.store.olm_sessions_for_device(&alice_curve).expect("no error").len(),
3888 1,
3889 "the retry decrypted the very same pre-key message and established the inbound session"
3890 );
3891 }
3892
3893 #[test]
3894 fn dispatch_load_older_produces_an_outgoing_request() {
3895 let mut core = open_fresh_core();
3896 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3897 let mut timeline = Timeline::new();
3898 let event: RawEvent = serde_json::from_value(serde_json::json!({
3899 "event_id": "$1:example.org",
3900 "type": "m.room.message",
3901 "sender": "@alice:example.org",
3902 "origin_server_ts": 1,
3903 "content": { "msgtype": "m.text", "body": "hi" }
3904 }))
3905 .expect("valid event");
3906 timeline.apply_timeline_batch(&[event], true, Some("t1".to_string()));
3907 core.timelines.insert(room_id.clone(), timeline);
3908 flush_and_ack(&mut core);
3909
3910 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3911 flush_and_ack(&mut core);
3912 let requests = core.releasable_requests(0);
3913 let load_older = requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomMessages);
3914 assert!(load_older.is_some(), "a RoomMessages request was enqueued using the room's own gap token");
3915 }
3916
3917 #[test]
3918 fn load_older_on_a_resumed_room_pages_back_from_the_sync_token() {
3919 let mut core = open_fresh_core();
3920 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3921 core.store.save_sync_token("s42_7".to_string()).expect("save the sync token");
3922 flush_and_ack(&mut core);
3923
3924 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3926 flush_and_ack(&mut core);
3927 assert!(core.releasable_requests(0).iter().all(|r| r.kind != OutgoingRequestKind::RoomMessages));
3928
3929 core.rooms.insert(room_id.clone(), RoomState::default());
3931 core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3932 flush_and_ack(&mut core);
3933 let requests = core.releasable_requests(0);
3934 let load_older = requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomMessages).expect("a RoomMessages request was enqueued");
3935 assert!(load_older.query.contains(&("from".to_string(), "s42_7".to_string())), "pages back from the sync token: {:?}", load_older.query);
3936 }
3937
3938 fn bob_joins_room_body(encrypted: bool) -> serde_json::Value {
3941 let mut state = Vec::new();
3942 if encrypted {
3943 state.push(serde_json::json!({
3944 "event_id": "$enc:example.org", "type": "m.room.encryption", "sender": "@alice:example.org",
3945 "origin_server_ts": 1, "state_key": "", "content": { "algorithm": "m.megolm.v1.aes-sha2" }
3946 }));
3947 }
3948 serde_json::json!({
3949 "next_batch": "s1",
3950 "rooms": { "join": { "!r:example.org": {
3951 "state": { "events": state },
3952 "timeline": { "events": [{
3953 "event_id": "$join:example.org", "type": "m.room.member", "sender": "@bob:example.org",
3954 "origin_server_ts": 2, "state_key": "@bob:example.org", "content": { "membership": "join" }
3955 }] }
3956 } } }
3957 })
3958 }
3959
3960 #[test]
3961 fn a_member_joining_an_encrypted_room_is_tracked_and_queried_right_away() {
3962 let mut core = open_fresh_core();
3963 let bob = UserId::parse("@bob:example.org").expect("valid user id");
3964 deliver_sync(&mut core, bob_joins_room_body(true));
3965 let tracked = core.store.tracked_users().expect("no error");
3966 assert!(tracked.iter().any(|(user_id, outdated)| *user_id == bob && *outdated), "bob is tracked with an outdated device list: {tracked:?}");
3967
3968 flush_and_ack(&mut core);
3969 let requests = core.releasable_requests(0);
3970 let query = requests.iter().find(|r| r.kind == OutgoingRequestKind::KeysQuery).expect("a /keys/query was enqueued for the new member");
3971 assert!(query.body.as_ref().is_some_and(|body| body["device_keys"].get(bob.as_str()).is_some()), "the query names bob: {:?}", query.body);
3972 }
3973
3974 #[test]
3975 fn a_member_joining_a_plaintext_room_is_not_tracked() {
3976 let mut core = open_fresh_core();
3977 deliver_sync(&mut core, bob_joins_room_body(false));
3978 assert!(core.store.tracked_users().expect("no error").is_empty());
3979 flush_and_ack(&mut core);
3980 assert!(core.releasable_requests(0).iter().all(|r| r.kind != OutgoingRequestKind::KeysQuery));
3981 }
3982
3983 #[test]
3984 fn dispatch_mark_read_and_set_typing_produce_outgoing_requests() {
3985 let mut core = open_fresh_core();
3986 flush_and_ack(&mut core);
3987 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3988 let event_id = EventId::parse("$1:example.org").expect("valid event id");
3989
3990 core.dispatch(MessengerCommand::MarkRead { room_id: room_id.clone(), event_id }, 0).expect("dispatch succeeds");
3991 core.dispatch(MessengerCommand::SetTyping { room_id: room_id.clone(), typing: true }, 0).expect("dispatch succeeds");
3992 flush_and_ack(&mut core);
3993
3994 let requests = core.releasable_requests(0);
3995 assert!(requests.iter().any(|r| r.kind == OutgoingRequestKind::ReadMarkers));
3996 assert!(requests.iter().any(|r| r.kind == OutgoingRequestKind::Typing));
3997 }
3998
3999 #[test]
4000 fn snapshot_reflects_the_latest_applied_sync() {
4001 let mut core = open_fresh_core();
4002 flush_and_ack(&mut core);
4003 core.releasable_requests(0);
4004 flush_and_ack(&mut core);
4005 let requests = core.releasable_requests(0);
4006 let sync_request = requests.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("sync enqueued");
4007
4008 let body = serde_json::json!({
4009 "next_batch": "s1",
4010 "rooms": {
4011 "join": {
4012 "!r:example.org": {
4013 "state": { "events": [] },
4014 "timeline": {
4015 "events": [
4016 {
4017 "event_id": "$1:example.org",
4018 "type": "m.room.message",
4019 "sender": "@alice:example.org",
4020 "origin_server_ts": 1,
4021 "content": { "msgtype": "m.text", "body": "hi" }
4022 }
4023 ]
4024 },
4025 "unread_notifications": { "notification_count": 1, "highlight_count": 0 }
4026 }
4027 }
4028 }
4029 })
4030 .to_string();
4031 let events = core.on_response(sync_request.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
4032
4033 assert!(events.contains(&MessengerEvent::RoomsChanged));
4034 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4035 assert!(events.contains(&MessengerEvent::TimelineChanged { room_id: room_id.clone() }));
4036 assert!(events.contains(&MessengerEvent::UnreadChanged { room_id: room_id.clone() }));
4037
4038 let timeline = core.timeline(&room_id).expect("timeline present");
4039 assert_eq!(timeline.items().len(), 1);
4040 assert_eq!(core.room_state(&room_id).expect("room present").unread_count(), 1);
4041 assert_eq!(core.change_counter(), events.len() as u64);
4042 assert!(core.events().is_empty());
4045 }
4046
4047 #[test]
4048 fn dispatch_set_account_data_updates_optimistically_and_sends_a_put() {
4049 let mut core = open_fresh_core();
4050 flush_and_ack(&mut core);
4051 let content = serde_json::json!({ "note": "keep" });
4052 core.dispatch(
4053 MessengerCommand::SetAccountData { event_type: "com.example.prefs".to_string(), content: content.clone() },
4054 0,
4055 )
4056 .expect("dispatch succeeds");
4057
4058 assert_eq!(
4059 core.global_account_data("com.example.prefs"),
4060 Some(&content),
4061 "SetAccountData updates the core's own cache before any round trip"
4062 );
4063
4064 flush_and_ack(&mut core);
4065 let requests = core.releasable_requests(0);
4066 let request = requests.iter().find(|r| r.kind == OutgoingRequestKind::AccountData).expect("an AccountData request was enqueued");
4067 assert_eq!(request.method, HttpMethod::Put);
4068 assert_eq!(request.path, "/_matrix/client/v3/user/%40alice%3Aexample.org/account_data/com.example.prefs");
4069 assert_eq!(request.body.as_ref(), Some(&content));
4070 }
4071
4072 #[test]
4073 fn dispatch_set_room_account_data_updates_optimistically_and_sends_a_put() {
4074 let mut core = open_fresh_core();
4075 flush_and_ack(&mut core);
4076 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4077 let content = serde_json::json!({ "flag": true });
4078 core.dispatch(
4079 MessengerCommand::SetRoomAccountData {
4080 room_id: room_id.clone(),
4081 event_type: "com.example.flag".to_string(),
4082 content: content.clone(),
4083 },
4084 0,
4085 )
4086 .expect("dispatch succeeds");
4087
4088 assert_eq!(
4089 core.room_account_data(&room_id, "com.example.flag"),
4090 Some(&content),
4091 "SetRoomAccountData updates the core's own cache before any round trip"
4092 );
4093
4094 flush_and_ack(&mut core);
4095 let requests = core.releasable_requests(0);
4096 let request =
4097 requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomAccountData).expect("a RoomAccountData request was enqueued");
4098 assert_eq!(request.method, HttpMethod::Put);
4099 assert_eq!(
4100 request.path,
4101 "/_matrix/client/v3/user/%40alice%3Aexample.org/rooms/%21r%3Aexample.org/account_data/com.example.flag"
4102 );
4103 assert_eq!(request.body.as_ref(), Some(&content));
4104 }
4105
4106 #[test]
4107 fn dispatch_search_public_rooms_sends_the_expected_request() {
4108 let mut core = open_fresh_core();
4109 flush_and_ack(&mut core);
4110 core.dispatch(MessengerCommand::SearchPublicRooms { term: "trading".to_string() }, 0).expect("dispatch succeeds");
4111 flush_and_ack(&mut core);
4112 let requests = core.releasable_requests(0);
4113 let request = requests.iter().find(|r| r.kind == OutgoingRequestKind::PublicRooms).expect("a PublicRooms request was enqueued");
4114 assert_eq!(request.method, HttpMethod::Post);
4115 assert_eq!(request.path, "/_matrix/client/v3/publicRooms");
4116 assert_eq!(request.body, Some(serde_json::json!({ "filter": { "generic_search_term": "trading" }, "limit": 50 })));
4117 }
4118
4119 #[test]
4120 fn dispatch_search_users_sends_the_expected_request() {
4121 let mut core = open_fresh_core();
4122 flush_and_ack(&mut core);
4123 core.dispatch(MessengerCommand::SearchUsers { term: "bob".to_string() }, 0).expect("dispatch succeeds");
4124 flush_and_ack(&mut core);
4125 let requests = core.releasable_requests(0);
4126 let request =
4127 requests.iter().find(|r| r.kind == OutgoingRequestKind::UserDirectorySearch).expect("a UserDirectorySearch request was enqueued");
4128 assert_eq!(request.method, HttpMethod::Post);
4129 assert_eq!(request.path, "/_matrix/client/v3/user_directory/search");
4130 assert_eq!(request.body, Some(serde_json::json!({ "search_term": "bob", "limit": 20 })));
4131 }
4132
4133 #[test]
4134 fn public_rooms_response_superseded_by_a_later_search_is_dropped() {
4135 let mut core = open_fresh_core();
4136 flush_and_ack(&mut core);
4137 core.dispatch(MessengerCommand::SearchPublicRooms { term: "first".to_string() }, 0).expect("dispatch succeeds");
4138 flush_and_ack(&mut core);
4139 let first_request = core
4140 .releasable_requests(0)
4141 .into_iter()
4142 .find(|r| r.kind == OutgoingRequestKind::PublicRooms)
4143 .expect("the first search's own request");
4144
4145 core.dispatch(MessengerCommand::SearchPublicRooms { term: "second".to_string() }, 0).expect("dispatch succeeds");
4148 flush_and_ack(&mut core);
4149 let second_request = core
4150 .releasable_requests(0)
4151 .into_iter()
4152 .find(|r| r.kind == OutgoingRequestKind::PublicRooms)
4153 .expect("the second search's own request");
4154
4155 let stale_body = serde_json::json!({ "chunk": [{ "room_id": "!stale:example.org", "num_joined_members": 1 }] });
4157 core.on_response(first_request.id, HttpResponseDescriptor { status: 200, body: serde_json::to_vec(&stale_body).expect("json") }, 0);
4158 assert!(core.public_rooms_result().is_empty(), "a response to a superseded search is dropped");
4159
4160 let fresh_body = serde_json::json!({ "chunk": [{ "room_id": "!fresh:example.org", "num_joined_members": 2 }] });
4162 core.on_response(second_request.id, HttpResponseDescriptor { status: 200, body: serde_json::to_vec(&fresh_body).expect("json") }, 0);
4163 assert_eq!(core.public_rooms_result().len(), 1);
4164 assert_eq!(core.public_rooms_result()[0].room_id, RoomId::parse("!fresh:example.org").expect("valid room id"));
4165 }
4166
4167 fn is_account_data_write(request: &OutgoingRequest) -> bool {
4168 matches!(request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData)
4169 }
4170
4171 #[test]
4172 fn pin_then_archive_back_to_back_keeps_both_tags() {
4173 let mut core = open_fresh_core();
4174 flush_and_ack(&mut core);
4175 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4176 for tag in ["m.favourite", "u.example.archived"] {
4177 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4178 .expect("dispatch succeeds");
4179 }
4180
4181 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4183 assert!(cached["tags"].get("m.favourite").is_some());
4184 assert!(cached["tags"].get("u.example.archived").is_some());
4185
4186 flush_and_ack(&mut core);
4187 let first_release = core.releasable_requests(0);
4188 let first_writes: Vec<&OutgoingRequest> = first_release.iter().filter(|r| is_account_data_write(r)).collect();
4189 assert_eq!(first_writes.len(), 1, "the second PUT is not released while the first is in flight");
4190 let first_body = first_writes[0].body.clone().expect("a body");
4191 assert!(first_body["tags"].get("m.favourite").is_some());
4192 assert!(first_body["tags"].get("u.example.archived").is_none(), "the first PUT carries only what existed when it was built");
4193
4194 core.on_response(first_writes[0].id.clone(), HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }, 0);
4195 flush_and_ack(&mut core);
4196 let second_release = core.releasable_requests(0);
4197 let second = second_release.iter().find(|r| is_account_data_write(r)).expect("the second PUT is released after the first completed");
4198 let second_body = second.body.clone().expect("a body");
4199 assert!(second_body["tags"].get("m.favourite").is_some(), "the later PUT still carries the earlier tag");
4200 assert!(second_body["tags"].get("u.example.archived").is_some());
4201 }
4202
4203 #[test]
4204 fn account_data_lane_is_fifo_single_flight() {
4205 let mut core = open_fresh_core();
4206 flush_and_ack(&mut core);
4207 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4208 core.dispatch(MessengerCommand::SetAccountData { event_type: "org.t.one".to_string(), content: serde_json::json!({}) }, 0)
4209 .expect("dispatch succeeds");
4210 core.dispatch(
4211 MessengerCommand::SetRoomAccountData {
4212 room_id: room_id.clone(),
4213 event_type: "org.t.two".to_string(),
4214 content: serde_json::json!({}),
4215 },
4216 0,
4217 )
4218 .expect("dispatch succeeds");
4219 core.dispatch(MessengerCommand::SetAccountData { event_type: "org.t.three".to_string(), content: serde_json::json!({}) }, 0)
4220 .expect("dispatch succeeds");
4221 core.dispatch(MessengerCommand::RemoveTag { room_id: room_id.clone(), tag: "m.favourite".to_string() }, 0)
4222 .expect("dispatch succeeds");
4223 flush_and_ack(&mut core);
4224
4225 let mut released_paths = Vec::new();
4226 for _ in 0..4 {
4227 let release = core.releasable_requests(0);
4228 let writes: Vec<&OutgoingRequest> = release.iter().filter(|r| is_account_data_write(r)).collect();
4229 assert_eq!(writes.len(), 1, "exactly one account-data write in flight at a time");
4230 released_paths.push(writes[0].path.clone());
4231 core.on_response(writes[0].id.clone(), HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }, 0);
4232 flush_and_ack(&mut core);
4233 }
4234 assert!(released_paths[0].ends_with("/account_data/org.t.one"));
4235 assert!(released_paths[1].ends_with("/account_data/org.t.two"));
4236 assert!(released_paths[2].ends_with("/account_data/org.t.three"));
4237 assert!(released_paths[3].ends_with("/account_data/m.tag"));
4238 assert!(core.releasable_requests(0).iter().all(|r| !is_account_data_write(r)), "the lane is drained");
4239 }
4240
4241 fn deliver_sync(core: &mut MessengerCore<InsecurePlainCodecForTests>, body: serde_json::Value) -> Vec<OutgoingRequest> {
4246 let mut others = Vec::new();
4247 for _ in 0..3 {
4248 flush_and_ack(core);
4249 let mut sync_request = None;
4250 for request in core.releasable_requests(0) {
4251 if request.kind == OutgoingRequestKind::Sync && sync_request.is_none() {
4252 sync_request = Some(request);
4253 } else {
4254 others.push(request);
4255 }
4256 }
4257 if let Some(sync_request) = sync_request {
4258 let body = serde_json::to_vec(&body).expect("valid JSON");
4259 core.on_response(sync_request.id, HttpResponseDescriptor { status: 200, body }, 0);
4260 return others;
4261 }
4262 }
4263 panic!("no sync request became releasable");
4264 }
4265
4266 fn tag_sync_body(room_id: &str, tags: serde_json::Value, next_batch: &str) -> serde_json::Value {
4267 let mut join = serde_json::Map::new();
4268 join.insert(
4269 room_id.to_string(),
4270 serde_json::json!({ "account_data": { "events": [{ "type": "m.tag", "content": { "tags": tags } }] } }),
4271 );
4272 serde_json::json!({ "next_batch": next_batch, "rooms": { "join": serde_json::Value::Object(join) } })
4273 }
4274
4275 fn ok_response() -> HttpResponseDescriptor {
4276 HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }
4277 }
4278
4279 #[test]
4280 fn sync_echo_of_an_older_tag_write_does_not_clobber_a_newer_optimistic_value() {
4281 let mut core = open_fresh_core();
4282 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4283 for tag in ["m.favourite", "u.example.archived"] {
4284 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4285 .expect("dispatch succeeds");
4286 }
4287
4288 let released = deliver_sync(&mut core, tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {} }), "s1"));
4290 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4291 assert!(cached["tags"].get("u.example.archived").is_some(), "the older echo must not clobber the newer optimistic value");
4292 assert!(cached["tags"].get("m.favourite").is_some());
4293 let key = (Some(room_id.clone()), "m.tag".to_string());
4294 assert_eq!(
4295 core.account_data_guard.get(&key).expect("writes are outstanding").server_value,
4296 Some(serde_json::json!({ "tags": { "m.favourite": {} } })),
4297 "the echo is remembered as the server's value"
4298 );
4299
4300 let first = released.iter().find(|r| r.kind == OutgoingRequestKind::RoomAccountData).expect("first write released");
4303 core.on_response(first.id.clone(), ok_response(), 0);
4304 flush_and_ack(&mut core);
4305 let second = core
4306 .releasable_requests(0)
4307 .into_iter()
4308 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4309 .expect("second write released after the first");
4310 core.on_response(second.id, ok_response(), 0);
4311 assert!(core.account_data_guard.is_empty());
4312 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4313 assert!(cached["tags"].get("m.favourite").is_some() && cached["tags"].get("u.example.archived").is_some());
4314
4315 deliver_sync(
4317 &mut core,
4318 tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {}, "u.example.archived": {} }), "s2"),
4319 );
4320 let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4321 assert!(cached["tags"].get("u.example.archived").is_some());
4322 }
4323
4324 #[test]
4325 fn terminal_account_data_failure_restores_the_server_value() {
4326 let mut core = open_fresh_core();
4327 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4328 deliver_sync(&mut core, tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {} }), "s1"));
4329
4330 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: "u.example.archived".to_string(), order: None }, 0)
4331 .expect("dispatch succeeds");
4332 assert!(core.room_account_data(&room_id, "m.tag").expect("cached")["tags"].get("u.example.archived").is_some());
4333 flush_and_ack(&mut core);
4334 let write = core
4335 .releasable_requests(0)
4336 .into_iter()
4337 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4338 .expect("the write is released");
4339
4340 let counter_before = core.change_counter();
4341 let forbidden =
4342 HttpResponseDescriptor { status: 403, body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec() };
4343 let events = core.on_response(write.id, forbidden, 0);
4344 assert!(core.account_data_guard.is_empty());
4345 let restored = core.room_account_data(&room_id, "m.tag").expect("the server's value is back");
4346 assert!(restored["tags"].get("u.example.archived").is_none(), "the failed write's optimistic value is gone");
4347 assert!(restored["tags"].get("m.favourite").is_some());
4348 assert!(events.contains(&MessengerEvent::RoomsChanged));
4349 assert!(core.change_counter() > counter_before, "the UI is told to re-render");
4350
4351 for tag in ["u.a", "u.b"] {
4355 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4356 .expect("dispatch succeeds");
4357 }
4358 flush_and_ack(&mut core);
4359 let first = core
4360 .releasable_requests(0)
4361 .into_iter()
4362 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4363 .expect("first write released");
4364 let forbidden =
4365 HttpResponseDescriptor { status: 403, body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec() };
4366 core.on_response(first.id, forbidden, 0);
4367 let cached = core.room_account_data(&room_id, "m.tag").expect("cached");
4368 assert!(cached["tags"].get("u.a").is_some() && cached["tags"].get("u.b").is_some(), "cache untouched while a write is pending");
4369 flush_and_ack(&mut core);
4370 let second = core
4371 .releasable_requests(0)
4372 .into_iter()
4373 .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4374 .expect("second write released");
4375 core.on_response(second.id, ok_response(), 0);
4376 let cached = core.room_account_data(&room_id, "m.tag").expect("cached");
4377 assert!(cached["tags"].get("u.b").is_some(), "the last write succeeded: its optimistic value stands");
4378 }
4379
4380 #[test]
4381 fn pending_account_data_writes_rebuild_their_guard_after_a_restart() {
4382 let mut core = open_fresh_core();
4383 let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4384 core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: "m.favourite".to_string(), order: None }, 0)
4385 .expect("dispatch succeeds");
4386 core.dispatch(
4387 MessengerCommand::SetAccountData { event_type: "org.t.one".to_string(), content: serde_json::json!({ "n": 1 }) },
4388 0,
4389 )
4390 .expect("dispatch succeeds");
4391
4392 let mut records = Vec::new();
4394 while let Some(batch) = core.take_flush_batch() {
4395 records.extend(batch.records.clone());
4396 core.ack_flush(batch.id);
4397 }
4398 let reopened = MessengerCore::open(
4399 records,
4400 InsecurePlainCodecForTests,
4401 test_config(),
4402 CoreSecrets::default(),
4403 0,
4404 Box::new(FixedJitter(0.0)),
4405 )
4406 .expect("reopen succeeds");
4407
4408 assert_eq!(reopened.account_data_guard.len(), 2, "one guard per key with a pending write");
4409 assert!(reopened.account_data_guard.values().all(|guard| guard.pending == 1 && guard.server_value.is_none()));
4410 assert_eq!(reopened.global_account_data("org.t.one"), Some(&serde_json::json!({ "n": 1 })));
4411 assert!(reopened.room_account_data(&room_id, "m.tag").expect("cached")["tags"].get("m.favourite").is_some());
4412 }
4413
4414 type TestCore = MessengerCore<InsecurePlainCodecForTests>;
4419
4420 fn room(id: &str) -> RoomId {
4421 RoomId::parse(id).expect("valid room id")
4422 }
4423
4424 fn event(id: &str) -> EventId {
4425 EventId::parse(id).expect("valid event id")
4426 }
4427
4428 fn add_room(core: &mut TestCore, room_id: &RoomId, encrypted: bool, name: Option<&str>) {
4431 let mut state = RoomState::new();
4432 if encrypted {
4433 state.encryption = Some(crate::wire::events::RoomEncryptionContent {
4434 algorithm: "m.megolm.v1.aes-sha2".to_string(),
4435 rotation_period_ms: 604_800_000,
4436 rotation_period_msgs: 100,
4437 });
4438 }
4439 state.name = name.map(str::to_string);
4440 core.rooms.insert(room_id.clone(), state);
4441 core.timelines.entry(room_id.clone()).or_default();
4442 }
4443
4444 #[test]
4447 fn set_events_default_opens_a_channel_for_every_member() {
4448 let mut core = open_fresh_core();
4449 let chan = room("!chan:example.org");
4450 add_room(&mut core, &chan, false, Some("Talk"));
4451 mark_as_channel(&mut core, &chan);
4452 core.dispatch(MessengerCommand::SetEventsDefault { room_id: chan.clone(), level: 0 }, 1_000).expect("dispatch");
4453 core.releasable_requests(1_000);
4454 flush_and_ack(&mut core);
4455 let sent = core.releasable_requests(1_000);
4456 let put = sent.iter().find(|r| r.kind == OutgoingRequestKind::SetState).expect("a state PUT");
4457 assert!(put.path.ends_with("/state/m.room.power_levels/"), "{}", put.path);
4458 let body = put.body.clone().expect("body");
4459 assert_eq!(body["events_default"], 0);
4460 assert_eq!(body["users_default"], 0);
4461 }
4462
4463 #[test]
4464 fn set_events_default_refuses_an_unknown_room() {
4465 let mut core = open_fresh_core();
4466 let err = core.dispatch(MessengerCommand::SetEventsDefault { room_id: room("!nope:example.org"), level: 0 }, 1_000);
4467 assert!(err.is_err());
4468 }
4469
4470 fn mark_as_channel(core: &mut TestCore, room_id: &RoomId) {
4471 let Some(state) = core.rooms.get_mut(room_id) else { return };
4472 state
4473 .apply_state_event(&state_event_raw(
4474 "m.room.join_rules",
4475 "",
4476 "@alice:example.org",
4477 serde_json::json!({ "join_rule": "public" }),
4478 ))
4479 .expect("join_rules");
4480 state
4481 .apply_state_event(&state_event_raw(
4482 "m.room.power_levels",
4483 "",
4484 "@alice:example.org",
4485 serde_json::json!({ "events_default": 50, "users_default": 0 }),
4486 ))
4487 .expect("power_levels");
4488 }
4489
4490 fn state_event_raw(event_type: &str, state_key: &str, sender: &str, content: serde_json::Value) -> RawEvent {
4491 serde_json::from_value(serde_json::json!({
4492 "event_id": format!("${event_type}:example.org"),
4493 "type": event_type,
4494 "sender": sender,
4495 "origin_server_ts": 1,
4496 "state_key": state_key,
4497 "content": content,
4498 }))
4499 .expect("valid raw event")
4500 }
4501
4502 fn raw_event(event_id: &str, event_type: &str, sender: &str, content: serde_json::Value) -> RawEvent {
4503 serde_json::from_value(serde_json::json!({
4504 "event_id": event_id,
4505 "type": event_type,
4506 "sender": sender,
4507 "origin_server_ts": 1,
4508 "content": content,
4509 }))
4510 .expect("valid raw event")
4511 }
4512
4513 fn seed_plain(core: &mut TestCore, room_id: &RoomId, event_id: &str, sender: &str, event_type: &str, content: serde_json::Value) {
4515 let raw = raw_event(event_id, event_type, sender, content);
4516 core.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(&[raw], false, None);
4517 }
4518
4519 fn seed_sealed(core: &mut TestCore, room_id: &RoomId, event_id: &str, sender: &str) {
4521 let content = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2", "ciphertext": "AAAA", "session_id": "s1" });
4522 seed_plain(core, room_id, event_id, sender, "m.room.encrypted", content);
4523 }
4524
4525 fn seed_decrypted(
4528 core: &mut TestCore,
4529 room_id: &RoomId,
4530 event_id_str: &str,
4531 sender: &str,
4532 event_type: &str,
4533 content: serde_json::Value,
4534 ) {
4535 seed_sealed(core, room_id, event_id_str, sender);
4536 if let Some(timeline) = core.timelines.get_mut(room_id) {
4537 timeline.set_decrypted_event(&event(event_id_str), event_type, &content);
4538 }
4539 }
4540
4541 fn sent_room_event(core: &mut TestCore) -> (String, serde_json::Value) {
4545 flush_and_ack(core);
4546 let request = core
4547 .releasable_requests(0)
4548 .into_iter()
4549 .find(|request| request.kind == OutgoingRequestKind::RoomSend)
4550 .expect("a room send is releasable");
4551 let confirmed = format!("$sent{}:example.org", core.counters.next_request_id);
4552 let body = serde_json::json!({ "event_id": confirmed }).to_string().into_bytes();
4553 core.on_response(request.id.clone(), HttpResponseDescriptor { status: 200, body }, 0);
4554 (request.path, request.body.expect("a room send has a body"))
4555 }
4556
4557 fn forward(core: &mut TestCore, from: &RoomId, event_id: &str, to: &RoomId) -> Result<(), MessengerError> {
4558 core.dispatch(
4559 MessengerCommand::Forward { from_room: from.clone(), event_id: event(event_id), to_room: to.clone(), txn_id: None },
4560 0,
4561 )
4562 }
4563
4564 fn last_item(core: &TestCore, room_id: &RoomId) -> crate::room::timeline::TimelineItem {
4565 core.timeline(room_id).and_then(|timeline| timeline.items().last()).expect("the room has an item").clone()
4566 }
4567
4568 #[test]
4569 fn forward_from_encrypted_room_carries_only_the_forwarded_flag() {
4570 let mut core = open_fresh_core();
4571 let dm = room("!dm:example.org");
4572 let target = room("!target:example.org");
4573 add_room(&mut core, &dm, true, Some("Secret DM name"));
4574 add_room(&mut core, &target, false, None);
4575 seed_decrypted(
4576 &mut core,
4577 &dm,
4578 "$src:example.org",
4579 "@bob:example.org",
4580 "m.room.message",
4581 serde_json::json!({
4582 "msgtype": "m.text",
4583 "body": "quarterly numbers",
4584 "m.relates_to": { "m.in_reply_to": { "event_id": "$parent:example.org" } },
4585 "m.mentions": { "user_ids": ["@carol:example.org"] },
4586 }),
4587 );
4588
4589 forward(&mut core, &dm, "$src:example.org", &target).expect("a decrypted message is forwardable");
4590
4591 let (path, body) = sent_room_event(&mut core);
4592 assert!(path.contains("/send/m.room.message/"), "the event type is kept: {path}");
4593 assert_eq!(
4594 body,
4595 serde_json::json!({ "msgtype": "m.text", "body": "quarterly numbers", "forwarded": true }),
4596 "the text plus the bare flag; the reply pointer and the mentions are stripped"
4597 );
4598 let wire = body.to_string();
4599 for leaked in ["bob", "carol", "!dm", "Secret DM name", "$parent"] {
4600 assert!(!wire.contains(leaked), "{leaked:?} must not travel with a forward out of an encrypted room: {wire}");
4601 }
4602 let echo = last_item(&core, &target);
4603 assert_eq!(echo.forwarded, Some(Forwarded::Hidden), "the forwarder's own echo shows the same marker");
4604 }
4605
4606 #[test]
4607 fn forward_from_public_channel_carries_room_id_and_name_no_sender() {
4608 let mut core = open_fresh_core();
4609 let channel = room("!chan:example.org");
4610 let target = room("!target:example.org");
4611 add_room(&mut core, &channel, true, Some("Announcements"));
4612 mark_as_channel(&mut core, &channel);
4613 add_room(&mut core, &target, false, None);
4614 seed_plain(
4615 &mut core,
4616 &channel,
4617 "$post:example.org",
4618 "@dave:example.org",
4619 "m.room.message",
4620 serde_json::json!({
4621 "msgtype": "m.text",
4622 "body": "BTC breaks out",
4623 "m.mentions": { "room": true },
4624 "m.relates_to": { "m.in_reply_to": { "event_id": "$older:example.org" } },
4625 }),
4626 );
4627
4628 forward(&mut core, &channel, "$post:example.org", &target).expect("a channel post is forwardable");
4629
4630 let (_, body) = sent_room_event(&mut core);
4631 assert_eq!(
4632 body,
4633 serde_json::json!({
4634 "msgtype": "m.text",
4635 "body": "BTC breaks out",
4636 "forwarded_from": { "room_id": "!chan:example.org", "room_name": "Announcements" },
4637 })
4638 );
4639 assert!(!body.to_string().contains("dave"), "the original sender never travels");
4640 assert_eq!(
4641 last_item(&core, &target).forwarded,
4642 Some(Forwarded::Channel { room_id: channel.clone(), room_name: "Announcements".to_string() })
4643 );
4644 }
4645
4646 #[test]
4647 fn forward_from_an_unnamed_room_never_lists_member_names() {
4648 let mut core = open_fresh_core();
4649 let channel = room("!chan:example.org");
4650 let target = room("!target:example.org");
4651 add_room(&mut core, &channel, true, None);
4652 mark_as_channel(&mut core, &channel);
4653 if let Some(state) = core.rooms.get_mut(&channel) {
4654 state.members.insert(
4655 UserId::parse("@erin:example.org").expect("valid user id"),
4656 crate::room::state::MemberState {
4657 membership: Membership::Join,
4658 displayname: Some("Erin Private".to_string()),
4659 is_direct: false,
4660 invited_by: None,
4661 },
4662 );
4663 }
4664 add_room(&mut core, &target, false, None);
4665 seed_plain(&mut core, &channel, "$post:example.org", "@erin:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "hi" }));
4666
4667 forward(&mut core, &channel, "$post:example.org", &target).expect("forwardable");
4668
4669 let (_, body) = sent_room_event(&mut core);
4670 let name = body["forwarded_from"]["room_name"].as_str().expect("a name is sent");
4671 assert!(!name.contains("Erin"), "an unnamed room is attributed by a generic label, not by its members: {name}");
4672 }
4673
4674 #[test]
4675 fn forward_replaces_the_sources_own_forward_marker() {
4676 let mut core = open_fresh_core();
4677 let dm = room("!dm:example.org");
4678 let target = room("!target:example.org");
4679 add_room(&mut core, &dm, true, None);
4680 add_room(&mut core, &target, false, None);
4681 seed_decrypted(
4682 &mut core,
4683 &dm,
4684 "$src:example.org",
4685 "@bob:example.org",
4686 "m.room.message",
4687 serde_json::json!({
4688 "msgtype": "m.text",
4689 "body": "second hop",
4690 "forwarded_from": { "room_id": "!older:example.org", "room_name": "Older channel" },
4691 }),
4692 );
4693
4694 forward(&mut core, &dm, "$src:example.org", &target).expect("forwardable");
4695
4696 let (_, body) = sent_room_event(&mut core);
4697 assert_eq!(body, serde_json::json!({ "msgtype": "m.text", "body": "second hop", "forwarded": true }));
4698 }
4699
4700 #[test]
4701 fn forward_of_edited_item_sends_the_latest_text() {
4702 let mut core = open_fresh_core();
4703 let channel = room("!chan:example.org");
4704 let dm = room("!dm:example.org");
4705 let target = room("!target:example.org");
4706 add_room(&mut core, &channel, false, Some("Announcements"));
4707 add_room(&mut core, &dm, true, None);
4708 add_room(&mut core, &target, false, None);
4709
4710 seed_plain(&mut core, &channel, "$orig:example.org", "@dave:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "typo text" }));
4712 seed_plain(
4713 &mut core,
4714 &channel,
4715 "$edit:example.org",
4716 "@dave:example.org",
4717 "m.room.message",
4718 serde_json::json!({
4719 "msgtype": "m.text",
4720 "body": "* fixed text",
4721 "m.new_content": { "msgtype": "m.text", "body": "fixed text" },
4722 "m.relates_to": { "rel_type": "m.replace", "event_id": "$orig:example.org" },
4723 }),
4724 );
4725 forward(&mut core, &channel, "$orig:example.org", &target).expect("forwardable");
4726 let (_, body) = sent_room_event(&mut core);
4727 assert_eq!(body["body"], "fixed text", "the latest edit's text, not the original and not the `* ` fallback");
4728 assert!(body.get("m.new_content").is_none() && body.get("m.relates_to").is_none());
4729
4730 seed_decrypted(&mut core, &dm, "$secret:example.org", "@bob:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "typo secret" }));
4732 seed_sealed(&mut core, &dm, "$secret-edit:example.org", "@bob:example.org");
4733 let applied = core.timelines.get_mut(&dm).expect("timeline").apply_decrypted_relation(
4734 &event("$secret-edit:example.org"),
4735 UserId::parse("@bob:example.org").expect("valid user id"),
4736 2,
4737 RelatesTo::Replace { event_id: event("$secret:example.org") },
4738 Some(serde_json::json!({ "msgtype": "m.text", "body": "fixed secret" })),
4739 );
4740 assert!(applied, "the decrypted edit folds onto its target");
4741 forward(&mut core, &dm, "$secret:example.org", &target).expect("forwardable");
4742 let (_, body) = sent_room_event(&mut core);
4743 assert_eq!(body, serde_json::json!({ "msgtype": "m.text", "body": "fixed secret", "forwarded": true }));
4744 }
4745
4746 #[test]
4747 fn forward_of_unknown_msgtype_keeps_its_fields() {
4748 let mut core = open_fresh_core();
4749 let dm = room("!dm:example.org");
4750 let target = room("!target:example.org");
4751 add_room(&mut core, &dm, true, None);
4752 add_room(&mut core, &target, false, None);
4753 let card = serde_json::json!({
4754 "msgtype": "com.example.card",
4755 "body": "opaque",
4756 "note": "kept raw",
4757 });
4758 seed_decrypted(&mut core, &dm, "$card:example.org", "@bob:example.org", "m.room.message", card.clone());
4759
4760 forward(&mut core, &dm, "$card:example.org", &target).expect("an unrecognized msgtype is still an m.room.message");
4761
4762 let (path, body) = sent_room_event(&mut core);
4763 assert!(path.contains("/send/m.room.message/"));
4764 let mut expected = card;
4765 expected["forwarded"] = serde_json::Value::Bool(true);
4766 assert_eq!(body, expected, "unrecognized msgtype fields pass through plus the marker");
4767 assert!(matches!(last_item(&core, &target).content, ItemContent::Unknown), "the echo does not invent a card type");
4768 }
4769
4770 #[test]
4771 fn forward_refuses_redacted_sealed_unknown_and_missing() {
4772 let mut core = open_fresh_core();
4773 let dm = room("!dm:example.org");
4774 let target = room("!target:example.org");
4775 add_room(&mut core, &dm, true, None);
4776 add_room(&mut core, &target, false, None);
4777
4778 seed_decrypted(&mut core, &dm, "$redacted:example.org", "@bob:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "gone" }));
4779 core.timelines.get_mut(&dm).expect("timeline").apply_redaction(&event("$redacted:example.org"));
4780 seed_sealed(&mut core, &dm, "$sealed:example.org", "@bob:example.org");
4781 seed_sealed(&mut core, &dm, "$utd:example.org", "@bob:example.org");
4782 core.timelines
4783 .get_mut(&dm)
4784 .expect("timeline")
4785 .set_decrypted(&event("$utd:example.org"), ItemContent::Undecryptable { reason: "MissingSession".to_string() });
4786 seed_plain(&mut core, &dm, "$call:example.org", "@bob:example.org", "m.call.invite", serde_json::json!({ "call_id": "c1" }));
4787
4788 for id in ["$redacted:example.org", "$sealed:example.org", "$utd:example.org", "$call:example.org", "$missing:example.org"] {
4789 match forward(&mut core, &dm, id, &target) {
4790 Err(MessengerError::IntentRefused(reason)) => {
4791 assert!(reason.contains(id), "the refusal names the event {id}: {reason}");
4792 }
4793 other => panic!("forwarding {id} must be refused, got {other:?}"),
4794 }
4795 }
4796 match forward(&mut core, &room("!nowhere:example.org"), "$redacted:example.org", &target) {
4797 Err(MessengerError::IntentRefused(reason)) => assert!(reason.contains("$redacted:example.org"), "{reason}"),
4798 other => panic!("an unknown source room means a missing event, got {other:?}"),
4799 }
4800 match forward(&mut core, &dm, "$sealed:example.org", &room("!nowhere:example.org")) {
4801 Err(MessengerError::IntentRefused(reason)) => assert!(reason.contains("unknown"), "{reason}"),
4802 other => panic!("an unknown target room is refused, got {other:?}"),
4803 }
4804
4805 assert!(core.send_state.is_empty(), "a refused forward queues nothing");
4806 assert!(core.timeline(&target).is_some_and(|timeline| timeline.items().is_empty()), "and leaves no echo");
4807 }
4808}