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. 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", diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 630fffdc3..c0a1af288 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -576,15 +576,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 updates = updates + .iter() + .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::>>()?; + self.inner - .update_power_levels(vec![(&user_id, power_level)]) + .update_power_levels(updates) .await .map_err(|e| ClientError::Generic { msg: e.to_string() })?; Ok(()) @@ -633,6 +639,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; 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/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/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-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"); diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 35ffb4866..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. @@ -206,10 +199,10 @@ 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)> { + ) -> 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 b4d0014d4..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, time::Duration}; +use std::{ + borrow::Borrow, + collections::{BTreeMap, HashMap}, + ops::Deref, + sync::Arc, + time::Duration, +}; use eyeball::SharedObservable; use futures_core::Stream; @@ -79,10 +85,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}, @@ -1586,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"); @@ -1871,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 @@ -2570,6 +2590,20 @@ 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.map(|(maybe_room, drop_handles)| { + // SAFETY: the `RoomEventCache` must always been found, since we're constructing + // from a `Room`. + (maybe_room.unwrap(), drop_handles) + }) + } } /// 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!({ 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": [] + } + }) +});