Merge branch 'main' into mauroromito/directory_search

This commit is contained in:
Mauro
2024-03-05 11:44:15 +01:00
committed by GitHub
18 changed files with 410 additions and 75 deletions

View File

@@ -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.

4
Cargo.lock generated
View File

@@ -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",

View File

@@ -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<UserPowerLevelUpdate>,
) -> 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::<Result<Vec<_>>>()?;
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<ImageInfo> for RumaAvatarImageInfo {
type Error = MediaInfoError;

View File

@@ -54,10 +54,10 @@ impl RoomInfo {
) -> matrix_sdk::Result<Self> {
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::<String, i64>::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 {

View File

@@ -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<u64>,
has_been_edited: bool,
},
CallInvite,
UnableToDecrypt {
msg: EncryptedMessage,
},

View File

@@ -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));
}
_ => (),
}
}
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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();

View File

@@ -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!(

View File

@@ -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()),

View File

@@ -193,6 +193,7 @@ pub fn default_event_filter(event: &AnySyncTimelineEvent, room_version: &RoomVer
| AnyMessageLikeEventContent::UnstablePollStart(
UnstablePollStartEventContent::New(_),
)
| AnyMessageLikeEventContent::CallInvite(_)
| AnyMessageLikeEventContent::RoomEncrypted(_) => true,
_ => false,

View File

@@ -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");

View File

@@ -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<EventCacheDropHandles>)> {
) -> Result<(Option<RoomEventCache>, Arc<EventCacheDropHandles>)> {
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<SyncTimelineEvent>,
) -> 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<RoomEventCache> {
/// 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<Option<RoomEventCache>> {
// 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))
}
}
}

View File

@@ -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<OwnedUserId, i64> {
let power_levels = self.room_power_levels().await.ok();
let mut user_power_levels = HashMap::<OwnedUserId, i64>::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<send_state_event::v3::Response> {
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<EventCacheDropHandles>)> {
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.

View File

@@ -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!({

View File

@@ -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());
}

View File

@@ -1525,3 +1525,192 @@ pub static VOIP_SYNC: Lazy<JsonValue> = Lazy::new(|| {
}
})
});
pub static SYNC_ADMIN_AND_MOD: Lazy<JsonValue> = 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": "<strong>baba</strong>",
"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": []
}
})
});