diff --git a/design.md b/design.md index 8b974294..2686f513 100644 --- a/design.md +++ b/design.md @@ -3,7 +3,7 @@ ## Design and Layout #### Async Client -The highest level structure that ties the other pieces of functionality together. The client is responsible for the Request/Response cycle. It can be thought of as a thin layer atop the `BaseClient` passing requests along for the `BaseClient` to handle. A user should be able to write their own `AsyncClient` using the `BaseClient`. It knows how to +The highest level structure that ties the other pieces of functionality together. The client is responsible for the Request/Response cycle. It can be thought of as a thin layer atop the `BaseClient` passing requests along for the `BaseClient` to handle. A user should be able to write their own `AsyncClient` using the `BaseClient`. It knows how to - login - send messages - encryption ... diff --git a/src/async_client.rs b/src/async_client.rs index 9ce12c68..718e423b 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -37,10 +37,10 @@ use ruma_api::{Endpoint, Outgoing}; use ruma_events::room::message::MessageEventContent; use ruma_events::EventResult; pub use ruma_events::EventType; -use ruma_identifiers::RoomId; +use ruma_identifiers::{RoomId, RoomIdOrAliasId, UserId}; #[cfg(feature = "encryption")] -use ruma_identifiers::{DeviceId, UserId}; +use ruma_identifiers::DeviceId; use crate::api; use crate::base_client::Client as BaseClient; @@ -181,7 +181,17 @@ impl SyncSettings { use api::r0::client_exchange::send_event_to_device; #[cfg(feature = "encryption")] use api::r0::keys::{claim_keys, get_keys, upload_keys, KeyAlgorithm}; +use api::r0::membership::join_room_by_id; +use api::r0::membership::join_room_by_id_or_alias; +use api::r0::membership::kick_user; +use api::r0::membership::leave_room; +use api::r0::membership::{ + invite_user::{self, InvitationRecipient}, + Invite3pid, +}; use api::r0::message::create_message_event; +use api::r0::message::get_message_events; +use api::r0::room::create_room; use api::r0::session::login; use api::r0::sync::sync_events; @@ -333,6 +343,207 @@ impl AsyncClient { Ok(response) } + /// Join a room by `RoomId`. + /// + /// Returns a `join_room_by_id::Response` consisting of the + /// joined rooms `RoomId`. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room to be joined. + pub async fn join_room_by_id(&mut self, room_id: &RoomId) -> Result { + let request = join_room_by_id::Request { + room_id: room_id.clone(), + third_party_signed: None, + }; + self.send(request).await + } + + /// Join a room by `RoomId`. + /// + /// Returns a `join_room_by_id_or_alias::Response` consisting of the + /// joined rooms `RoomId`. + /// + /// # Arguments + /// + /// * alias - The `RoomId` or `RoomAliasId` of the room to be joined. + /// An alias looks like this `#name:example.com` + pub async fn join_room_by_id_or_alias( + &mut self, + alias: &RoomIdOrAliasId, + ) -> Result { + let request = join_room_by_id_or_alias::Request { + room_id_or_alias: alias.clone(), + third_party_signed: None, + }; + self.send(request).await + } + + /// Kick a user out of the specified room. + /// + /// Returns a `kick_user::Response`, an empty response. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room the user should be kicked out of. + /// + /// * user_id - The `UserId` of the user that should be kicked out of the room. + /// + /// * reason - Optional reason why the room member is being kicked out. + pub async fn kick_user( + &mut self, + room_id: &RoomId, + user_id: &UserId, + reason: Option, + ) -> Result { + let request = kick_user::Request { + reason, + room_id: room_id.clone(), + user_id: user_id.clone(), + }; + self.send(request).await + } + + /// Leave the specified room. + /// + /// Returns a `leave_room::Response`, an empty response. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room to leave. + /// + pub async fn leave_room(&mut self, room_id: &RoomId) -> Result { + let request = leave_room::Request { + room_id: room_id.clone(), + }; + self.send(request).await + } + + /// Invite the specified user by `UserId` to the given room. + /// + /// Returns a `invite_user::Response`, an empty response. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room to invite the specified user to. + /// + /// * user_id - The `UserId` of the user to invite to the room. + pub async fn invite_user_by_id( + &mut self, + room_id: &RoomId, + user_id: &UserId, + ) -> Result { + let request = invite_user::Request { + room_id: room_id.clone(), + recipient: InvitationRecipient::UserId { + user_id: user_id.clone(), + }, + }; + self.send(request).await + } + + /// Invite the specified user by third party id to the given room. + /// + /// Returns a `invite_user::Response`, an empty response. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room to invite the specified user to. + /// + /// * invite_id - A third party id of a user to invite to the room. + pub async fn invite_user_by_3pid( + &mut self, + room_id: &RoomId, + invite_id: &Invite3pid, + ) -> Result { + let request = invite_user::Request { + room_id: room_id.clone(), + recipient: InvitationRecipient::ThirdPartyId(invite_id.clone()), + }; + self.send(request).await + } + + /// Create a room using the `RoomBuilder` and send the request. + /// + /// Sends a request to `/_matrix/client/r0/createRoom`, returns a `create_room::Response`, + /// this is an empty response. + /// + /// # Arguments + /// + /// * room - The easiest way to create this request is using the `RoomBuilder`. + /// + /// # Examples + /// ```no_run + /// use matrix_sdk::{AsyncClient, RoomBuilder}; + /// # use matrix_sdk::api::r0::room::Visibility; + /// # use url::Url; + /// + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// let mut builder = RoomBuilder::default(); + /// builder.creation_content(false) + /// .initial_state(vec![]) + /// .visibility(Visibility::Public) + /// .name("name") + /// .room_version("v1.0"); + /// + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); + /// # use futures::executor::block_on; + /// # block_on(async { + /// assert!(cli.create_room(builder).await.is_ok()); + /// # }); + /// ``` + pub async fn create_room>( + &mut self, + room: R, + ) -> Result { + let request = room.into(); + self.send(request).await + } + + /// Get messages starting at a specific sync point using the + /// `MessagesRequestBuilder`s `from` field as a starting point. + /// + /// Sends a request to `/_matrix/client/r0/rooms/{room_id}/messages` and + /// returns a `get_message_events::IncomingResponse` that contains chunks + /// of `RoomEvents`. + /// + /// # Arguments + /// + /// * request - The easiest way to create a `Request` is using the + /// `MessagesRequestBuilder`. + /// + /// # Examples + /// ```no_run + /// # use std::convert::TryFrom; + /// use matrix_sdk::{AsyncClient, MessagesRequestBuilder}; + /// # use matrix_sdk::identifiers::RoomId; + /// # use matrix_sdk::api::r0::filter::RoomEventFilter; + /// # use matrix_sdk::api::r0::message::get_message_events::Direction; + /// # use url::Url; + /// # use js_int::UInt; + /// + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// let mut builder = MessagesRequestBuilder::new(); + /// builder.room_id(RoomId::try_from("!roomid:example.com").unwrap()) + /// .from("t47429-4392820_219380_26003_2265".to_string()) + /// .to("t4357353_219380_26003_2265".to_string()) + /// .direction(Direction::Backward) + /// .limit(UInt::new(10).unwrap()); + /// + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); + /// # use futures::executor::block_on; + /// # block_on(async { + /// assert!(cli.room_messages(builder).await.is_ok()); + /// # }); + /// ``` + pub async fn room_messages>( + &mut self, + request: R, + ) -> Result { + let req = request.into(); + self.send(req).await + } + /// Synchronize the client's state with the latest state on the server. /// /// # Arguments @@ -635,6 +846,10 @@ impl AsyncClient { /// /// * `content` - The content of the message event. /// + /// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent` held + /// in it's unsigned field as `transaction_id`. If not given one is created for the + /// message. + /// /// # Example /// ```no_run /// # use matrix_sdk::Room; @@ -649,6 +864,7 @@ impl AsyncClient { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); /// # let mut client = AsyncClient::new(homeserver, None).unwrap(); /// # let room_id = RoomId::try_from("!test:localhost").unwrap(); + /// use uuid::Uuid; /// /// let content = MessageEventContent::Text(TextMessageEventContent { /// body: "Hello world".to_owned(), @@ -656,14 +872,15 @@ impl AsyncClient { /// formatted_body: None, /// relates_to: None, /// }); - /// - /// client.room_send(&room_id, content).await.unwrap(); + /// let txn_id = Uuid::new_v4(); + /// client.room_send(&room_id, content, Some(txn_id)).await.unwrap(); /// }) /// ``` pub async fn room_send( &mut self, room_id: &RoomId, #[allow(unused_mut)] mut content: MessageEventContent, + txn_id: Option, ) -> Result { #[allow(unused_mut)] let mut event_type = EventType::RoomMessage; @@ -722,7 +939,7 @@ impl AsyncClient { let request = create_message_event::Request { room_id: room_id.clone(), event_type, - txn_id: Uuid::new_v4().to_string(), + txn_id: txn_id.unwrap_or_else(Uuid::new_v4).to_string(), data: content, }; diff --git a/src/lib.rs b/src/lib.rs index 28bf9375..b3c13bdb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ mod base_client; mod error; mod event_emitter; mod models; +mod request_builder; mod session; #[cfg(test)] @@ -50,5 +51,6 @@ pub use async_client::{AsyncClient, AsyncClientConfig, SyncSettings}; pub use base_client::Client; pub use event_emitter::EventEmitter; pub use models::Room; +pub use request_builder::{MessagesRequestBuilder, RoomBuilder}; pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/request_builder.rs b/src/request_builder.rs new file mode 100644 index 00000000..42f088f5 --- /dev/null +++ b/src/request_builder.rs @@ -0,0 +1,379 @@ +use crate::api; +use crate::events::room::power_levels::PowerLevelsEventContent; +use crate::identifiers::{RoomId, UserId}; +use api::r0::filter::RoomEventFilter; +use api::r0::membership::Invite3pid; +use api::r0::message::get_message_events::{self, Direction}; +use api::r0::room::{ + create_room::{self, CreationContent, InitialStateEvent, RoomPreset}, + Visibility, +}; + +use js_int::UInt; + +/// A builder used to create rooms. +/// +/// # Examples +/// ``` +/// # use std::convert::TryFrom; +/// # use matrix_sdk::{AsyncClient, RoomBuilder}; +/// # use matrix_sdk::api::r0::room::Visibility; +/// # use matrix_sdk::identifiers::UserId; +/// # use url::Url; +/// # let homeserver = Url::parse("http://example.com").unwrap(); +/// # let mut rt = tokio::runtime::Runtime::new().unwrap(); +/// # rt.block_on(async { +/// let mut builder = RoomBuilder::default(); +/// builder.creation_content(false) +/// .initial_state(vec![]) +/// .visibility(Visibility::Public) +/// .name("name") +/// .room_version("v1.0"); +/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); +/// cli.create_room(builder).await; +/// # }) +/// ``` +#[derive(Clone, Default)] +pub struct RoomBuilder { + /// Extra keys to be added to the content of the `m.room.create`. + creation_content: Option, + /// List of state events to send to the new room. + /// + /// Takes precedence over events set by preset, but gets overriden by + /// name and topic keys. + initial_state: Vec, + /// A list of user IDs to invite to the room. + /// + /// This will tell the server to invite everyone in the list to the newly created room. + invite: Vec, + /// List of third party IDs of users to invite. + invite_3pid: Vec, + /// If set, this sets the `is_direct` flag on room invites. + is_direct: Option, + /// If this is included, an `m.room.name` event will be sent into the room to indicate + /// the name of the room. + name: Option, + /// Power level content to override in the default power level event. + power_level_content_override: Option, + /// Convenience parameter for setting various default state events based on a preset. + preset: Option, + /// The desired room alias local part. + room_alias_name: Option, + /// Room version to set for the room. Defaults to homeserver's default if not specified. + room_version: Option, + /// If this is included, an `m.room.topic` event will be sent into the room to indicate + /// the topic for the room. + topic: Option, + /// A public visibility indicates that the room will be shown in the published room + /// list. A private visibility will hide the room from the published room list. Rooms + /// default to private visibility if this key is not included. + visibility: Option, +} + +impl RoomBuilder { + /// Returns an empty `RoomBuilder` for creating rooms. + pub fn new() -> Self { + Self::default() + } + + /// Set the `CreationContent`. + /// + /// Weather users on other servers can join this room. + pub fn creation_content(&mut self, federate: bool) -> &mut Self { + let federate = Some(federate); + self.creation_content = Some(CreationContent { federate }); + self + } + + /// Set the `InitialStateEvent` vector. + pub fn initial_state(&mut self, state: Vec) -> &mut Self { + self.initial_state = state; + self + } + + /// Set the vec of `UserId`s. + pub fn invite(&mut self, invite: Vec) -> &mut Self { + self.invite = invite; + self + } + + /// Set the vec of `Invite3pid`s. + pub fn invite_3pid(&mut self, invite: Vec) -> &mut Self { + self.invite_3pid = invite; + self + } + + /// Set the vec of `Invite3pid`s. + pub fn is_direct(&mut self, direct: bool) -> &mut Self { + self.is_direct = Some(direct); + self + } + + /// Set the room name. A `m.room.name` event will be sent to the room. + pub fn name>(&mut self, name: S) -> &mut Self { + self.name = Some(name.into()); + self + } + + /// Set the room's power levels. + pub fn power_level_override(&mut self, power: PowerLevelsEventContent) -> &mut Self { + self.power_level_content_override = Some(power); + self + } + + /// Convenience for setting various default state events based on a preset. + pub fn preset(&mut self, preset: RoomPreset) -> &mut Self { + self.preset = Some(preset); + self + } + + /// The local part of a room alias. + pub fn room_alias_name>(&mut self, alias: S) -> &mut Self { + self.room_alias_name = Some(alias.into()); + self + } + + /// Room version, defaults to homeserver's version if left unspecified. + pub fn room_version>(&mut self, version: S) -> &mut Self { + self.room_version = Some(version.into()); + self + } + + /// If included, a `m.room.topic` event will be sent to the room. + pub fn topic>(&mut self, topic: S) -> &mut Self { + self.topic = Some(topic.into()); + self + } + + /// A public visibility indicates that the room will be shown in the published + /// room list. A private visibility will hide the room from the published room list. + /// Rooms default to private visibility if this key is not included. + pub fn visibility(&mut self, vis: Visibility) -> &mut Self { + self.visibility = Some(vis); + self + } +} + +impl Into for RoomBuilder { + fn into(self) -> create_room::Request { + create_room::Request { + creation_content: self.creation_content, + initial_state: self.initial_state, + invite: self.invite, + invite_3pid: self.invite_3pid, + is_direct: self.is_direct, + name: self.name, + power_level_content_override: self.power_level_content_override, + preset: self.preset, + room_alias_name: self.room_alias_name, + room_version: self.room_version, + topic: self.topic, + visibility: self.visibility, + } + } +} + +/// Create a builder for making get_message_event requests. +/// +/// # Examples +/// ``` +/// # use matrix_sdk::{AsyncClient, MessagesRequestBuilder}; +/// # use matrix_sdk::api::r0::message::get_message_events::{self, Direction}; +/// # use matrix_sdk::identifiers::RoomId; +/// # use url::Url; +/// # let homeserver = Url::parse("http://example.com").unwrap(); +/// # let mut rt = tokio::runtime::Runtime::new().unwrap(); +/// # rt.block_on(async { +/// # let room_id = RoomId::new(homeserver.as_str()).unwrap(); +/// # let last_sync_token = "".to_string();; +/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); +/// +/// let mut builder = MessagesRequestBuilder::new(); +/// builder.room_id(room_id) +/// .from(last_sync_token) +/// .direction(Direction::Forward); +/// +/// cli.room_messages(builder).await.is_err(); +/// # }) +/// ``` +#[derive(Clone, Default)] +pub struct MessagesRequestBuilder { + /// The room to get events from. + room_id: Option, + /// The token to start returning events from. + /// + /// This token can be obtained from a + /// prev_batch token returned for each room by the sync API, or from a start or end token + /// returned by a previous request to this endpoint. + from: Option, + /// The token to stop returning events at. + /// + /// This token can be obtained from a prev_batch + /// token returned for each room by the sync endpoint, or from a start or end token returned + /// by a previous request to this endpoint. + to: Option, + /// The direction to return events from. + direction: Option, + /// The maximum number of events to return. + /// + /// Default: 10. + limit: Option, + /// A filter of the returned events with. + filter: Option, +} + +impl MessagesRequestBuilder { + /// Create a `MessagesRequestBuilder` builder to make a `get_message_events::Request`. + /// + /// The `room_id` and `from`` fields **need to be set** to create the request. + pub fn new() -> Self { + Self::default() + } + + /// RoomId is required to create a `get_message_events::Request`. + pub fn room_id(&mut self, room_id: RoomId) -> &mut Self { + self.room_id = Some(room_id); + self + } + + /// A `next_batch` token or `start` or `end` from a previous `get_message_events` request. + /// + /// This is required to create a `get_message_events::Request`. + pub fn from(&mut self, from: String) -> &mut Self { + self.from = Some(from); + self + } + + /// A `next_batch` token or `start` or `end` from a previous `get_message_events` request. + /// + /// This token signals when to stop receiving events. + pub fn to(&mut self, to: String) -> &mut Self { + self.to = Some(to); + self + } + + /// The direction to return events from. + /// + /// If not specified `Direction::Backward` is used. + pub fn direction(&mut self, direction: Direction) -> &mut Self { + self.direction = Some(direction); + self + } + + /// The maximum number of events to return. + pub fn limit(&mut self, limit: UInt) -> &mut Self { + self.limit = Some(limit); + self + } + + /// Filter events by the given `RoomEventFilter`. + pub fn filter(&mut self, filter: RoomEventFilter) -> &mut Self { + self.filter = Some(filter); + self + } +} + +impl Into for MessagesRequestBuilder { + fn into(self) -> get_message_events::Request { + get_message_events::Request { + room_id: self.room_id.expect("`room_id` and `from` need to be set"), + from: self.from.expect("`room_id` and `from` need to be set"), + to: self.to, + dir: self.direction.unwrap_or(Direction::Backward), + limit: self.limit, + filter: self.filter, + } + } +} + +#[cfg(test)] +mod test { + use std::collections::HashMap; + + use super::*; + use crate::events::room::power_levels::NotificationPowerLevels; + use crate::{identifiers::RoomId, AsyncClient, Session}; + + use js_int::Int; + use mockito::{mock, Matcher}; + use std::convert::TryFrom; + use url::Url; + + #[tokio::test] + async fn create_room_builder() { + let homeserver = Url::parse(&mockito::server_url()).unwrap(); + + let _m = mock("POST", "/_matrix/client/r0/createRoom") + .with_status(200) + .with_body_from_file("./tests/data/room_id.json") + .create(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:localhost").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let mut builder = RoomBuilder::new(); + builder + .creation_content(false) + .initial_state(vec![]) + .visibility(Visibility::Public) + .name("room_name") + .room_version("v1.0") + .invite_3pid(vec![]) + .is_direct(true) + .power_level_override(PowerLevelsEventContent { + ban: Int::max_value(), + events: HashMap::default(), + events_default: Int::min_value(), + invite: Int::min_value(), + kick: Int::min_value(), + redact: Int::max_value(), + state_default: Int::min_value(), + users_default: Int::min_value(), + notifications: NotificationPowerLevels { + room: Int::min_value(), + }, + users: HashMap::default(), + }) + .preset(RoomPreset::PrivateChat) + .room_alias_name("room_alias") + .topic("room topic") + .visibility(Visibility::Private); + let mut cli = AsyncClient::new(homeserver, Some(session)).unwrap(); + assert!(cli.create_room(builder).await.is_ok()); + } + + #[tokio::test] + async fn get_message_events() { + let homeserver = Url::parse(&mockito::server_url()).unwrap(); + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/messages".to_string()), + ) + .with_status(200) + .with_body_from_file("./tests/data/room_messages.json") + .create(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:localhost").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let mut builder = MessagesRequestBuilder::new(); + builder + .room_id(RoomId::try_from("!roomid:example.com").unwrap()) + .from("t47429-4392820_219380_26003_2265".to_string()) + .to("t4357353_219380_26003_2265".to_string()) + .direction(Direction::Backward) + .limit(UInt::new(10).unwrap()); + // TODO this makes ruma error `Err(IntoHttp(IntoHttpError(Query(Custom("unsupported value")))))`?? + // .filter(RoomEventFilter::default()); + + let mut cli = AsyncClient::new(homeserver, Some(session)).unwrap(); + assert!(cli.room_messages(builder).await.is_ok()); + } +} diff --git a/tests/data/events/room_avatar.json b/tests/data/events/room_avatar.json index b92f9161..92ed2519 100644 --- a/tests/data/events/room_avatar.json +++ b/tests/data/events/room_avatar.json @@ -18,4 +18,3 @@ "age": 1234 } } - diff --git a/tests/data/sync2.json b/tests/data/sync2.json deleted file mode 100644 index 6adb2c13..00000000 --- a/tests/data/sync2.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "next_batch": "s72595_4483_1934", - "presence": { - "events": [ - { - "content": { - "avatar_url": "mxc://localhost:wefuiwegh8742w", - "last_active_ago": 2478593, - "presence": "online", - "currently_active": false, - "status_msg": "Making cupcakes" - }, - "type": "m.presence", - "sender": "@example:localhost" - } - ] - }, - "account_data": { - "events": [ - { - "type": "org.example.custom.config", - "content": { - "custom_config_key": "custom_config_value" - } - } - ] - }, - "rooms": { - "join": { - "!726s6s6q:example.com": { - "summary": { - "m.heroes": [ - "@alice:example.com", - "@bob:example.com" - ], - "m.joined_member_count": 2, - "m.invited_member_count": 0 - }, - "state": { - "events": [ - { - "content": { - "membership": "join", - "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", - "displayname": "Alice Margatroid" - }, - "type": "m.room.member", - "event_id": "$143273582443PhrSn:example.org", - "room_id": "!726s6s6q:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": { - "age": 1234 - }, - "state_key": "@alice:example.org" - } - ] - }, - "timeline": { - "events": [ - { - "content": { - "membership": "join", - "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", - "displayname": "Alice Margatroid" - }, - "type": "m.room.member", - "event_id": "$143273582443PhrSn:example.org", - "room_id": "!726s6s6q:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": { - "age": 1234 - }, - "state_key": "@alice:example.org" - }, - { - "content": { - "body": "This is an example text message", - "msgtype": "m.text", - "format": "org.matrix.custom.html", - "formatted_body": "This is an example text message" - }, - "type": "m.room.message", - "event_id": "$143273582443PhrSn:example.org", - "room_id": "!726s6s6q:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": { - "age": 1234 - } - } - ], - "limited": true, - "prev_batch": "t34-23535_0_0" - }, - "ephemeral": { - "events": [ - { - "content": { - "user_ids": [ - "@alice:matrix.org", - "@bob:example.com" - ] - }, - "type": "m.typing", - "room_id": "!jEsUZKDJdhlrceRyVU:example.org" - } - ] - }, - "account_data": { - "events": [ - { - "content": { - "tags": { - "u.work": { - "order": 0.9 - } - } - }, - "type": "m.tag" - }, - { - "type": "org.example.custom.room.config", - "content": { - "custom_config_key": "custom_config_value" - } - } - ] - } - } - }, - "invite": { - "!696r7674:example.com": { - "invite_state": { - "events": [ - { - "sender": "@alice:example.com", - "type": "m.room.name", - "state_key": "", - "content": { - "name": "My Room Name" - } - }, - { - "sender": "@alice:example.com", - "type": "m.room.member", - "state_key": "@bob:example.com", - "content": { - "membership": "invite" - } - } - ] - } - } - }, - "leave": {} - } - } - \ No newline at end of file