From c251f16292891dbc5d8d624ce561cb07e7a598a4 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 29 Feb 2024 12:16:58 +0000 Subject: [PATCH 1/8] ffi: Update the power levels of multiple users as once. --- bindings/matrix-sdk-ffi/src/room.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index e700f99c9..bbad67fa8 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -581,15 +581,21 @@ impl Room { Ok(()) } - pub async fn update_power_level_for_user( + pub async fn update_power_levels_for_users( &self, - user_id: String, - power_level: i64, + updates: Vec, ) -> Result<(), ClientError> { - let user_id = UserId::parse(&user_id)?; - let power_level = Int::new(power_level).context("Invalid power level")?; + let parse_result: Result> = updates + .iter() + .map(|upl| -> Result<(&UserId, Int)> { + let user_id: &UserId = <&UserId>::try_from(upl.user_id.as_str())?; + let pl = Int::new(upl.power_level).context("Invalid power level")?; + Ok((user_id, pl)) + }) + .collect(); + self.inner - .update_power_levels(vec![(&user_id, power_level)]) + .update_power_levels(parse_result?) .await .map_err(|e| ClientError::Generic { msg: e.to_string() })?; Ok(()) @@ -638,6 +644,15 @@ impl RoomMembersIterator { } } +/// An update for a particular user's power level within the room. +#[derive(uniffi::Record)] +pub struct UserPowerLevelUpdate { + /// The user ID of the user to update. + user_id: String, + /// The power level to assign to the user. + power_level: i64, +} + impl TryFrom for RumaAvatarImageInfo { type Error = MediaInfoError; From 370f4735f7b98d05e69a42411aa2956232825bba Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 29 Feb 2024 14:56:16 +0100 Subject: [PATCH 2/8] ffi: simplify the declaration type --- bindings/matrix-sdk-ffi/src/room.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index bbad67fa8..74600e757 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -585,17 +585,17 @@ impl Room { &self, updates: Vec, ) -> Result<(), ClientError> { - let parse_result: Result> = updates + let updates = updates .iter() - .map(|upl| -> Result<(&UserId, Int)> { - let user_id: &UserId = <&UserId>::try_from(upl.user_id.as_str())?; - let pl = Int::new(upl.power_level).context("Invalid power level")?; - Ok((user_id, pl)) + .map(|update| { + let user_id: &UserId = update.user_id.as_str().try_into()?; + let power_level = Int::new(update.power_level).context("Invalid power level")?; + Ok((user_id, power_level)) }) - .collect(); + .collect::>>()?; self.inner - .update_power_levels(parse_result?) + .update_power_levels(updates) .await .map_err(|e| ClientError::Generic { msg: e.to_string() })?; Ok(()) From 74727e5f84effb9b7392de95e031f6ad05e16df4 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 29 Feb 2024 11:46:28 +0100 Subject: [PATCH 3/8] ci: update codecov action to v4 --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5d16f25d5..78531e7f7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -111,7 +111,7 @@ jobs: SLIDING_SYNC_PROXY_URL: "http://localhost:8118" - name: Upload to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: # Work around frequent upload errors, for runs inside the main repo (not PRs from forks). # Otherwise not required for public repos. From e4be216731dad6b69f33fd13dc8820976f9b48e0 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Sat, 2 Mar 2024 19:10:15 +0200 Subject: [PATCH 4/8] feat: add support for `m.call.invite` events in the timeline and as a last room message --- .../matrix-sdk-ffi/src/timeline/content.rs | 2 + crates/matrix-sdk-base/src/client.rs | 13 +++--- crates/matrix-sdk-base/src/latest_event.rs | 41 ++++++++++++++++++- crates/matrix-sdk-base/src/sliding_sync.rs | 6 ++- .../src/timeline/event_handler.rs | 4 ++ .../src/timeline/event_item/content/mod.rs | 18 ++++++++ .../matrix-sdk-ui/src/timeline/inner/mod.rs | 1 + crates/matrix-sdk-ui/src/timeline/mod.rs | 3 ++ 8 files changed, 79 insertions(+), 9 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index 3b7f6fcf9..51e71e6b9 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -41,6 +41,7 @@ impl TimelineItemContent { } } Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()), + Content::CallInvite => TimelineItemContentKind::CallInvite, Content::UnableToDecrypt(msg) => { TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) } } @@ -113,6 +114,7 @@ pub enum TimelineItemContentKind { end_time: Option, has_been_edited: bool, }, + CallInvite, UnableToDecrypt { msg: EncryptedMessage, }, diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 5caca6225..1d5a7f676 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -720,11 +720,14 @@ impl BaseClient { // We found an event we can decrypt if let Ok(any_sync_event) = decrypted.event.deserialize() { // We can deserialize it to find its type - if let PossibleLatestEvent::YesRoomMessage(_) = - is_suitable_for_latest_event(&any_sync_event) - { - // The event is the right type for us to use as latest_event - return Some((Box::new(LatestEvent::new(decrypted)), i)); + match is_suitable_for_latest_event(&any_sync_event) { + PossibleLatestEvent::YesRoomMessage(_) + | PossibleLatestEvent::YesPoll(_) + | PossibleLatestEvent::YesCallInvite(_) => { + // The event is the right type for us to use as latest_event + return Some((Box::new(LatestEvent::new(decrypted)), i)); + } + _ => (), } } } diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 0dd0af267..8d1e41c43 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -9,7 +9,10 @@ use ruma::events::{ poll::unstable_start::SyncUnstablePollStartEvent, room::message::SyncRoomMessageEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, }; -use ruma::{events::relation::RelationType, MxcUri, OwnedEventId}; +use ruma::{ + events::{call::invite::SyncCallInviteEvent, relation::RelationType}, + MxcUri, OwnedEventId, +}; use serde::{Deserialize, Serialize}; use crate::MinimalRoomMemberEvent; @@ -25,6 +28,10 @@ pub enum PossibleLatestEvent<'a> { YesRoomMessage(&'a SyncRoomMessageEvent), /// This message is suitable - it is a poll YesPoll(&'a SyncUnstablePollStartEvent), + + /// This message is suitable - it is a call invite + YesCallInvite(&'a SyncCallInviteEvent), + // Later: YesState(), // Later: YesReaction(), /// Not suitable - it's a state event @@ -67,6 +74,10 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat PossibleLatestEvent::YesPoll(poll) } + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => { + PossibleLatestEvent::YesCallInvite(invite) + } + // Encrypted events are not suitable AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => { PossibleLatestEvent::NoEncrypted @@ -243,6 +254,10 @@ mod tests { use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use ruma::{ events::{ + call::{ + invite::{CallInviteEventContent, SyncCallInviteEvent}, + SessionDescription, + }, poll::unstable_start::{ NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, UnstablePollAnswer, UnstablePollStartContentBlock, @@ -268,7 +283,7 @@ mod tests { }, owned_event_id, owned_mxc_uri, owned_user_id, serde::Raw, - MilliSecondsSinceUnixEpoch, UInt, + MilliSecondsSinceUnixEpoch, UInt, VoipVersionId, }; use serde_json::json; @@ -321,6 +336,28 @@ mod tests { assert_eq!(m.content.poll_start().question.text, "do you like rust?"); } + #[test] + fn call_invites_are_suitable() { + let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite( + SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent { + content: CallInviteEventContent::new( + "call_id".into(), + UInt::new(123).unwrap(), + SessionDescription::new("".into(), "".into()), + VoipVersionId::V1, + ), + event_id: owned_event_id!("$1"), + sender: owned_user_id!("@a:b.c"), + origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), + unsigned: MessageLikeUnsigned::new(), + }), + )); + assert_let!( + PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) = + is_suitable_for_latest_event(&event) + ); + } + #[test] fn different_types_of_messagelike_are_unsuitable() { let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker( diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index ea4ee5b2e..f66a01a80 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -585,8 +585,10 @@ async fn cache_latest_events( for event in events.iter().rev() { if let Ok(timeline_event) = event.event.deserialize() { match is_suitable_for_latest_event(&timeline_event) { - PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) => { - // m.room.message or m.poll.start - we found one! Store it. + PossibleLatestEvent::YesRoomMessage(_) + | PossibleLatestEvent::YesPoll(_) + | PossibleLatestEvent::YesCallInvite(_) => { + // We found a suitable latest event. Store it. // In order to make the latest event fast to read, we want to keep the // associated sender in cache. This is a best-effort to gather enough diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 247574ac6..c49cdc320 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -309,6 +309,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { ) => self.handle_poll_start(c, should_add), AnyMessageLikeEventContent::UnstablePollResponse(c) => self.handle_poll_response(c), AnyMessageLikeEventContent::UnstablePollEnd(c) => self.handle_poll_end(c), + AnyMessageLikeEventContent::CallInvite(_) => { + self.add(should_add, TimelineItemContent::CallInvite); + } + // TODO _ => { debug!( diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 90de9430e..06e655209 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -19,6 +19,7 @@ use imbl::Vector; use matrix_sdk_base::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}; use ruma::{ events::{ + call::invite::SyncCallInviteEvent, policy::rule::{ room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent, user::PolicyRuleUserEventContent, @@ -106,6 +107,9 @@ pub enum TimelineItemContent { /// An `m.poll.start` event. Poll(PollState), + + /// An `m.call.invite` event + CallInvite, } impl TimelineItemContent { @@ -122,6 +126,9 @@ impl TimelineItemContent { PossibleLatestEvent::YesPoll(poll) => { Some(Self::from_suitable_latest_poll_event_content(poll)) } + PossibleLatestEvent::YesCallInvite(call_invite) => { + Some(Self::from_suitable_latest_call_invite_content(call_invite)) + } PossibleLatestEvent::NoUnsupportedEventType => { // TODO: when we support state events in message previews, this will need change warn!("Found a state event cached as latest_event! ID={}", event.event_id()); @@ -189,6 +196,15 @@ impl TimelineItemContent { } } + fn from_suitable_latest_call_invite_content( + event: &SyncCallInviteEvent, + ) -> TimelineItemContent { + match event { + SyncCallInviteEvent::Original(_) => TimelineItemContent::CallInvite, + SyncCallInviteEvent::Redacted(_) => TimelineItemContent::RedactedMessage, + } + } + /// If `self` is of the [`Message`][Self::Message] variant, return the inner /// [`Message`]. pub fn as_message(&self) -> Option<&Message> { @@ -228,6 +244,7 @@ impl TimelineItemContent { TimelineItemContent::FailedToParseMessageLike { .. } | TimelineItemContent::FailedToParseState { .. } => "an event that couldn't be parsed", TimelineItemContent::Poll(_) => "a poll", + TimelineItemContent::CallInvite => "a call invite", } } @@ -306,6 +323,7 @@ impl TimelineItemContent { | Self::RedactedMessage | Self::Sticker(_) | Self::Poll(_) + | Self::CallInvite | Self::UnableToDecrypt(_) => Self::RedactedMessage, Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(room_version)), Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()), diff --git a/crates/matrix-sdk-ui/src/timeline/inner/mod.rs b/crates/matrix-sdk-ui/src/timeline/inner/mod.rs index 3f33bb028..8d42e9c87 100644 --- a/crates/matrix-sdk-ui/src/timeline/inner/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/inner/mod.rs @@ -193,6 +193,7 @@ pub fn default_event_filter(event: &AnySyncTimelineEvent, room_version: &RoomVer | AnyMessageLikeEventContent::UnstablePollStart( UnstablePollStartEventContent::New(_), ) + | AnyMessageLikeEventContent::CallInvite(_) | AnyMessageLikeEventContent::RoomEncrypted(_) => true, _ => false, diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index c4c3ce872..f1a50d879 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -598,6 +598,9 @@ impl Timeline { TimelineItemContent::Poll(poll_state) => AnyMessageLikeEventContent::UnstablePollStart( UnstablePollStartEventContent::New(poll_state.into()), ), + TimelineItemContent::CallInvite => { + error_return!("Retrying call events is not currently supported"); + } }; debug!("Retrying failed local echo"); From 8d2e790bca1089e0d8329dc5ae6b3a8cb36b8208 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 29 Feb 2024 12:10:49 +0100 Subject: [PATCH 5/8] event cache: add a `Room::event_cache()` method This keeps the `RoomNotFound` error, which could still happen in theory if the `EventCache` is being misused internally. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 2 +- crates/matrix-sdk/src/event_cache/mod.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 15 +++++- .../tests/integration/event_cache.rs | 49 ++++++++----------- 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 697064a69..54edde8d3 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -127,7 +127,7 @@ impl TimelineBuilder { // Subscribe the event cache to sync responses, in case we hadn't done it yet. event_cache.subscribe()?; - let (room_event_cache, event_cache_drop) = event_cache.for_room(room.room_id()).await?; + let (room_event_cache, event_cache_drop) = room.event_cache().await?; let (events, mut event_subscriber) = room_event_cache.subscribe().await?; let has_events = !events.is_empty(); diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 35ffb4866..a5226c2b6 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -206,7 +206,7 @@ impl EventCache { } /// Return a room-specific view over the [`EventCache`]. - pub async fn for_room( + pub(crate) async fn for_room( &self, room_id: &RoomId, ) -> Result<(RoomEventCache, Arc)> { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index b4d0014d4..f071ad9b9 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1,6 +1,6 @@ //! High-level room API -use std::{borrow::Borrow, collections::BTreeMap, ops::Deref, time::Duration}; +use std::{borrow::Borrow, collections::BTreeMap, ops::Deref, sync::Arc, time::Duration}; use eyeball::SharedObservable; use futures_core::Stream; @@ -79,10 +79,13 @@ pub use self::{ member::{RoomMember, RoomMemberRole}, messages::{Messages, MessagesOptions}, }; +#[cfg(doc)] +use crate::event_cache::EventCache; use crate::{ attachment::AttachmentConfig, config::RequestConfig, error::WrongRoomState, + event_cache::{self, EventCacheDropHandles, RoomEventCache}, event_handler::{EventHandler, EventHandlerDropGuard, EventHandlerHandle, SyncEvent}, media::{MediaFormat, MediaRequest}, notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode}, @@ -2570,6 +2573,16 @@ impl Room { self.client.send(request, None).await?; Ok(()) } + + /// Returns the [`RoomEventCache`] associated to this room, assuming the + /// global [`EventCache`] has been enabled for subscription. + pub async fn event_cache( + &self, + ) -> event_cache::Result<(RoomEventCache, Arc)> { + let global_event_cache = self.client.event_cache(); + + global_event_cache.for_room(self.room_id()).await + } } /// Details of the (latest) invite. diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 2f9dc4287..9ec2aec62 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -3,7 +3,7 @@ use std::time::Duration; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ event_cache::{EventCacheError, RoomEventCacheUpdate}, - test_utils::{logged_in_client, logged_in_client_with_server}, + test_utils::logged_in_client_with_server, }; use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::{ @@ -32,46 +32,37 @@ fn assert_event_matches_msg(event: &SyncTimelineEvent, expected: &str) { #[async_test] async fn test_must_explicitly_subscribe() { - let client = logged_in_client(None).await; + let (client, server) = logged_in_client_with_server().await; - let event_cache = client.event_cache(); + let room_id = room_id!("!omelette:fromage.fr"); + + // Make sure the client is aware of the room. + { + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + let response_body = sync_builder.build_json_sync_response(); + + mock_sync(&server, response_body, None).await; + client.sync_once(Default::default()).await.unwrap(); + server.reset().await; + } // If I create a room event subscriber for a room before subscribing the event // cache, - let room_id = room_id!("!omelette:fromage.fr"); - let result = event_cache.for_room(room_id).await; + let room = client.get_room(room_id).unwrap(); + let result = room.event_cache().await; // Then it fails, because one must explicitly call `.subscribe()` on the event // cache. assert_matches!(result, Err(EventCacheError::NotSubscribedYet)); } -#[async_test] -async fn test_cant_subscribe_to_unknown_room() { - let client = logged_in_client(None).await; - - let event_cache = client.event_cache(); - - // First, subscribe to the event cache. - event_cache.subscribe().unwrap(); - - // If I create a room event subscriber for a room unknown to the client, - let room_id = room_id!("!omelette:fromage.fr"); - let result = event_cache.for_room(room_id).await; - - // Then it fails, because this particular room is still unknown to the client. - assert_let!(Err(EventCacheError::RoomNotFound(observed_room_id)) = result); - assert_eq!(observed_room_id, room_id); -} - #[async_test] async fn test_add_initial_events() { let (client, server) = logged_in_client_with_server().await; - let event_cache = client.event_cache(); - // Immediately subscribe the event cache to sync updates. - event_cache.subscribe().unwrap(); + client.event_cache().subscribe().unwrap(); // If I sync and get informed I've joined The Room, but with no events, let room_id = room_id!("!omelette:fromage.fr"); @@ -86,7 +77,8 @@ async fn test_add_initial_events() { // If I create a room event subscriber, - let (room_event_cache, _drop_handles) = event_cache.for_room(room_id).await.unwrap(); + let room = client.get_room(room_id).unwrap(); + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); let (events, mut subscriber) = room_event_cache.subscribe().await.unwrap(); // Then at first it's empty, and the subscriber doesn't yield anything. @@ -131,7 +123,8 @@ async fn test_add_initial_events() { // XXX: when we get rid of `add_initial_events`, we can keep this test as a // smoke test for the event cache. - event_cache + client + .event_cache() .add_initial_events( room_id, vec![SyncTimelineEvent::new(sync_timeline_event!({ From 4b56ca1841786bafccc718077e11fb2123e12f41 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 29 Feb 2024 12:17:55 +0100 Subject: [PATCH 6/8] event cache: remove the `RoomNotFound` error Having `EventCache::for_room` return an `Option` avoids the need for the error. One caller can safely unwrap it, and others can log and ignore rooms that have disappeared (like we do in `call_sync_response_handlers`). --- crates/matrix-sdk/src/event_cache/mod.rs | 46 +++++++++++++----------- crates/matrix-sdk/src/room/mod.rs | 6 +++- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index a5226c2b6..621077708 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -60,7 +60,7 @@ use tokio::sync::{ broadcast::{error::RecvError, Receiver, Sender}, Mutex, RwLock, }; -use tracing::{error, trace}; +use tracing::{error, instrument, trace, warn}; use self::store::{EventCacheStore, MemoryStore}; use crate::{client::ClientInner, Client, Room}; @@ -77,13 +77,6 @@ pub enum EventCacheError { )] NotSubscribedYet, - /// The room hasn't been found in the client. - /// - /// Technically, it's possible to request a `RoomEventCache` for a room that - /// is not known to the client, leading to this error. - #[error("Room {0} hasn't been found in the Client.")] - RoomNotFound(OwnedRoomId), - /// The [`EventCache`] owns a weak reference to the [`Client`] it pertains /// to. It's possible this weak reference points to nothing anymore, at /// times where we try to use the client. @@ -209,7 +202,7 @@ impl EventCache { pub(crate) async fn for_room( &self, room_id: &RoomId, - ) -> Result<(RoomEventCache, Arc)> { + ) -> Result<(Option, Arc)> { let Some(drop_handles) = self.inner.drop_handles.get().cloned() else { return Err(EventCacheError::NotSubscribedYet); }; @@ -223,12 +216,16 @@ impl EventCache { /// /// TODO: temporary for API compat, as the event cache should take care of /// its own store. + #[instrument(skip(self, events))] pub async fn add_initial_events( &self, room_id: &RoomId, events: Vec, ) -> Result<()> { - let room_cache = self.inner.for_room(room_id).await?; + let Some(room_cache) = self.inner.for_room(room_id).await? else { + warn!("unknown room, skipping"); + return Ok(()); + }; // We could have received events during a previous sync; remove them all, since // we can't know where to insert the "initial events" with respect to @@ -270,6 +267,7 @@ impl EventCacheInner { } /// Handles a single set of room updates at once. + #[instrument(skip(self, updates))] async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> { // First, take the lock that indicates we're processing updates, to avoid // handling multiple updates concurrently. @@ -277,7 +275,10 @@ impl EventCacheInner { // Left rooms. for (room_id, left_room_update) in updates.leave { - let room = self.for_room(&room_id).await?; + let Some(room) = self.for_room(&room_id).await? else { + warn!(%room_id, "missing left room"); + continue; + }; if let Err(err) = room.inner.handle_left_room_update(left_room_update).await { // Non-fatal error, try to continue to the next room. @@ -287,7 +288,10 @@ impl EventCacheInner { // Joined rooms. for (room_id, joined_room_update) in updates.join { - let room = self.for_room(&room_id).await?; + let Some(room) = self.for_room(&room_id).await? else { + warn!(%room_id, "missing joined room"); + continue; + }; if let Err(err) = room.inner.handle_joined_room_update(joined_room_update).await { // Non-fatal error, try to continue to the next room. @@ -303,14 +307,15 @@ impl EventCacheInner { /// Return a room-specific view over the [`EventCache`]. /// - /// It may not be found, if the room isn't known to the client. - async fn for_room(&self, room_id: &RoomId) -> Result { + /// It may not be found, if the room isn't known to the client, in which + /// case it'll return None. + async fn for_room(&self, room_id: &RoomId) -> Result> { // Fast path: the entry exists; let's acquire a read lock, it's cheaper than a // write lock. let by_room_guard = self.by_room.read().await; match by_room_guard.get(room_id) { - Some(room) => Ok(room.clone()), + Some(room) => Ok(Some(room.clone())), None => { // Slow-path: the entry doesn't exist; let's acquire a write lock. @@ -320,19 +325,18 @@ impl EventCacheInner { // In the meanwhile, some other caller might have obtained write access and done // the same, so check for existence again. if let Some(room) = by_room_guard.get(room_id) { - return Ok(room.clone()); + return Ok(Some(room.clone())); } - let room = self - .client()? - .get_room(room_id) - .ok_or_else(|| EventCacheError::RoomNotFound(room_id.to_owned()))?; + let Some(room) = self.client()?.get_room(room_id) else { + return Ok(None); + }; let room_event_cache = RoomEventCache::new(room, self.store.clone()); by_room_guard.insert(room_id.to_owned(), room_event_cache.clone()); - Ok(room_event_cache) + Ok(Some(room_event_cache)) } } } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index f071ad9b9..18379367e 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2581,7 +2581,11 @@ impl Room { ) -> event_cache::Result<(RoomEventCache, Arc)> { let global_event_cache = self.client.event_cache(); - global_event_cache.for_room(self.room_id()).await + global_event_cache.for_room(self.room_id()).await.map(|(maybe_room, drop_handles)| { + // SAFETY: the `RoomEventCache` must always been found, since we're constructing + // from a `Room`. + (maybe_room.unwrap(), drop_handles) + }) } } From 0c98e26a055a305812fdda5c19110f492aaa6b72 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 4 Mar 2024 16:37:16 +0100 Subject: [PATCH 7/8] sdk: create new `users_with_power_levels` fn (#3182) It maps user ids to users' power levels. Also, make sure it just returns an empty map if this info is not available, instead of crashing. Then use it in the FFI side to output updated data for the `RoomInfo`. --- * sdk: create new `users_with_power_levels` fn which maps user ids to users' power levels Also, make sure it just returns an empty map if this info is not available, instead of crashing. * Update crates/matrix-sdk/src/room/mod.rs Co-authored-by: Benjamin Bouvier Signed-off-by: Jorge Martin Espinosa * Improve tests --------- Signed-off-by: Jorge Martin Espinosa Co-authored-by: Benjamin Bouvier --- bindings/matrix-sdk-ffi/src/room_info.rs | 6 +- crates/matrix-sdk/src/room/mod.rs | 23 ++- .../tests/integration/room/joined.rs | 30 +++ testing/matrix-sdk-test/src/test_json/sync.rs | 189 ++++++++++++++++++ 4 files changed, 242 insertions(+), 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_info.rs b/bindings/matrix-sdk-ffi/src/room_info.rs index 879c89f66..5d3f31e5c 100644 --- a/bindings/matrix-sdk-ffi/src/room_info.rs +++ b/bindings/matrix-sdk-ffi/src/room_info.rs @@ -54,10 +54,10 @@ impl RoomInfo { ) -> matrix_sdk::Result { let unread_notification_counts = room.unread_notification_counts(); - let power_levels = room.room_power_levels().await?; + let power_levels_map = room.users_with_power_levels().await; let mut user_power_levels = HashMap::::new(); - for (id, level) in power_levels.users.iter() { - user_power_levels.insert(id.to_string(), (*level).into()); + for (id, level) in power_levels_map.iter() { + user_power_levels.insert(id.to_string(), *level); } Ok(Self { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 18379367e..61e3e41a3 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1,6 +1,12 @@ //! High-level room API -use std::{borrow::Borrow, collections::BTreeMap, ops::Deref, sync::Arc, time::Duration}; +use std::{ + borrow::Borrow, + collections::{BTreeMap, HashMap}, + ops::Deref, + sync::Arc, + time::Duration, +}; use eyeball::SharedObservable; use futures_core::Stream; @@ -1589,8 +1595,6 @@ impl Room { /// Run /keys/query requests for all the non-tracked users. #[cfg(feature = "e2e-encryption")] async fn query_keys_for_untracked_users(&self) -> Result<()> { - use std::collections::HashMap; - let olm = self.client.olm_machine().await; let olm = olm.as_ref().expect("Olm machine wasn't started"); @@ -1874,6 +1878,19 @@ impl Room { Ok(event.for_user(user_id).into()) } + /// Gets a map with the `UserId` of users with power levels other than `0` + /// and this power level. + pub async fn users_with_power_levels(&self) -> HashMap { + let power_levels = self.room_power_levels().await.ok(); + let mut user_power_levels = HashMap::::new(); + if let Some(power_levels) = power_levels { + for (id, level) in power_levels.users.into_iter() { + user_power_levels.insert(id, level.into()); + } + } + user_power_levels + } + /// Sets the name of this room. pub async fn set_name(&self, name: String) -> Result { self.send_state_event(RoomNameEventContent::new(name)).await diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index d3dc62031..f8988d352 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -787,3 +787,33 @@ async fn get_power_level_for_user() { room.get_user_power_level(user_id!("@non-existing:localhost")).await.unwrap(); assert_eq!(power_level_unknown, 0); } + +#[async_test] +async fn get_users_with_power_levels() { + let (client, server) = logged_in_client_with_server().await; + + mock_sync(&server, &*test_json::sync::SYNC_ADMIN_AND_MOD, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings).await.unwrap(); + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let users_with_power_levels = room.users_with_power_levels().await; + assert_eq!(users_with_power_levels.len(), 2); + assert_eq!(users_with_power_levels[user_id!("@admin:localhost")], 100); + assert_eq!(users_with_power_levels[user_id!("@mod:localhost")], 50); +} + +#[async_test] +async fn get_users_with_power_levels_is_empty_if_power_level_info_is_not_available() { + let (client, server) = logged_in_client_with_server().await; + + mock_sync(&server, &*test_json::INVITE_SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings).await.unwrap(); + // The room doesn't have any power level info + let room = client.get_room(room_id!("!696r7674:example.com")).unwrap(); + + assert!(room.users_with_power_levels().await.is_empty()); +} diff --git a/testing/matrix-sdk-test/src/test_json/sync.rs b/testing/matrix-sdk-test/src/test_json/sync.rs index 6a1e40bf8..3a878d5ae 100644 --- a/testing/matrix-sdk-test/src/test_json/sync.rs +++ b/testing/matrix-sdk-test/src/test_json/sync.rs @@ -1525,3 +1525,192 @@ pub static VOIP_SYNC: Lazy = Lazy::new(|| { } }) }); + +pub static SYNC_ADMIN_AND_MOD: Lazy = Lazy::new(|| { + json!({ + "device_one_time_keys_count": {}, + "next_batch": "s526_47314_0_7_1_1_1_11444_1", + "device_lists": { + "changed": [ + "@admin:example.org" + ], + "left": [] + }, + "rooms": { + "invite": {}, + "join": { + *DEFAULT_TEST_ROOM_ID: { + "summary": { + "m.heroes": [ + "@example2:localhost" + ], + "m.joined_member_count": 2, + "m.invited_member_count": 0 + }, + "account_data": { + "events": [] + }, + "ephemeral": { + "events": [] + }, + "state": { + "events": [ + { + "content": { + "join_rule": "public" + }, + "event_id": "$15139375514WsgmR:localhost", + "origin_server_ts": 151393755000000_u64, + "sender": "@admin:localhost", + "state_key": "", + "type": "m.room.join_rules", + "unsigned": { + "age": 7034220 + } + }, + { + "content": { + "avatar_url": null, + "displayname": "admin", + "membership": "join" + }, + "event_id": "$151800140517rfvjc:localhost", + "membership": "join", + "origin_server_ts": 151800140000000_u64, + "sender": "@admin:localhost", + "state_key": "@admin:localhost", + "type": "m.room.member", + "unsigned": { + "age": 297036, + "replaces_state": "$151800111315tsynI:localhost" + } + }, + { + "content": { + "avatar_url": null, + "displayname": "mod", + "membership": "join" + }, + "event_id": "$151800140518rfvjc:localhost", + "membership": "join", + "origin_server_ts": 1518001450000000_u64, + "sender": "@mod:localhost", + "state_key": "@mod:localhost", + "type": "m.room.member", + "unsigned": { + "age": 297035, + } + }, + { + "content": { + "history_visibility": "shared" + }, + "event_id": "$15139375515VaJEY:localhost", + "origin_server_ts": 151393755000000_u64, + "sender": "@admin:localhost", + "state_key": "", + "type": "m.room.history_visibility", + "unsigned": { + "age": 703422 + } + }, + { + "content": { + "creator": "@example:localhost" + }, + "event_id": "$15139375510KUZHi:localhost", + "origin_server_ts": 151393755000000_u64, + "sender": "@admin:localhost", + "state_key": "", + "type": "m.room.create", + "unsigned": { + "age": 703422 + } + }, + { + "content": { + "topic": "room topic" + }, + "event_id": "$151957878228ssqrJ:localhost", + "origin_server_ts": 151957878000000_u64, + "sender": "@admin:localhost", + "state_key": "", + "type": "m.room.topic", + "unsigned": { + "age": 1392989709, + "prev_content": { + "topic": "test" + }, + "prev_sender": "@example:localhost", + "replaces_state": "$151957069225EVYKm:localhost" + } + }, + { + "content": { + "ban": 50, + "events": { + "m.room.avatar": 50, + "m.room.canonical_alias": 50, + "m.room.history_visibility": 100, + "m.room.name": 50, + "m.room.power_levels": 100 + }, + "events_default": 0, + "invite": 0, + "kick": 50, + "redact": 50, + "state_default": 50, + "users": { + "@admin:localhost": 100, + "@mod:localhost": 50 + }, + "users_default": 0 + }, + "event_id": "$15139375512JaHAW:localhost", + "origin_server_ts": 151393755000000_u64, + "sender": "@admin:localhost", + "state_key": "", + "type": "m.room.power_levels", + "unsigned": { + "age": 703422 + } + } + ] + }, + "timeline": { + "events": [ + { + "content": { + "body": "baba", + "format": "org.matrix.custom.html", + "formatted_body": "baba", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 152037280000000_u64, + "sender": "@admin:localhost", + "type": "m.room.message", + "unsigned": { + "age": 598971425 + } + } + ], + "limited": true, + "prev_batch": "t392-516_47314_0_7_1_1_1_11444_1" + }, + "unread_notifications": { + "highlight_count": 0, + "notification_count": 11 + } + } + }, + "leave": {} + }, + "to_device": { + "events": [] + }, + "presence": { + "events": [] + } + }) +}); From 98a68632dfa5377c002adf0684891a116c9690b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 21:32:07 +0000 Subject: [PATCH 8/8] chore(deps): bump mio from 0.8.10 to 0.8.11 Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11. - [Release notes](https://github.com/tokio-rs/mio/releases) - [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11) --- updated-dependencies: - dependency-name: mio dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 970e5372a..d30ccf281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3442,9 +3442,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1",