From 20618e7a2098650106ae1443a8dee818a5f13273 Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 16:14:16 -0400 Subject: [PATCH 01/28] calculate room name internal `Room` method --- src/base_client.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/src/base_client.rs b/src/base_client.rs index b0398a49..a22c338b 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -17,8 +17,14 @@ use std::collections::HashMap; use crate::api::r0 as api; use crate::events::collections::all::{RoomEvent, StateEvent}; -use crate::events::room::member::{MemberEvent, MembershipState}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MembershipState}, + name::{NameEvent}, +}; use crate::events::EventResult; +use crate::identifiers::{RoomAliasId}; use crate::session::Session; use std::sync::{Arc, RwLock}; @@ -34,6 +40,17 @@ pub type Token = String; pub type RoomId = String; pub type UserId = String; +#[derive(Debug, Default)] +/// `RoomName` allows the calculation of a text room name. +pub struct RoomName { + /// The displayed name of the room. + name: Option, + /// The canonical alias of the room ex. `#room-name:example.com` and port number. + canonical_alias: Option, + /// List of `RoomAliasId`s the room has been given. + aliases: Vec, +} + #[derive(Debug)] /// A Matrix room member. pub struct RoomMember { @@ -52,6 +69,8 @@ pub struct RoomMember { pub struct Room { /// The unique id of the room. pub room_id: RoomId, + /// The name of the room, clients use this to represent a room. + pub room_name: RoomName, /// The mxid of our own user. pub own_user_id: UserId, /// The mxid of the room creator. @@ -64,6 +83,46 @@ pub struct Room { pub encrypted: bool, } +impl RoomName { + pub fn push_alias(&mut self, alias: RoomAliasId) -> bool { + self.aliases.push(alias); + true + } + + pub fn set_canonical(&mut self, alias: RoomAliasId) -> bool { + self.canonical_alias = Some(alias); + true + } + + pub fn set_name(&mut self, name: &str) -> bool { + self.name = Some(name.to_string()); + true + } + + pub fn calculate_name(&self, room_id: &RoomId, members: &HashMap) -> String { + // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 + // the order in which we check for a name ^^ + if let Some(name) = &self.name { + name.clone() + } else if let Some(alias) = &self.canonical_alias { + alias.alias().to_string() + } else if !self.aliases.is_empty() { + self.aliases[0].alias().to_string() + } else { + // TODO + let mut names = members.values().flat_map(|m| m.display_name.clone()).take(3).collect::>(); + + if names.is_empty() { + format!("Room {}", room_id) + } else { + // stablize order + names.sort(); + names.join(", ").to_string() + } + } + } +} + impl Room { /// Create a new room. /// @@ -75,6 +134,7 @@ impl Room { pub fn new(room_id: &str, own_user_id: &str) -> Self { Room { room_id: room_id.to_string(), + room_name: RoomName::default(), own_user_id: own_user_id.to_owned(), creator: None, members: HashMap::new(), @@ -146,6 +206,50 @@ impl Room { } } + /// Add to the list of `RoomAliasId`s. + fn room_aliases(&mut self, alias: &RoomAliasId) -> bool { + self.room_name.push_alias(alias.clone()); + true + } + + /// RoomAliasId is `#alias:hostname` and `port` + fn canonical_alias(&mut self, alias: &RoomAliasId) -> bool { + self.room_name.set_canonical(alias.clone()); + true + } + + fn name_room(&mut self, name: &str) -> bool { + self.room_name.set_name(name); + true + } + + /// Handle a room.aliases event, updating the room state if necessary. + /// Returns true if the room name changed, false otherwise. + pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { + match event.content.aliases.as_slice() { + [alias] => self.room_aliases(alias), + _ => false, + } + } + + /// Handle a room.canonical_alias event, updating the room state if necessary. + /// Returns true if the room name changed, false otherwise. + pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { + match &event.content.alias { + Some(name) => self.canonical_alias(&name), + _ => false, + } + } + + /// Handle a room.name event, updating the room state if necessary. + /// Returns true if the room name changed, false otherwise. + pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { + match event.content.name() { + Some(name) => self.name_room(name), + _ => false, + } + } + /// Receive a timeline event for this room and update the room state. /// /// Returns true if the joined member list changed, false otherwise. @@ -156,6 +260,9 @@ impl Room { pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool { match event { RoomEvent::RoomMember(m) => self.handle_membership(m), + RoomEvent::RoomName(n) => self.handle_room_name(n), + RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), + RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), _ => false, } } @@ -170,6 +277,9 @@ impl Room { pub fn receive_state_event(&mut self, event: &StateEvent) -> bool { match event { StateEvent::RoomMember(m) => self.handle_membership(m), + StateEvent::RoomName(n) => self.handle_room_name(n), + StateEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), + StateEvent::RoomAliases(a) => self.handle_room_aliases(a), _ => false, } } From a29ae2a62e2b9ae0673cc286ce3dd8256f898c32 Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 17:22:11 -0400 Subject: [PATCH 02/28] add test, AsyncClient room name methods --- src/async_client.rs | 19 ++- src/base_client.rs | 18 ++- tests/async_client_tests.rs | 33 ++++++ tests/data/timeline.json | 226 ++++++++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 4 deletions(-) create mode 100644 tests/data/timeline.json diff --git a/src/async_client.rs b/src/async_client.rs index f74e469e..00bb7e9b 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -14,11 +14,12 @@ // limitations under the License. use futures::future::{BoxFuture, Future, FutureExt}; +use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock as SyncLock}; use std::time::{Duration, Instant}; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, RwLockReadGuard}; use async_std::task::sleep; @@ -261,6 +262,22 @@ impl AsyncClient { &self.homeserver } + #[doc(hidden)] + /// Access to the underlying `BaseClient`. Used for testing and debugging so far. + pub async fn base_client(&self) -> RwLockReadGuard<'_, BaseClient> { + self.base_client.read().await + } + + /// Calculate the room name from a `RoomId`, returning a string. + pub async fn get_room_name(&self, room_id: &str) -> Option { + self.base_client.read().await.calculate_room_name(room_id) + } + + /// Calculate the room names this client knows about. + pub async fn get_room_names(&self) -> Vec { + self.base_client.read().await.calculate_room_names() + } + /// Add a callback that will be called every time the client receives a room /// event /// diff --git a/src/base_client.rs b/src/base_client.rs index a22c338b..0bb7c4fc 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -24,7 +24,7 @@ use crate::events::room::{ name::{NameEvent}, }; use crate::events::EventResult; -use crate::identifiers::{RoomAliasId}; +use crate::identifiers::RoomAliasId; use crate::session::Session; use std::sync::{Arc, RwLock}; @@ -99,7 +99,7 @@ impl RoomName { true } - pub fn calculate_name(&self, room_id: &RoomId, members: &HashMap) -> String { + pub fn calculate_name(&self, room_id: &str, members: &HashMap) -> String { // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 // the order in which we check for a name ^^ if let Some(name) = &self.name { @@ -113,9 +113,10 @@ impl RoomName { let mut names = members.values().flat_map(|m| m.display_name.clone()).take(3).collect::>(); if names.is_empty() { + // TODO implement the rest of matrix-js-sdk handling of room names format!("Room {}", room_id) } else { - // stablize order + // stabilize order names.sort(); names.join(", ").to_string() } @@ -351,6 +352,17 @@ impl Client { } } + pub(crate) fn calculate_room_name(&self, room_id: &str) -> Option { + self.joined_rooms.get(room_id) + .and_then(|r| r.read().map(|r| r.room_name.calculate_name(room_id, &r.members)).ok()) + } + + pub(crate) fn calculate_room_names(&self) -> Vec { + self.joined_rooms.iter() + .flat_map(|(id, room)| room.read().map(|r| r.room_name.calculate_name(id, &r.members)).ok()) + .collect() + } + fn get_or_create_room(&mut self, room_id: &str) -> &mut Arc> { #[allow(clippy::or_fun_call)] self.joined_rooms diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 3c1e1251..23c215bb 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -58,3 +58,36 @@ fn sync() { assert!(rt.block_on(client.sync_token()).is_some()); } + + +#[test] +fn timeline() { + let mut rt = Runtime::new().unwrap(); + + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:example.com").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ) + .with_status(200) + .with_body_from_file("tests/data/timeline.json") + .create(); + + let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); + + let sync_settings = SyncSettings::new().timeout(3000).unwrap(); + + let _response = rt.block_on(client.sync(sync_settings)).unwrap(); + + assert_eq!(vec!["tutorial"], rt.block_on(client.get_room_names())); + assert_eq!(Some("tutorial".into()), rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost"))); + + // rt.block_on(async { println!("{:#?}", &client.base_client().await.joined_rooms ) }); +} diff --git a/tests/data/timeline.json b/tests/data/timeline.json new file mode 100644 index 00000000..3b898e20 --- /dev/null +++ b/tests/data/timeline.json @@ -0,0 +1,226 @@ +{ + "device_one_time_keys_count": {}, + "next_batch": "s526_47314_0_7_1_1_1_11444_1", + "device_lists": { + "changed": [ + "@example:example.org" + ], + "left": [] + }, + + "rooms": { + "invite": {}, + "join": { + "!SVkFJHzfwvuaIEawgC:localhost": { + "account_data": { + "events": [] + }, + "ephemeral": { + "events": [ + { + "content": { + "$151680659217152dPKjd:localhost": { + "m.read": { + "@example:localhost": { + "ts": 1516809890615 + } + } + } + }, + "type": "m.receipt" + } + ] + }, + "state": { + "events": [ + { + "content": { + "join_rule": "public" + }, + "event_id": "$15139375514WsgmR:localhost", + "origin_server_ts": 1513937551539, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.join_rules", + "unsigned": { + "age": 7034220355 + } + }, + { + "content": { + "avatar_url": null, + "displayname": "example", + "membership": "join" + }, + "event_id": "$151800140517rfvjc:localhost", + "membership": "join", + "origin_server_ts": 1518001405556, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "m.room.member", + "unsigned": { + "age": 2970366338, + "replaces_state": "$151800111315tsynI:localhost" + } + }, + { + "content": { + "history_visibility": "shared" + }, + "event_id": "$15139375515VaJEY:localhost", + "origin_server_ts": 1513937551613, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.history_visibility", + "unsigned": { + "age": 7034220281 + } + }, + { + "content": { + "creator": "@example:localhost" + }, + "event_id": "$15139375510KUZHi:localhost", + "origin_server_ts": 1513937551203, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.create", + "unsigned": { + "age": 7034220691 + } + }, + { + "content": { + "aliases": [ + "#tutorial:localhost" + ] + }, + "event_id": "$15139375516NUgtD:localhost", + "origin_server_ts": 1513937551720, + "sender": "@example:localhost", + "state_key": "localhost", + "type": "m.room.aliases", + "unsigned": { + "age": 7034220174 + } + }, + { + "content": { + "topic": "\ud83d\ude00" + }, + "event_id": "$151957878228ssqrJ:localhost", + "origin_server_ts": 1519578782185, + "sender": "@example: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": { + "@example:localhost": 100 + }, + "users_default": 0 + }, + "event_id": "$15139375512JaHAW:localhost", + "origin_server_ts": 1513937551359, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.power_levels", + "unsigned": { + "age": 7034220535 + } + }, + { + "content": { + "alias": "#tutorial:localhost" + }, + "event_id": "$15139375513VdeRF:localhost", + "origin_server_ts": 1513937551461, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.canonical_alias", + "unsigned": { + "age": 7034220433 + } + }, + { + "content": { + "avatar_url": null, + "displayname": "example2", + "membership": "join" + }, + "event_id": "$152034824468gOeNB:localhost", + "membership": "join", + "origin_server_ts": 1520348244605, + "sender": "@example2:localhost", + "state_key": "@example2:localhost", + "type": "m.room.member", + "unsigned": { + "age": 623527289, + "prev_content": { + "membership": "leave" + }, + "prev_sender": "@example:localhost", + "replaces_state": "$152034819067QWJxM:localhost" + } + } + ] + }, + "timeline": { + "events": [ + { + "content": { + "body": "baba", + "format": "org.matrix.custom.html", + "formatted_body": "baba", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1520372800469, + "sender": "@example: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 82f8af4c08a45bb37df233b4e002d2f6c7f3d2bd Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 17:23:49 -0400 Subject: [PATCH 03/28] cargo fmt/clippy --- src/async_client.rs | 1 - src/base_client.rs | 30 +++++++++++++++++++++--------- tests/async_client_tests.rs | 6 ++++-- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 00bb7e9b..ba8e90cb 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -14,7 +14,6 @@ // limitations under the License. use futures::future::{BoxFuture, Future, FutureExt}; -use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock as SyncLock}; diff --git a/src/base_client.rs b/src/base_client.rs index 0bb7c4fc..54f60b0a 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -21,7 +21,7 @@ use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, member::{MemberEvent, MembershipState}, - name::{NameEvent}, + name::NameEvent, }; use crate::events::EventResult; use crate::identifiers::RoomAliasId; @@ -109,16 +109,20 @@ impl RoomName { } else if !self.aliases.is_empty() { self.aliases[0].alias().to_string() } else { - // TODO - let mut names = members.values().flat_map(|m| m.display_name.clone()).take(3).collect::>(); - + // TODO + let mut names = members + .values() + .flat_map(|m| m.display_name.clone()) + .take(3) + .collect::>(); + if names.is_empty() { // TODO implement the rest of matrix-js-sdk handling of room names format!("Room {}", room_id) } else { // stabilize order names.sort(); - names.join(", ").to_string() + names.join(", ") } } } @@ -353,13 +357,21 @@ impl Client { } pub(crate) fn calculate_room_name(&self, room_id: &str) -> Option { - self.joined_rooms.get(room_id) - .and_then(|r| r.read().map(|r| r.room_name.calculate_name(room_id, &r.members)).ok()) + self.joined_rooms.get(room_id).and_then(|r| { + r.read() + .map(|r| r.room_name.calculate_name(room_id, &r.members)) + .ok() + }) } pub(crate) fn calculate_room_names(&self) -> Vec { - self.joined_rooms.iter() - .flat_map(|(id, room)| room.read().map(|r| r.room_name.calculate_name(id, &r.members)).ok()) + self.joined_rooms + .iter() + .flat_map(|(id, room)| { + room.read() + .map(|r| r.room_name.calculate_name(id, &r.members)) + .ok() + }) .collect() } diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 23c215bb..863bb5c5 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -59,7 +59,6 @@ fn sync() { assert!(rt.block_on(client.sync_token()).is_some()); } - #[test] fn timeline() { let mut rt = Runtime::new().unwrap(); @@ -87,7 +86,10 @@ fn timeline() { let _response = rt.block_on(client.sync(sync_settings)).unwrap(); assert_eq!(vec!["tutorial"], rt.block_on(client.get_room_names())); - assert_eq!(Some("tutorial".into()), rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost"))); + assert_eq!( + Some("tutorial".into()), + rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost")) + ); // rt.block_on(async { println!("{:#?}", &client.base_client().await.joined_rooms ) }); } From 3df15b72eb17f95269d52f6979fcbea57f2ebf4d Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 17:26:10 -0400 Subject: [PATCH 04/28] use sync.json --- tests/async_client_tests.rs | 2 +- tests/data/timeline.json | 226 ------------------------------------ 2 files changed, 1 insertion(+), 227 deletions(-) delete mode 100644 tests/data/timeline.json diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 863bb5c5..0f4b425b 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -76,7 +76,7 @@ fn timeline() { Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), ) .with_status(200) - .with_body_from_file("tests/data/timeline.json") + .with_body_from_file("tests/data/sync.json") .create(); let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); diff --git a/tests/data/timeline.json b/tests/data/timeline.json deleted file mode 100644 index 3b898e20..00000000 --- a/tests/data/timeline.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "device_one_time_keys_count": {}, - "next_batch": "s526_47314_0_7_1_1_1_11444_1", - "device_lists": { - "changed": [ - "@example:example.org" - ], - "left": [] - }, - - "rooms": { - "invite": {}, - "join": { - "!SVkFJHzfwvuaIEawgC:localhost": { - "account_data": { - "events": [] - }, - "ephemeral": { - "events": [ - { - "content": { - "$151680659217152dPKjd:localhost": { - "m.read": { - "@example:localhost": { - "ts": 1516809890615 - } - } - } - }, - "type": "m.receipt" - } - ] - }, - "state": { - "events": [ - { - "content": { - "join_rule": "public" - }, - "event_id": "$15139375514WsgmR:localhost", - "origin_server_ts": 1513937551539, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.join_rules", - "unsigned": { - "age": 7034220355 - } - }, - { - "content": { - "avatar_url": null, - "displayname": "example", - "membership": "join" - }, - "event_id": "$151800140517rfvjc:localhost", - "membership": "join", - "origin_server_ts": 1518001405556, - "sender": "@example:localhost", - "state_key": "@example:localhost", - "type": "m.room.member", - "unsigned": { - "age": 2970366338, - "replaces_state": "$151800111315tsynI:localhost" - } - }, - { - "content": { - "history_visibility": "shared" - }, - "event_id": "$15139375515VaJEY:localhost", - "origin_server_ts": 1513937551613, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.history_visibility", - "unsigned": { - "age": 7034220281 - } - }, - { - "content": { - "creator": "@example:localhost" - }, - "event_id": "$15139375510KUZHi:localhost", - "origin_server_ts": 1513937551203, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.create", - "unsigned": { - "age": 7034220691 - } - }, - { - "content": { - "aliases": [ - "#tutorial:localhost" - ] - }, - "event_id": "$15139375516NUgtD:localhost", - "origin_server_ts": 1513937551720, - "sender": "@example:localhost", - "state_key": "localhost", - "type": "m.room.aliases", - "unsigned": { - "age": 7034220174 - } - }, - { - "content": { - "topic": "\ud83d\ude00" - }, - "event_id": "$151957878228ssqrJ:localhost", - "origin_server_ts": 1519578782185, - "sender": "@example: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": { - "@example:localhost": 100 - }, - "users_default": 0 - }, - "event_id": "$15139375512JaHAW:localhost", - "origin_server_ts": 1513937551359, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.power_levels", - "unsigned": { - "age": 7034220535 - } - }, - { - "content": { - "alias": "#tutorial:localhost" - }, - "event_id": "$15139375513VdeRF:localhost", - "origin_server_ts": 1513937551461, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.canonical_alias", - "unsigned": { - "age": 7034220433 - } - }, - { - "content": { - "avatar_url": null, - "displayname": "example2", - "membership": "join" - }, - "event_id": "$152034824468gOeNB:localhost", - "membership": "join", - "origin_server_ts": 1520348244605, - "sender": "@example2:localhost", - "state_key": "@example2:localhost", - "type": "m.room.member", - "unsigned": { - "age": 623527289, - "prev_content": { - "membership": "leave" - }, - "prev_sender": "@example:localhost", - "replaces_state": "$152034819067QWJxM:localhost" - } - } - ] - }, - "timeline": { - "events": [ - { - "content": { - "body": "baba", - "format": "org.matrix.custom.html", - "formatted_body": "baba", - "msgtype": "m.text" - }, - "event_id": "$152037280074GZeOm:localhost", - "origin_server_ts": 1520372800469, - "sender": "@example: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 c2022180aded4e1a51fd7147dcb44a4c78e281cc Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 20:17:06 -0400 Subject: [PATCH 05/28] fix slice match room_aliases --- src/async_client.rs | 6 +++--- src/base_client.rs | 5 +++++ tests/async_client_tests.rs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index ba8e90cb..a76ef571 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -263,10 +263,10 @@ impl AsyncClient { #[doc(hidden)] /// Access to the underlying `BaseClient`. Used for testing and debugging so far. - pub async fn base_client(&self) -> RwLockReadGuard<'_, BaseClient> { - self.base_client.read().await + pub fn base_client(&self) -> Arc> { + Arc::clone(&self.base_client) } - + /// Calculate the room name from a `RoomId`, returning a string. pub async fn get_room_name(&self, room_id: &str) -> Option { self.base_client.read().await.calculate_room_name(room_id) diff --git a/src/base_client.rs b/src/base_client.rs index 54f60b0a..6fce1abf 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -199,6 +199,7 @@ impl Room { } /// Handle a room.member updating the room state if necessary. + /// /// Returns true if the joined member list changed, false otherwise. pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { match event.content.membership { @@ -229,15 +230,18 @@ impl Room { } /// Handle a room.aliases event, updating the room state if necessary. + /// /// Returns true if the room name changed, false otherwise. pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { match event.content.aliases.as_slice() { [alias] => self.room_aliases(alias), + [alias, ..] => self.room_aliases(alias), _ => false, } } /// Handle a room.canonical_alias event, updating the room state if necessary. + /// /// Returns true if the room name changed, false otherwise. pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { match &event.content.alias { @@ -247,6 +251,7 @@ impl Room { } /// Handle a room.name event, updating the room state if necessary. + /// /// Returns true if the room name changed, false otherwise. pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { match event.content.name() { diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 0f4b425b..ecf9c2a1 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -91,5 +91,5 @@ fn timeline() { rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost")) ); - // rt.block_on(async { println!("{:#?}", &client.base_client().await.joined_rooms ) }); + // rt.block_on(async { println!("{:#?}", &client.base_client().read().await.joined_rooms ) }); } From 090600e6aab46bd27a29a70cbdcea656321f6cc9 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 06:58:30 -0400 Subject: [PATCH 06/28] restructure folders, add User, fill out RoomMember, handle prescence. --- examples/login.rs | 4 +- src/async_client.rs | 13 +- src/base_client.rs | 272 ++------------------------ src/event_emitter/mod.rs | 3 + src/lib.rs | 4 +- src/models/mod.rs | 197 +++++++++++++++++++ src/models/room.rs | 373 ++++++++++++++++++++++++++++++++++++ src/models/room_member.rs | 106 ++++++++++ src/models/room_state.rs | 0 src/models/user.rs | 77 ++++++++ tests/async_client_tests.rs | 2 +- tests/data/sync.json | 50 ++++- 12 files changed, 831 insertions(+), 270 deletions(-) create mode 100644 src/event_emitter/mod.rs create mode 100644 src/models/mod.rs create mode 100644 src/models/room.rs create mode 100644 src/models/room_member.rs create mode 100644 src/models/room_state.rs create mode 100644 src/models/user.rs diff --git a/examples/login.rs b/examples/login.rs index 8fda44ab..0ccb8d18 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -25,10 +25,10 @@ async fn async_cb(room: Arc>, event: Arc>) { .. }) = event { - let user = room.members.get(&sender.to_string()).unwrap(); + let member = room.members.get(&sender.to_string()).unwrap(); println!( "{}: {}", - user.display_name.as_ref().unwrap_or(&sender.to_string()), + member.user.display_name.as_ref().unwrap_or(&sender.to_string()), msg_body ); } diff --git a/src/async_client.rs b/src/async_client.rs index a76ef571..3e843752 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -37,7 +37,7 @@ use ruma_identifiers::RoomId; use crate::api; use crate::base_client::Client as BaseClient; -use crate::base_client::Room; +use crate::models::Room; use crate::error::{Error, InnerError}; use crate::session::Session; use crate::VERSION; @@ -310,10 +310,10 @@ impl AsyncClient { /// .. /// }) = event /// { - /// let user = room.members.get(&sender.to_string()).unwrap(); + /// let member = room.members.get(&sender.to_string()).unwrap(); /// println!( /// "{}: {}", - /// user.display_name.as_ref().unwrap_or(&sender.to_string()), + /// member.user.display_name.as_ref().unwrap_or(&sender.to_string()), /// msg_body /// ); /// } @@ -414,6 +414,13 @@ impl AsyncClient { client.receive_joined_timeline_event(&room_id, &event); } + for presence in &response.presence.events { + let mut client = self.base_client.write().await; + if let EventResult::Ok(e) = presence { + client.receive_presence_event(&room_id, e); + } + } + let event = Arc::new(event.clone()); let callbacks = { diff --git a/src/base_client.rs b/src/base_client.rs index 6fce1abf..43f70caf 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -23,9 +23,11 @@ use crate::events::room::{ member::{MemberEvent, MembershipState}, name::NameEvent, }; +use crate::events::presence::PresenceEvent; use crate::events::EventResult; use crate::identifiers::RoomAliasId; use crate::session::Session; +use crate::models::{Room}; use std::sync::{Arc, RwLock}; #[cfg(feature = "encryption")] @@ -40,261 +42,6 @@ pub type Token = String; pub type RoomId = String; pub type UserId = String; -#[derive(Debug, Default)] -/// `RoomName` allows the calculation of a text room name. -pub struct RoomName { - /// The displayed name of the room. - name: Option, - /// The canonical alias of the room ex. `#room-name:example.com` and port number. - canonical_alias: Option, - /// List of `RoomAliasId`s the room has been given. - aliases: Vec, -} - -#[derive(Debug)] -/// A Matrix room member. -pub struct RoomMember { - /// The unique mxid of the user. - pub user_id: UserId, - /// The human readable name of the user. - pub display_name: Option, - /// The matrix url of the users avatar. - pub avatar_url: Option, - /// The users power level. - pub power_level: u8, -} - -#[derive(Debug)] -/// A Matrix rooom. -pub struct Room { - /// The unique id of the room. - pub room_id: RoomId, - /// The name of the room, clients use this to represent a room. - pub room_name: RoomName, - /// The mxid of our own user. - pub own_user_id: UserId, - /// The mxid of the room creator. - pub creator: Option, - /// The map of room members. - pub members: HashMap, - /// A list of users that are currently typing. - pub typing_users: Vec, - /// A flag indicating if the room is encrypted. - pub encrypted: bool, -} - -impl RoomName { - pub fn push_alias(&mut self, alias: RoomAliasId) -> bool { - self.aliases.push(alias); - true - } - - pub fn set_canonical(&mut self, alias: RoomAliasId) -> bool { - self.canonical_alias = Some(alias); - true - } - - pub fn set_name(&mut self, name: &str) -> bool { - self.name = Some(name.to_string()); - true - } - - pub fn calculate_name(&self, room_id: &str, members: &HashMap) -> String { - // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 - // the order in which we check for a name ^^ - if let Some(name) = &self.name { - name.clone() - } else if let Some(alias) = &self.canonical_alias { - alias.alias().to_string() - } else if !self.aliases.is_empty() { - self.aliases[0].alias().to_string() - } else { - // TODO - let mut names = members - .values() - .flat_map(|m| m.display_name.clone()) - .take(3) - .collect::>(); - - if names.is_empty() { - // TODO implement the rest of matrix-js-sdk handling of room names - format!("Room {}", room_id) - } else { - // stabilize order - names.sort(); - names.join(", ") - } - } - } -} - -impl Room { - /// Create a new room. - /// - /// # Arguments - /// - /// * `room_id` - The unique id of the room. - /// - /// * `own_user_id` - The mxid of our own user. - pub fn new(room_id: &str, own_user_id: &str) -> Self { - Room { - room_id: room_id.to_string(), - room_name: RoomName::default(), - own_user_id: own_user_id.to_owned(), - creator: None, - members: HashMap::new(), - typing_users: Vec::new(), - encrypted: false, - } - } - - fn add_member(&mut self, event: &MemberEvent) -> bool { - if self.members.contains_key(&event.state_key) { - return false; - } - - let member = RoomMember { - user_id: event.state_key.clone(), - display_name: event.content.displayname.clone(), - avatar_url: event.content.avatar_url.clone(), - power_level: 0, - }; - - self.members.insert(event.state_key.clone(), member); - - true - } - - fn remove_member(&mut self, event: &MemberEvent) -> bool { - if !self.members.contains_key(&event.state_key) { - return false; - } - - true - } - - fn update_joined_member(&mut self, event: &MemberEvent) -> bool { - if let Some(member) = self.members.get_mut(&event.state_key) { - member.display_name = event.content.displayname.clone(); - member.avatar_url = event.content.avatar_url.clone(); - } - - false - } - - fn handle_join(&mut self, event: &MemberEvent) -> bool { - match &event.prev_content { - Some(c) => match c.membership { - MembershipState::Join => self.update_joined_member(event), - MembershipState::Invite => self.add_member(event), - MembershipState::Leave => self.remove_member(event), - _ => false, - }, - None => self.add_member(event), - } - } - - fn handle_leave(&mut self, _event: &MemberEvent) -> bool { - false - } - - /// Handle a room.member updating the room state if necessary. - /// - /// Returns true if the joined member list changed, false otherwise. - pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { - match event.content.membership { - MembershipState::Join => self.handle_join(event), - MembershipState::Leave => self.handle_leave(event), - MembershipState::Ban => self.handle_leave(event), - MembershipState::Invite => false, - MembershipState::Knock => false, - _ => false, - } - } - - /// Add to the list of `RoomAliasId`s. - fn room_aliases(&mut self, alias: &RoomAliasId) -> bool { - self.room_name.push_alias(alias.clone()); - true - } - - /// RoomAliasId is `#alias:hostname` and `port` - fn canonical_alias(&mut self, alias: &RoomAliasId) -> bool { - self.room_name.set_canonical(alias.clone()); - true - } - - fn name_room(&mut self, name: &str) -> bool { - self.room_name.set_name(name); - true - } - - /// Handle a room.aliases event, updating the room state if necessary. - /// - /// Returns true if the room name changed, false otherwise. - pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { - match event.content.aliases.as_slice() { - [alias] => self.room_aliases(alias), - [alias, ..] => self.room_aliases(alias), - _ => false, - } - } - - /// Handle a room.canonical_alias event, updating the room state if necessary. - /// - /// Returns true if the room name changed, false otherwise. - pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { - match &event.content.alias { - Some(name) => self.canonical_alias(&name), - _ => false, - } - } - - /// Handle a room.name event, updating the room state if necessary. - /// - /// Returns true if the room name changed, false otherwise. - pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { - match event.content.name() { - Some(name) => self.name_room(name), - _ => false, - } - } - - /// Receive a timeline event for this room and update the room state. - /// - /// Returns true if the joined member list changed, false otherwise. - /// - /// # Arguments - /// - /// * `event` - The event of the room. - pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool { - match event { - RoomEvent::RoomMember(m) => self.handle_membership(m), - RoomEvent::RoomName(n) => self.handle_room_name(n), - RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), - RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), - _ => false, - } - } - - /// Receive a state event for this room and update the room state. - /// - /// Returns true if the joined member list changed, false otherwise. - /// - /// # Arguments - /// - /// * `event` - The event of the room. - pub fn receive_state_event(&mut self, event: &StateEvent) -> bool { - match event { - StateEvent::RoomMember(m) => self.handle_membership(m), - StateEvent::RoomName(n) => self.handle_room_name(n), - StateEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), - StateEvent::RoomAliases(a) => self.handle_room_aliases(a), - _ => false, - } - } -} - #[derive(Debug)] /// A no IO Client implementation. /// @@ -434,6 +181,21 @@ impl Client { room.receive_state_event(event) } + /// Receive a presence event from an `IncomingResponse` and updates the client state. + /// + /// Returns true if the membership list of the room changed, false + /// otherwise. + /// + /// # Arguments + /// + /// * `room_id` - The unique id of the room the event belongs to. + /// + /// * `event` - The event that should be handled by the client. + pub fn receive_presence_event(&mut self, room_id: &str, event: &PresenceEvent) -> bool { + let mut room = self.get_or_create_room(room_id).write().unwrap(); + room.receive_presence_event(event) + } + /// Receive a response from a sync call. /// /// # Arguments diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs new file mode 100644 index 00000000..a383ea2f --- /dev/null +++ b/src/event_emitter/mod.rs @@ -0,0 +1,3 @@ +pub trait EventEmitter { + fn on_room_name() {} +} diff --git a/src/lib.rs b/src/lib.rs index d7e33f71..5adfc537 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,12 +25,14 @@ pub use ruma_identifiers as identifiers; mod async_client; mod base_client; mod error; +mod models; mod session; #[cfg(feature = "encryption")] mod crypto; pub use async_client::{AsyncClient, AsyncClientConfig, SyncSettings}; -pub use base_client::{Client, Room}; +pub use base_client::Client; +pub use models::Room; pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 00000000..4e2d3307 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,197 @@ +use crate::events::{ + call::{ + answer::AnswerEvent, candidates::CandidatesEvent, hangup::HangupEvent, invite::InviteEvent, + }, + direct::DirectEvent, + dummy::DummyEvent, + forwarded_room_key::ForwardedRoomKeyEvent, + fully_read::FullyReadEvent, + ignored_user_list::IgnoredUserListEvent, + key::verification::{ + accept::AcceptEvent, cancel::CancelEvent, key::KeyEvent, mac::MacEvent, + request::RequestEvent, start::StartEvent, + }, + presence::PresenceEvent, + push_rules::PushRulesEvent, + receipt::ReceiptEvent, + room::{ + aliases::AliasesEvent, + avatar::AvatarEvent, + canonical_alias::CanonicalAliasEvent, + create::CreateEvent, + encrypted::EncryptedEvent, + encryption::EncryptionEvent, + guest_access::GuestAccessEvent, + history_visibility::HistoryVisibilityEvent, + join_rules::JoinRulesEvent, + member::MemberEvent, + message::{feedback::FeedbackEvent, MessageEvent}, + name::NameEvent, + pinned_events::PinnedEventsEvent, + power_levels::PowerLevelsEvent, + redaction::RedactionEvent, + server_acl::ServerAclEvent, + third_party_invite::ThirdPartyInviteEvent, + tombstone::TombstoneEvent, + topic::TopicEvent, + }, + room_key::RoomKeyEvent, + room_key_request::RoomKeyRequestEvent, + sticker::StickerEvent, + tag::TagEvent, + typing::TypingEvent, + CustomEvent, CustomRoomEvent, CustomStateEvent, +}; + +mod room_member; +mod room_state; +mod room; +mod user; + +pub use room::{Room, RoomName}; +pub use room_member::RoomMember; +pub use user::User; + +pub type Token = String; +pub type RoomId = String; +pub type UserId = String; + +pub enum EventWrapper<'ev> { + /// m.call.answer + CallAnswer(&'ev AnswerEvent), + + /// m.call.candidates + CallCandidates(&'ev CandidatesEvent), + + /// m.call.hangup + CallHangup(&'ev HangupEvent), + + /// m.call.invite + CallInvite(&'ev InviteEvent), + + /// m.direct + Direct(&'ev DirectEvent), + + /// m.dummy + Dummy(&'ev DummyEvent), + + /// m.forwarded_room_key + ForwardedRoomKey(&'ev ForwardedRoomKeyEvent), + + /// m.fully_read + FullyRead(&'ev FullyReadEvent), + + /// m.ignored_user_list + IgnoredUserList(&'ev IgnoredUserListEvent), + + /// m.key.verification.accept + KeyVerificationAccept(&'ev AcceptEvent), + + /// m.key.verification.cancel + KeyVerificationCancel(&'ev CancelEvent), + + /// m.key.verification.key + KeyVerificationKey(&'ev KeyEvent), + + /// m.key.verification.mac + KeyVerificationMac(&'ev MacEvent), + + /// m.key.verification.request + KeyVerificationRequest(&'ev RequestEvent), + + /// m.key.verification.start + KeyVerificationStart(&'ev StartEvent), + + /// m.presence + Presence(&'ev PresenceEvent), + + /// m.push_rules + PushRules(&'ev PushRulesEvent), + + /// m.receipt + Receipt(&'ev ReceiptEvent), + + /// m.room.aliases + RoomAliases(&'ev AliasesEvent), + + /// m.room.avatar + RoomAvatar(&'ev AvatarEvent), + + /// m.room.canonical_alias + RoomCanonicalAlias(&'ev CanonicalAliasEvent), + + /// m.room.create + RoomCreate(&'ev CreateEvent), + + /// m.room.encrypted + RoomEncrypted(&'ev EncryptedEvent), + + /// m.room.encryption + RoomEncryption(&'ev EncryptionEvent), + + /// m.room.guest_access + RoomGuestAccess(&'ev GuestAccessEvent), + + /// m.room.history_visibility + RoomHistoryVisibility(&'ev HistoryVisibilityEvent), + + /// m.room.join_rules + RoomJoinRules(&'ev JoinRulesEvent), + + /// m.room.member + RoomMember(&'ev MemberEvent), + + /// m.room.message + RoomMessage(&'ev MessageEvent), + + /// m.room.message.feedback + RoomMessageFeedback(&'ev FeedbackEvent), + + /// m.room.name + RoomName(&'ev NameEvent), + + /// m.room.pinned_events + RoomPinnedEvents(&'ev PinnedEventsEvent), + + /// m.room.power_levels + RoomPowerLevels(&'ev PowerLevelsEvent), + + /// m.room.redaction + RoomRedaction(&'ev RedactionEvent), + + /// m.room.server_acl + RoomServerAcl(&'ev ServerAclEvent), + + /// m.room.third_party_invite + RoomThirdPartyInvite(&'ev ThirdPartyInviteEvent), + + /// m.room.tombstone + RoomTombstone(&'ev TombstoneEvent), + + /// m.room.topic + RoomTopic(&'ev TopicEvent), + + /// m.room_key + RoomKey(&'ev RoomKeyEvent), + + /// m.room_key_request + RoomKeyRequest(&'ev RoomKeyRequestEvent), + + /// m.sticker + Sticker(&'ev StickerEvent), + + /// m.tag + Tag(&'ev TagEvent), + + /// m.typing + Typing(&'ev TypingEvent), + + /// Any basic event that is not part of the specification. + Custom(&'ev CustomEvent), + + /// Any room event that is not part of the specification. + CustomRoom(&'ev CustomRoomEvent), + + /// Any state event that is not part of the specification. + CustomState(&'ev CustomStateEvent), +} diff --git a/src/models/room.rs b/src/models/room.rs new file mode 100644 index 00000000..4a69a366 --- /dev/null +++ b/src/models/room.rs @@ -0,0 +1,373 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::r0 as api; +use crate::events::collections::all::{RoomEvent, StateEvent}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MembershipState}, + name::NameEvent, +}; +use crate::events::{presence::{PresenceEvent, PresenceEventContent}, EventResult}; +use crate::identifiers::RoomAliasId; +use crate::session::Session; +use super::{RoomId, UserId, RoomMember, User}; + +#[cfg(feature = "encryption")] +use tokio::sync::Mutex; + +#[cfg(feature = "encryption")] +use crate::crypto::{OlmMachine, OneTimeKeys}; +#[cfg(feature = "encryption")] +use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; + +#[derive(Debug, Default)] +/// `RoomName` allows the calculation of a text room name. +pub struct RoomName { + /// The displayed name of the room. + name: Option, + /// The canonical alias of the room ex. `#room-name:example.com` and port number. + canonical_alias: Option, + /// List of `RoomAliasId`s the room has been given. + aliases: Vec, +} + +#[derive(Debug)] +/// A Matrix rooom. +pub struct Room { + /// The unique id of the room. + pub room_id: RoomId, + /// The name of the room, clients use this to represent a room. + pub room_name: RoomName, + /// The mxid of our own user. + pub own_user_id: UserId, + /// The mxid of the room creator. + pub creator: Option, + /// The map of room members. + pub members: HashMap, + /// A list of users that are currently typing. + pub typing_users: Vec, + /// A flag indicating if the room is encrypted. + pub encrypted: bool, +} + +impl RoomName { + pub fn push_alias(&mut self, alias: RoomAliasId) -> bool { + self.aliases.push(alias); + true + } + + pub fn set_canonical(&mut self, alias: RoomAliasId) -> bool { + self.canonical_alias = Some(alias); + true + } + + pub fn set_name(&mut self, name: &str) -> bool { + self.name = Some(name.to_string()); + true + } + + pub fn calculate_name(&self, room_id: &str, members: &HashMap) -> String { + // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 + // the order in which we check for a name ^^ + if let Some(name) = &self.name { + name.clone() + } else if let Some(alias) = &self.canonical_alias { + alias.alias().to_string() + } else if !self.aliases.is_empty() { + self.aliases[0].alias().to_string() + } else { + // TODO + let mut names = members + .values() + .flat_map(|m| m.user.display_name.clone()) + .take(3) + .collect::>(); + + if names.is_empty() { + // TODO implement the rest of matrix-js-sdk handling of room names + format!("Room {}", room_id) + } else { + // stabilize order + names.sort(); + names.join(", ") + } + } + } +} + +impl Room { + /// Create a new room. + /// + /// # Arguments + /// + /// * `room_id` - The unique id of the room. + /// + /// * `own_user_id` - The mxid of our own user. + pub fn new(room_id: &str, own_user_id: &str) -> Self { + Room { + room_id: room_id.to_string(), + room_name: RoomName::default(), + own_user_id: own_user_id.to_owned(), + creator: None, + members: HashMap::new(), + typing_users: Vec::new(), + encrypted: false, + } + } + + fn add_member(&mut self, event: &MemberEvent) -> bool { + if self.members.contains_key(&event.state_key) { + return false; + } + + let member = RoomMember::new(event); + + self.members.insert(event.state_key.clone(), member); + + true + } + + // fn remove_member(&mut self, event: &MemberEvent) -> bool { + // if let Some(member) = self.members.get_mut(&event.sender.to_string()) { + // let changed = member.membership == event.content.membership; + // member.membership = event.content.membership; + // changed + // } else { + // false + // } + // } + + // fn update_joined_member(&mut self, event: &MemberEvent) -> bool { + // if let Some(member) = self.members.get_mut(&event.state_key) { + // member.update(event); + // } + + // false + // } + + // fn handle_join(&mut self, event: &MemberEvent) -> bool { + // match &event.prev_content { + // Some(c) => match c.membership { + // MembershipState::Join => self.update_joined_member(event), + // MembershipState::Invite => self.add_member(event), + // MembershipState::Leave => self.remove_member(event), + // _ => false, + // }, + // None => self.add_member(event), + // } + // } + + // fn handle_leave(&mut self, event: &MemberEvent) -> bool { + + // } + + /// Handle a room.member updating the room state if necessary. + /// + /// Returns true if the joined member list changed, false otherwise. + pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { + match &event.content.membership { + MembershipState::Invite | MembershipState::Join => self.add_member(event), + _ => { + if let Some(member) = self.members.get_mut(&event.sender.to_string()) { + let changed = member.membership == event.content.membership; + member.membership = event.content.membership; + changed + } else { + false + } + } + } + } + + /// Add to the list of `RoomAliasId`s. + fn room_aliases(&mut self, alias: &RoomAliasId) -> bool { + self.room_name.push_alias(alias.clone()); + true + } + + /// RoomAliasId is `#alias:hostname` and `port` + fn canonical_alias(&mut self, alias: &RoomAliasId) -> bool { + self.room_name.set_canonical(alias.clone()); + true + } + + fn name_room(&mut self, name: &str) -> bool { + self.room_name.set_name(name); + true + } + + /// Handle a room.aliases event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { + match event.content.aliases.as_slice() { + [alias] => self.room_aliases(alias), + [alias, ..] => self.room_aliases(alias), + _ => false, + } + } + + /// Handle a room.canonical_alias event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { + match &event.content.alias { + Some(name) => self.canonical_alias(&name), + _ => false, + } + } + + /// Handle a room.name event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { + match event.content.name() { + Some(name) => self.name_room(name), + _ => false, + } + } + + /// Receive a timeline event for this room and update the room state. + /// + /// Returns true if the joined member list changed, false otherwise. + /// + /// # Arguments + /// + /// * `event` - The event of the room. + pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool { + match event { + // update to the current members of the room + RoomEvent::RoomMember(m) => self.handle_membership(m), + // finds all events related to the name of the room for later calculation + RoomEvent::RoomName(n) => self.handle_room_name(n), + RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), + RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), + // power levels of the room members + RoomEvent::RoomPowerLevels(p) => self.handle_power_level(p), + _ => false, + } + } + + /// Receive a state event for this room and update the room state. + /// + /// Returns true if the joined member list changed, false otherwise. + /// + /// # Arguments + /// + /// * `event` - The event of the room. + pub fn receive_state_event(&mut self, event: &StateEvent) -> bool { + match event { + StateEvent::RoomMember(m) => self.handle_membership(m), + StateEvent::RoomName(n) => self.handle_room_name(n), + StateEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), + StateEvent::RoomAliases(a) => self.handle_room_aliases(a), + _ => false, + } + } + + /// Receive a presence event from an `IncomingResponse` and updates the client state. + /// + /// Returns true if the joined member list changed, false otherwise. + /// + /// # Arguments + /// + /// * `event` - The event of the room. + pub fn receive_presence_event(&mut self, event: &PresenceEvent) -> bool { + let PresenceEvent { + content: PresenceEventContent { + avatar_url, + currently_active, + displayname, + last_active_ago, + presence, + status_msg, + }, + sender, + } = event; + + if let Some(user) = self.members.get_mut(&sender.to_string()).map(|m| &mut m.user) { + if user.display_name == *displayname && user.avatar_url == *avatar_url + && user.presence.as_ref() == Some(presence) && user.status_msg == *status_msg + && user.last_active_ago == *last_active_ago && user.currently_active == *currently_active + { + false + } else { + user.presence_events.push(event.clone()); + *user = User { + display_name: displayname.clone(), + avatar_url: avatar_url.clone(), + presence: Some(presence.clone()), + status_msg: status_msg.clone(), + last_active_ago: *last_active_ago, + currently_active: *currently_active, + // TODO better way of moving vec over + events: user.events.clone(), + presence_events: user.presence_events.clone(), + }; + true + } + } else { + // this is probably an error as we have a `PresenceEvent` for a user + // we dont know about + false + } + } +} + +// pub struct User { +// /// The human readable name of the user. +// pub display_name: Option, +// /// The matrix url of the users avatar. +// pub avatar_url: Option, +// /// The presence of the user, if found. +// pub presence: Option, +// /// The presence status message, if found. +// pub status_msg: Option, +// /// The time, in ms, since the user interacted with the server. +// pub last_active_ago: Option, +// /// If the user should be considered active. +// pub currently_active: Option, +// /// The events that created the state of the current user. +// // TODO do we want to hold the whole state or just update our structures. +// pub events: Vec, +// /// The `PresenceEvent`s connected to this user. +// pub presence_events: Vec, +// } + +// pub struct RoomMember { +// /// The unique mxid of the user. +// pub user_id: UserId, +// /// The unique id of the room. +// pub room_id: Option, +// /// If the member is typing. +// pub typing: Option, +// /// The user data for this room member. +// pub user: User, +// /// The users power level. +// pub power_level: Option, +// /// The normalized power level of this `RoomMember` (0-100). +// pub power_level_norm: Option, +// /// The `MembershipState` of this `RoomMember`. +// pub membership: MembershipState, +// /// The human readable name of this room member. +// pub name: String, +// /// The events that created the state of this room member. +// pub events: Vec +// } diff --git a/src/models/room_member.rs b/src/models/room_member.rs new file mode 100644 index 00000000..c8f67ef8 --- /dev/null +++ b/src/models/room_member.rs @@ -0,0 +1,106 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::r0 as api; +use crate::events::collections::all::{Event, RoomEvent, StateEvent}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MemberEventContent, MembershipState}, + name::NameEvent, +}; +use crate::events::EventResult; +use crate::identifiers::RoomAliasId; +use crate::session::Session; +use super::{UserId, RoomId, User}; + +use js_int::{Int, UInt}; +#[cfg(feature = "encryption")] +use tokio::sync::Mutex; + +#[cfg(feature = "encryption")] +use crate::crypto::{OlmMachine, OneTimeKeys}; +#[cfg(feature = "encryption")] +use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; + +#[derive(Debug)] +/// A Matrix room member. +pub struct RoomMember { + /// The unique mxid of the user. + pub user_id: UserId, + /// The unique id of the room. + pub room_id: Option, + /// If the member is typing. + pub typing: Option, + /// The user data for this room member. + pub user: User, + /// The users power level. + pub power_level: Option, + /// The normalized power level of this `RoomMember` (0-100). + pub power_level_norm: Option, + /// The `MembershipState` of this `RoomMember`. + pub membership: MembershipState, + /// The human readable name of this room member. + pub name: String, + /// The events that created the state of this room member. + pub events: Vec +} + +impl RoomMember { + pub fn new(event: &MemberEvent) -> Self { + let user = User::new(event); + Self { + room_id: event.room_id.as_ref().map(|id| id.to_string()), + user_id: event.state_key.clone(), + typing: None, + user, + power_level: None, + power_level_norm: None, + membership: event.content.membership, + name: event.state_key.clone(), + events: vec![Event::RoomMember(event.clone())] + } + } + + pub fn update(&mut self, event: &MemberEvent) { + let MemberEvent { + content: MemberEventContent { + membership, + .. + }, + room_id, + state_key, + .. + } = event; + + let mut events = Vec::new(); + events.extend(self.events.drain(..).chain(Some(Event::RoomMember(event.clone())))); + + *self = Self { + room_id: room_id.as_ref().map(|id| id.to_string()).or(self.room_id.take()), + user_id: state_key.clone(), + typing: None, + user: User::new(event), + power_level: None, + power_level_norm: None, + membership: membership.clone(), + name: state_key.clone(), + events, + } + } +} diff --git a/src/models/room_state.rs b/src/models/room_state.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 00000000..060ec88c --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,77 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::r0 as api; +use crate::events::collections::all::{Event, RoomEvent, StateEvent}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MembershipState}, + name::NameEvent, +}; +use crate::events::presence::{PresenceEvent, PresenceState}; +use crate::events::EventResult; +use crate::identifiers::RoomAliasId; +use crate::session::Session; +use super::{UserId, RoomId}; + +use js_int::UInt; +#[cfg(feature = "encryption")] +use tokio::sync::Mutex; + +#[cfg(feature = "encryption")] +use crate::crypto::{OlmMachine, OneTimeKeys}; +#[cfg(feature = "encryption")] +use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; + +#[derive(Debug)] +/// A Matrix room member. +pub struct User { + /// The human readable name of the user. + pub display_name: Option, + /// The matrix url of the users avatar. + pub avatar_url: Option, + /// The presence of the user, if found. + pub presence: Option, + /// The presence status message, if found. + pub status_msg: Option, + /// The time, in ms, since the user interacted with the server. + pub last_active_ago: Option, + /// If the user should be considered active. + pub currently_active: Option, + /// The events that created the state of the current user. + // TODO do we want to hold the whole state or just update our structures. + pub events: Vec, + /// The `PresenceEvent`s connected to this user. + pub presence_events: Vec, +} + +impl User { + pub fn new(event: &MemberEvent) -> Self { + Self { + display_name: event.content.displayname.clone(), + avatar_url: event.content.avatar_url.clone(), + presence: None, + status_msg: None, + last_active_ago: None, + currently_active: None, + events: Vec::default(), + presence_events: Vec::default(), + } + } +} diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index ecf9c2a1..89768fa6 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -91,5 +91,5 @@ fn timeline() { rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost")) ); - // rt.block_on(async { println!("{:#?}", &client.base_client().read().await.joined_rooms ) }); + rt.block_on(async { println!("{:#?}", &client.base_client().read().await.joined_rooms ) }); } diff --git a/tests/data/sync.json b/tests/data/sync.json index 3b898e20..d873fb47 100644 --- a/tests/data/sync.json +++ b/tests/data/sync.json @@ -2,12 +2,11 @@ "device_one_time_keys_count": {}, "next_batch": "s526_47314_0_7_1_1_1_11444_1", "device_lists": { - "changed": [ - "@example:example.org" - ], - "left": [] + "changed": [ + "@example:example.org" + ], + "left": [] }, - "rooms": { "invite": {}, "join": { @@ -184,7 +183,31 @@ "prev_sender": "@example:localhost", "replaces_state": "$152034819067QWJxM:localhost" } - } + }, + { + "content": { + "membership": "leave", + "reason": "offline", + "avatar_url": "avatar.com", + "displayname": "example" + }, + "event_id": "$1585345508297748AIUBh:matrix.org", + "origin_server_ts": 1585345508223, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "m.room.member", + "unsigned": { + "replaces_state": "$1585345354296486IGZfp:localhost", + "prev_content": { + "avatar_url": "avatar.com", + "displayname": "example", + "membership": "join" + }, + "prev_sender": "@example2:localhost", + "age": 6992 + }, + "room_id": "!roomid:room.com" + } ] }, "timeline": { @@ -219,8 +242,19 @@ "to_device": { "events": [] }, - "presence": { - "events": [] + "events": [ + { + "content": { + "avatar_url": "mxc://localhost:wefuiwegh8742w", + "currently_active": false, + "last_active_ago": 2478593, + "presence": "online", + "status_msg": "Making cupcakes" + }, + "sender": "@example:localhost", + "type": "m.presence" + } + ] } } From e8b5534a713279466256e92c695a44d30d4bb883 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 07:07:50 -0400 Subject: [PATCH 07/28] start handle_power_levels --- src/event_emitter/mod.rs | 43 +++++++++++++++++++++++++++++++++++++++- src/lib.rs | 1 + 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs index a383ea2f..7bce3fc1 100644 --- a/src/event_emitter/mod.rs +++ b/src/event_emitter/mod.rs @@ -1,3 +1,44 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::r0 as api; +use crate::events::collections::all::{Event, RoomEvent, StateEvent}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MemberEventContent, MembershipState}, + name::NameEvent, +}; +use crate::events::EventResult; +use crate::identifiers::RoomAliasId; +use crate::session::Session; +use crate::models::Room; + +use js_int::{Int, UInt}; +#[cfg(feature = "encryption")] +use tokio::sync::Mutex; + +#[cfg(feature = "encryption")] +use crate::crypto::{OlmMachine, OneTimeKeys}; +#[cfg(feature = "encryption")] +use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; + pub trait EventEmitter { - fn on_room_name() {} + fn on_room_name(&mut self, _: &Room) {} + fn on_room_member(&mut self, _: &Room) {} } diff --git a/src/lib.rs b/src/lib.rs index 5adfc537..47053fb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ mod base_client; mod error; mod models; mod session; +mod event_emitter; #[cfg(feature = "encryption")] mod crypto; From 011e77cf4b57b44feb1789706f664838ce91f6c8 Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 16:14:16 -0400 Subject: [PATCH 08/28] calculate room name internal `Room` method --- src/base_client.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/src/base_client.rs b/src/base_client.rs index 8d3a0fae..34ef5d6c 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -21,8 +21,14 @@ use std::result::Result as StdResult; use crate::api::r0 as api; use crate::error::Result; use crate::events::collections::all::{RoomEvent, StateEvent}; -use crate::events::room::member::{MemberEvent, MembershipState}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MembershipState}, + name::{NameEvent}, +}; use crate::events::EventResult; +use crate::identifiers::{RoomAliasId}; use crate::session::Session; use std::sync::{Arc, RwLock}; @@ -38,6 +44,17 @@ use ruma_identifiers::RoomId; pub type Token = String; pub type UserId = String; +#[derive(Debug, Default)] +/// `RoomName` allows the calculation of a text room name. +pub struct RoomName { + /// The displayed name of the room. + name: Option, + /// The canonical alias of the room ex. `#room-name:example.com` and port number. + canonical_alias: Option, + /// List of `RoomAliasId`s the room has been given. + aliases: Vec, +} + #[derive(Debug)] /// A Matrix room member. pub struct RoomMember { @@ -55,7 +72,9 @@ pub struct RoomMember { /// A Matrix rooom. pub struct Room { /// The unique id of the room. - pub room_id: String, + pub room_id: RoomId, + /// The name of the room, clients use this to represent a room. + pub room_name: RoomName, /// The mxid of our own user. pub own_user_id: UserId, /// The mxid of the room creator. @@ -68,6 +87,46 @@ pub struct Room { pub encrypted: bool, } +impl RoomName { + pub fn push_alias(&mut self, alias: RoomAliasId) -> bool { + self.aliases.push(alias); + true + } + + pub fn set_canonical(&mut self, alias: RoomAliasId) -> bool { + self.canonical_alias = Some(alias); + true + } + + pub fn set_name(&mut self, name: &str) -> bool { + self.name = Some(name.to_string()); + true + } + + pub fn calculate_name(&self, room_id: &RoomId, members: &HashMap) -> String { + // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 + // the order in which we check for a name ^^ + if let Some(name) = &self.name { + name.clone() + } else if let Some(alias) = &self.canonical_alias { + alias.alias().to_string() + } else if !self.aliases.is_empty() { + self.aliases[0].alias().to_string() + } else { + // TODO + let mut names = members.values().flat_map(|m| m.display_name.clone()).take(3).collect::>(); + + if names.is_empty() { + format!("Room {}", room_id) + } else { + // stablize order + names.sort(); + names.join(", ").to_string() + } + } + } +} + impl Room { /// Create a new room. /// @@ -79,6 +138,7 @@ impl Room { pub fn new(room_id: &str, own_user_id: &str) -> Self { Room { room_id: room_id.to_string(), + room_name: RoomName::default(), own_user_id: own_user_id.to_owned(), creator: None, members: HashMap::new(), @@ -150,6 +210,50 @@ impl Room { } } + /// Add to the list of `RoomAliasId`s. + fn room_aliases(&mut self, alias: &RoomAliasId) -> bool { + self.room_name.push_alias(alias.clone()); + true + } + + /// RoomAliasId is `#alias:hostname` and `port` + fn canonical_alias(&mut self, alias: &RoomAliasId) -> bool { + self.room_name.set_canonical(alias.clone()); + true + } + + fn name_room(&mut self, name: &str) -> bool { + self.room_name.set_name(name); + true + } + + /// Handle a room.aliases event, updating the room state if necessary. + /// Returns true if the room name changed, false otherwise. + pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { + match event.content.aliases.as_slice() { + [alias] => self.room_aliases(alias), + _ => false, + } + } + + /// Handle a room.canonical_alias event, updating the room state if necessary. + /// Returns true if the room name changed, false otherwise. + pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { + match &event.content.alias { + Some(name) => self.canonical_alias(&name), + _ => false, + } + } + + /// Handle a room.name event, updating the room state if necessary. + /// Returns true if the room name changed, false otherwise. + pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { + match event.content.name() { + Some(name) => self.name_room(name), + _ => false, + } + } + /// Receive a timeline event for this room and update the room state. /// /// Returns true if the joined member list changed, false otherwise. @@ -160,6 +264,9 @@ impl Room { pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool { match event { RoomEvent::RoomMember(m) => self.handle_membership(m), + RoomEvent::RoomName(n) => self.handle_room_name(n), + RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), + RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), _ => false, } } @@ -174,6 +281,9 @@ impl Room { pub fn receive_state_event(&mut self, event: &StateEvent) -> bool { match event { StateEvent::RoomMember(m) => self.handle_membership(m), + StateEvent::RoomName(n) => self.handle_room_name(n), + StateEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), + StateEvent::RoomAliases(a) => self.handle_room_aliases(a), _ => false, } } From bcdd81dc8cb06f612f4f36104badb4cfa655c293 Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 17:22:11 -0400 Subject: [PATCH 09/28] add test, AsyncClient room name methods --- src/async_client.rs | 17 +++ src/base_client.rs | 20 +++- tests/async_client_tests.rs | 33 ++++++ tests/data/timeline.json | 226 ++++++++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 4 deletions(-) create mode 100644 tests/data/timeline.json diff --git a/src/async_client.rs b/src/async_client.rs index 89e09390..1c53f1a0 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -14,6 +14,7 @@ // limitations under the License. use futures::future::{BoxFuture, Future, FutureExt}; +use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::result::Result as StdResult; use std::sync::atomic::{AtomicU64, Ordering}; @@ -260,6 +261,22 @@ impl AsyncClient { &self.homeserver } + #[doc(hidden)] + /// Access to the underlying `BaseClient`. Used for testing and debugging so far. + pub async fn base_client(&self) -> RwLockReadGuard<'_, BaseClient> { + self.base_client.read().await + } + + /// Calculate the room name from a `RoomId`, returning a string. + pub async fn get_room_name(&self, room_id: &str) -> Option { + self.base_client.read().await.calculate_room_name(room_id) + } + + /// Calculate the room names this client knows about. + pub async fn get_room_names(&self) -> Vec { + self.base_client.read().await.calculate_room_names() + } + /// Add a callback that will be called every time the client receives a room /// event /// diff --git a/src/base_client.rs b/src/base_client.rs index 34ef5d6c..005e9140 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -28,7 +28,7 @@ use crate::events::room::{ name::{NameEvent}, }; use crate::events::EventResult; -use crate::identifiers::{RoomAliasId}; +use crate::identifiers::RoomAliasId; use crate::session::Session; use std::sync::{Arc, RwLock}; @@ -103,7 +103,7 @@ impl RoomName { true } - pub fn calculate_name(&self, room_id: &RoomId, members: &HashMap) -> String { + pub fn calculate_name(&self, room_id: &str, members: &HashMap) -> String { // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 // the order in which we check for a name ^^ if let Some(name) = &self.name { @@ -117,9 +117,10 @@ impl RoomName { let mut names = members.values().flat_map(|m| m.display_name.clone()).take(3).collect::>(); if names.is_empty() { + // TODO implement the rest of matrix-js-sdk handling of room names format!("Room {}", room_id) } else { - // stablize order + // stabilize order names.sort(); names.join(", ").to_string() } @@ -360,7 +361,18 @@ impl Client { Ok(()) } - pub(crate) fn get_or_create_room(&mut self, room_id: &str) -> &mut Arc> { + pub(crate) fn calculate_room_name(&self, room_id: &str) -> Option { + self.joined_rooms.get(room_id) + .and_then(|r| r.read().map(|r| r.room_name.calculate_name(room_id, &r.members)).ok()) + } + + pub(crate) fn calculate_room_names(&self) -> Vec { + self.joined_rooms.iter() + .flat_map(|(id, room)| room.read().map(|r| r.room_name.calculate_name(id, &r.members)).ok()) + .collect() + } + + fn get_or_create_room(&mut self, room_id: &str) -> &mut Arc> { #[allow(clippy::or_fun_call)] self.joined_rooms .entry(room_id.to_string()) diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index f5a972c8..6ecfbfec 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -59,3 +59,36 @@ fn sync() { assert!(rt.block_on(client.sync_token()).is_some()); } + + +#[test] +fn timeline() { + let mut rt = Runtime::new().unwrap(); + + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:example.com").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ) + .with_status(200) + .with_body_from_file("tests/data/timeline.json") + .create(); + + let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); + + let sync_settings = SyncSettings::new().timeout(3000).unwrap(); + + let _response = rt.block_on(client.sync(sync_settings)).unwrap(); + + assert_eq!(vec!["tutorial"], rt.block_on(client.get_room_names())); + assert_eq!(Some("tutorial".into()), rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost"))); + + // rt.block_on(async { println!("{:#?}", &client.base_client().await.joined_rooms ) }); +} diff --git a/tests/data/timeline.json b/tests/data/timeline.json new file mode 100644 index 00000000..3b898e20 --- /dev/null +++ b/tests/data/timeline.json @@ -0,0 +1,226 @@ +{ + "device_one_time_keys_count": {}, + "next_batch": "s526_47314_0_7_1_1_1_11444_1", + "device_lists": { + "changed": [ + "@example:example.org" + ], + "left": [] + }, + + "rooms": { + "invite": {}, + "join": { + "!SVkFJHzfwvuaIEawgC:localhost": { + "account_data": { + "events": [] + }, + "ephemeral": { + "events": [ + { + "content": { + "$151680659217152dPKjd:localhost": { + "m.read": { + "@example:localhost": { + "ts": 1516809890615 + } + } + } + }, + "type": "m.receipt" + } + ] + }, + "state": { + "events": [ + { + "content": { + "join_rule": "public" + }, + "event_id": "$15139375514WsgmR:localhost", + "origin_server_ts": 1513937551539, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.join_rules", + "unsigned": { + "age": 7034220355 + } + }, + { + "content": { + "avatar_url": null, + "displayname": "example", + "membership": "join" + }, + "event_id": "$151800140517rfvjc:localhost", + "membership": "join", + "origin_server_ts": 1518001405556, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "m.room.member", + "unsigned": { + "age": 2970366338, + "replaces_state": "$151800111315tsynI:localhost" + } + }, + { + "content": { + "history_visibility": "shared" + }, + "event_id": "$15139375515VaJEY:localhost", + "origin_server_ts": 1513937551613, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.history_visibility", + "unsigned": { + "age": 7034220281 + } + }, + { + "content": { + "creator": "@example:localhost" + }, + "event_id": "$15139375510KUZHi:localhost", + "origin_server_ts": 1513937551203, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.create", + "unsigned": { + "age": 7034220691 + } + }, + { + "content": { + "aliases": [ + "#tutorial:localhost" + ] + }, + "event_id": "$15139375516NUgtD:localhost", + "origin_server_ts": 1513937551720, + "sender": "@example:localhost", + "state_key": "localhost", + "type": "m.room.aliases", + "unsigned": { + "age": 7034220174 + } + }, + { + "content": { + "topic": "\ud83d\ude00" + }, + "event_id": "$151957878228ssqrJ:localhost", + "origin_server_ts": 1519578782185, + "sender": "@example: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": { + "@example:localhost": 100 + }, + "users_default": 0 + }, + "event_id": "$15139375512JaHAW:localhost", + "origin_server_ts": 1513937551359, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.power_levels", + "unsigned": { + "age": 7034220535 + } + }, + { + "content": { + "alias": "#tutorial:localhost" + }, + "event_id": "$15139375513VdeRF:localhost", + "origin_server_ts": 1513937551461, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.canonical_alias", + "unsigned": { + "age": 7034220433 + } + }, + { + "content": { + "avatar_url": null, + "displayname": "example2", + "membership": "join" + }, + "event_id": "$152034824468gOeNB:localhost", + "membership": "join", + "origin_server_ts": 1520348244605, + "sender": "@example2:localhost", + "state_key": "@example2:localhost", + "type": "m.room.member", + "unsigned": { + "age": 623527289, + "prev_content": { + "membership": "leave" + }, + "prev_sender": "@example:localhost", + "replaces_state": "$152034819067QWJxM:localhost" + } + } + ] + }, + "timeline": { + "events": [ + { + "content": { + "body": "baba", + "format": "org.matrix.custom.html", + "formatted_body": "baba", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1520372800469, + "sender": "@example: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 063d86af714c59b54bff4d9b69eb923c202bad3a Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 17:23:49 -0400 Subject: [PATCH 10/28] cargo fmt/clippy --- src/async_client.rs | 1 - src/base_client.rs | 30 +++++++++++++++++++++--------- tests/async_client_tests.rs | 6 ++++-- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 1c53f1a0..811af61c 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -14,7 +14,6 @@ // limitations under the License. use futures::future::{BoxFuture, Future, FutureExt}; -use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::result::Result as StdResult; use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/src/base_client.rs b/src/base_client.rs index 005e9140..6c1a23e2 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -25,7 +25,7 @@ use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, member::{MemberEvent, MembershipState}, - name::{NameEvent}, + name::NameEvent, }; use crate::events::EventResult; use crate::identifiers::RoomAliasId; @@ -113,16 +113,20 @@ impl RoomName { } else if !self.aliases.is_empty() { self.aliases[0].alias().to_string() } else { - // TODO - let mut names = members.values().flat_map(|m| m.display_name.clone()).take(3).collect::>(); - + // TODO + let mut names = members + .values() + .flat_map(|m| m.display_name.clone()) + .take(3) + .collect::>(); + if names.is_empty() { // TODO implement the rest of matrix-js-sdk handling of room names format!("Room {}", room_id) } else { // stabilize order names.sort(); - names.join(", ").to_string() + names.join(", ") } } } @@ -362,13 +366,21 @@ impl Client { } pub(crate) fn calculate_room_name(&self, room_id: &str) -> Option { - self.joined_rooms.get(room_id) - .and_then(|r| r.read().map(|r| r.room_name.calculate_name(room_id, &r.members)).ok()) + self.joined_rooms.get(room_id).and_then(|r| { + r.read() + .map(|r| r.room_name.calculate_name(room_id, &r.members)) + .ok() + }) } pub(crate) fn calculate_room_names(&self) -> Vec { - self.joined_rooms.iter() - .flat_map(|(id, room)| room.read().map(|r| r.room_name.calculate_name(id, &r.members)).ok()) + self.joined_rooms + .iter() + .flat_map(|(id, room)| { + room.read() + .map(|r| r.room_name.calculate_name(id, &r.members)) + .ok() + }) .collect() } diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 6ecfbfec..c4eca2ae 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -60,7 +60,6 @@ fn sync() { assert!(rt.block_on(client.sync_token()).is_some()); } - #[test] fn timeline() { let mut rt = Runtime::new().unwrap(); @@ -88,7 +87,10 @@ fn timeline() { let _response = rt.block_on(client.sync(sync_settings)).unwrap(); assert_eq!(vec!["tutorial"], rt.block_on(client.get_room_names())); - assert_eq!(Some("tutorial".into()), rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost"))); + assert_eq!( + Some("tutorial".into()), + rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost")) + ); // rt.block_on(async { println!("{:#?}", &client.base_client().await.joined_rooms ) }); } From 6b5a357e336147974f5d2008ae9fdb08e98338a0 Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 17:26:10 -0400 Subject: [PATCH 11/28] use sync.json --- tests/async_client_tests.rs | 2 +- tests/data/timeline.json | 226 ------------------------------------ 2 files changed, 1 insertion(+), 227 deletions(-) delete mode 100644 tests/data/timeline.json diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index c4eca2ae..6584f0c0 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -77,7 +77,7 @@ fn timeline() { Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), ) .with_status(200) - .with_body_from_file("tests/data/timeline.json") + .with_body_from_file("tests/data/sync.json") .create(); let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); diff --git a/tests/data/timeline.json b/tests/data/timeline.json deleted file mode 100644 index 3b898e20..00000000 --- a/tests/data/timeline.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "device_one_time_keys_count": {}, - "next_batch": "s526_47314_0_7_1_1_1_11444_1", - "device_lists": { - "changed": [ - "@example:example.org" - ], - "left": [] - }, - - "rooms": { - "invite": {}, - "join": { - "!SVkFJHzfwvuaIEawgC:localhost": { - "account_data": { - "events": [] - }, - "ephemeral": { - "events": [ - { - "content": { - "$151680659217152dPKjd:localhost": { - "m.read": { - "@example:localhost": { - "ts": 1516809890615 - } - } - } - }, - "type": "m.receipt" - } - ] - }, - "state": { - "events": [ - { - "content": { - "join_rule": "public" - }, - "event_id": "$15139375514WsgmR:localhost", - "origin_server_ts": 1513937551539, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.join_rules", - "unsigned": { - "age": 7034220355 - } - }, - { - "content": { - "avatar_url": null, - "displayname": "example", - "membership": "join" - }, - "event_id": "$151800140517rfvjc:localhost", - "membership": "join", - "origin_server_ts": 1518001405556, - "sender": "@example:localhost", - "state_key": "@example:localhost", - "type": "m.room.member", - "unsigned": { - "age": 2970366338, - "replaces_state": "$151800111315tsynI:localhost" - } - }, - { - "content": { - "history_visibility": "shared" - }, - "event_id": "$15139375515VaJEY:localhost", - "origin_server_ts": 1513937551613, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.history_visibility", - "unsigned": { - "age": 7034220281 - } - }, - { - "content": { - "creator": "@example:localhost" - }, - "event_id": "$15139375510KUZHi:localhost", - "origin_server_ts": 1513937551203, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.create", - "unsigned": { - "age": 7034220691 - } - }, - { - "content": { - "aliases": [ - "#tutorial:localhost" - ] - }, - "event_id": "$15139375516NUgtD:localhost", - "origin_server_ts": 1513937551720, - "sender": "@example:localhost", - "state_key": "localhost", - "type": "m.room.aliases", - "unsigned": { - "age": 7034220174 - } - }, - { - "content": { - "topic": "\ud83d\ude00" - }, - "event_id": "$151957878228ssqrJ:localhost", - "origin_server_ts": 1519578782185, - "sender": "@example: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": { - "@example:localhost": 100 - }, - "users_default": 0 - }, - "event_id": "$15139375512JaHAW:localhost", - "origin_server_ts": 1513937551359, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.power_levels", - "unsigned": { - "age": 7034220535 - } - }, - { - "content": { - "alias": "#tutorial:localhost" - }, - "event_id": "$15139375513VdeRF:localhost", - "origin_server_ts": 1513937551461, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.canonical_alias", - "unsigned": { - "age": 7034220433 - } - }, - { - "content": { - "avatar_url": null, - "displayname": "example2", - "membership": "join" - }, - "event_id": "$152034824468gOeNB:localhost", - "membership": "join", - "origin_server_ts": 1520348244605, - "sender": "@example2:localhost", - "state_key": "@example2:localhost", - "type": "m.room.member", - "unsigned": { - "age": 623527289, - "prev_content": { - "membership": "leave" - }, - "prev_sender": "@example:localhost", - "replaces_state": "$152034819067QWJxM:localhost" - } - } - ] - }, - "timeline": { - "events": [ - { - "content": { - "body": "baba", - "format": "org.matrix.custom.html", - "formatted_body": "baba", - "msgtype": "m.text" - }, - "event_id": "$152037280074GZeOm:localhost", - "origin_server_ts": 1520372800469, - "sender": "@example: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 7062bdb484880e499eb080e768f2db6a9a9950e8 Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 20:17:06 -0400 Subject: [PATCH 12/28] fix slice match room_aliases --- src/async_client.rs | 6 +++--- src/base_client.rs | 5 +++++ tests/async_client_tests.rs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 811af61c..7996c4c9 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -262,10 +262,10 @@ impl AsyncClient { #[doc(hidden)] /// Access to the underlying `BaseClient`. Used for testing and debugging so far. - pub async fn base_client(&self) -> RwLockReadGuard<'_, BaseClient> { - self.base_client.read().await + pub fn base_client(&self) -> Arc> { + Arc::clone(&self.base_client) } - + /// Calculate the room name from a `RoomId`, returning a string. pub async fn get_room_name(&self, room_id: &str) -> Option { self.base_client.read().await.calculate_room_name(room_id) diff --git a/src/base_client.rs b/src/base_client.rs index 6c1a23e2..d5d7b74f 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -203,6 +203,7 @@ impl Room { } /// Handle a room.member updating the room state if necessary. + /// /// Returns true if the joined member list changed, false otherwise. pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { match event.content.membership { @@ -233,15 +234,18 @@ impl Room { } /// Handle a room.aliases event, updating the room state if necessary. + /// /// Returns true if the room name changed, false otherwise. pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { match event.content.aliases.as_slice() { [alias] => self.room_aliases(alias), + [alias, ..] => self.room_aliases(alias), _ => false, } } /// Handle a room.canonical_alias event, updating the room state if necessary. + /// /// Returns true if the room name changed, false otherwise. pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { match &event.content.alias { @@ -251,6 +255,7 @@ impl Room { } /// Handle a room.name event, updating the room state if necessary. + /// /// Returns true if the room name changed, false otherwise. pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { match event.content.name() { diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 6584f0c0..670a43da 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -92,5 +92,5 @@ fn timeline() { rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost")) ); - // rt.block_on(async { println!("{:#?}", &client.base_client().await.joined_rooms ) }); + // rt.block_on(async { println!("{:#?}", &client.base_client().read().await.joined_rooms ) }); } From bb89db30cb78d8fc9a41d9886e26980973eb6de0 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 07:52:25 -0400 Subject: [PATCH 13/28] remove unused import --- src/async_client.rs | 2 +- src/models/room.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 3e843752..6894f6af 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -18,7 +18,7 @@ use std::convert::{TryFrom, TryInto}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock as SyncLock}; use std::time::{Duration, Instant}; -use tokio::sync::{RwLock, RwLockReadGuard}; +use tokio::sync::RwLock; use async_std::task::sleep; diff --git a/src/models/room.rs b/src/models/room.rs index 4a69a366..deea05ad 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -260,7 +260,7 @@ impl Room { RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), // power levels of the room members - RoomEvent::RoomPowerLevels(p) => self.handle_power_level(p), + // RoomEvent::RoomPowerLevels(p) => self.handle_power_level(p), _ => false, } } From b760b32bae184f007d1705903cb169edec90c1cd Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 08:12:52 -0400 Subject: [PATCH 14/28] fix rebase swap --- src/base_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base_client.rs b/src/base_client.rs index d5d7b74f..1a54ab80 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -389,7 +389,7 @@ impl Client { .collect() } - fn get_or_create_room(&mut self, room_id: &str) -> &mut Arc> { + pub(crate) fn get_or_create_room(&mut self, room_id: &str) -> &mut Arc> { #[allow(clippy::or_fun_call)] self.joined_rooms .entry(room_id.to_string()) From bd50e615a70460adc1d235905b8d2879de4e6130 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 08:27:16 -0400 Subject: [PATCH 15/28] use tokio::test, cargo fmt/clippy --- src/async_client.rs | 2 +- src/base_client.rs | 10 +++++----- tests/async_client_tests.rs | 38 +++++++++++++++++-------------------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 7996c4c9..6ca98e88 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -265,7 +265,7 @@ impl AsyncClient { pub fn base_client(&self) -> Arc> { Arc::clone(&self.base_client) } - + /// Calculate the room name from a `RoomId`, returning a string. pub async fn get_room_name(&self, room_id: &str) -> Option { self.base_client.read().await.calculate_room_name(room_id) diff --git a/src/base_client.rs b/src/base_client.rs index 1a54ab80..43952fd0 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -72,7 +72,7 @@ pub struct RoomMember { /// A Matrix rooom. pub struct Room { /// The unique id of the room. - pub room_id: RoomId, + pub room_id: String, /// The name of the room, clients use this to represent a room. pub room_name: RoomName, /// The mxid of our own user. @@ -203,7 +203,7 @@ impl Room { } /// Handle a room.member updating the room state if necessary. - /// + /// /// Returns true if the joined member list changed, false otherwise. pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { match event.content.membership { @@ -234,7 +234,7 @@ impl Room { } /// Handle a room.aliases event, updating the room state if necessary. - /// + /// /// Returns true if the room name changed, false otherwise. pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { match event.content.aliases.as_slice() { @@ -245,7 +245,7 @@ impl Room { } /// Handle a room.canonical_alias event, updating the room state if necessary. - /// + /// /// Returns true if the room name changed, false otherwise. pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { match &event.content.alias { @@ -255,7 +255,7 @@ impl Room { } /// Handle a room.name event, updating the room state if necessary. - /// + /// /// Returns true if the room name changed, false otherwise. pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { match event.content.name() { diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 670a43da..0c213d0c 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -9,10 +9,8 @@ use std::convert::TryFrom; use std::str::FromStr; use std::time::Duration; -#[test] -fn login() { - let mut rt = Runtime::new().unwrap(); - +#[tokio::test] +async fn login() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); let _m = mock("POST", "/_matrix/client/r0/login") @@ -22,17 +20,17 @@ fn login() { let mut client = AsyncClient::new(homeserver, None).unwrap(); - rt.block_on(client.login("example", "wordpass", None, None)) + client + .login("example", "wordpass", None, None) + .await .unwrap(); - let logged_in = rt.block_on(client.logged_in()); + let logged_in = client.logged_in().await; assert!(logged_in, "Clint should be logged in"); } -#[test] -fn sync() { - let mut rt = Runtime::new().unwrap(); - +#[tokio::test] +async fn sync() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); let session = Session { @@ -53,17 +51,15 @@ fn sync() { let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - let response = rt.block_on(client.sync(sync_settings)).unwrap(); + let response = client.sync(sync_settings).await.unwrap(); assert_ne!(response.next_batch, ""); - assert!(rt.block_on(client.sync_token()).is_some()); + assert!(client.sync_token().await.is_some()); } -#[test] -fn timeline() { - let mut rt = Runtime::new().unwrap(); - +#[tokio::test] +async fn timeline() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); let session = Session { @@ -82,15 +78,15 @@ fn timeline() { let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); - let sync_settings = SyncSettings::new().timeout(3000).unwrap(); + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - let _response = rt.block_on(client.sync(sync_settings)).unwrap(); + let _response = client.sync(sync_settings).await.unwrap(); - assert_eq!(vec!["tutorial"], rt.block_on(client.get_room_names())); + assert_eq!(vec!["tutorial"], client.get_room_names().await); assert_eq!( Some("tutorial".into()), - rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost")) + client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost").await ); - // rt.block_on(async { println!("{:#?}", &client.base_client().read().await.joined_rooms ) }); + println!("{:#?}", &client.base_client().read().await.joined_rooms); } From 9ee8a2d011d7e097d9f19a57d1c15090756a78b9 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 08:58:02 -0400 Subject: [PATCH 16/28] merged add-events, fix a few type changes and merge fails --- examples/login.rs | 6 +- src/async_client.rs | 11 +-- src/base_client.rs | 4 +- src/event_emitter/mod.rs | 51 +++++----- src/lib.rs | 2 +- src/models/mod.rs | 188 +----------------------------------- src/models/room.rs | 51 ++++++---- src/models/room_member.rs | 24 +++-- src/models/room_state.rs | 15 +++ src/models/user.rs | 4 +- tests/async_client_tests.rs | 35 ------- 11 files changed, 102 insertions(+), 289 deletions(-) diff --git a/examples/login.rs b/examples/login.rs index 088895e6..561795e6 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -28,7 +28,11 @@ async fn async_cb(room: Arc>, event: Arc>) { let member = room.members.get(&sender.to_string()).unwrap(); println!( "{}: {}", - member.user.display_name.as_ref().unwrap_or(&sender.to_string()), + member + .user + .display_name + .as_ref() + .unwrap_or(&sender.to_string()), msg_body ); } diff --git a/src/async_client.rs b/src/async_client.rs index 8fe5d916..90b8bdce 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -38,7 +38,6 @@ use ruma_identifiers::RoomId; use crate::api; use crate::base_client::Client as BaseClient; use crate::models::Room; -use crate::error::{Error, InnerError}; use crate::session::Session; use crate::VERSION; use crate::{Error, Result}; @@ -266,7 +265,7 @@ impl AsyncClient { pub fn base_client(&self) -> Arc> { Arc::clone(&self.base_client) } - + /// Calculate the room name from a `RoomId`, returning a string. pub async fn get_room_name(&self, room_id: &str) -> Option { self.base_client.read().await.calculate_room_name(room_id) @@ -424,16 +423,14 @@ impl AsyncClient { if let Some(e) = decrypted_event { *event = e; } - + for presence in &response.presence.events { let mut client = self.base_client.write().await; if let EventResult::Ok(e) = presence { - client.receive_presence_event(&room_id, e); + client.receive_presence_event(&room_id_string, e); } } - let event = Arc::new(event.clone()); - let callbacks = { let mut cb_futures = self.event_callbacks.lock().unwrap(); @@ -446,7 +443,7 @@ impl AsyncClient { let mut callbacks = Vec::new(); for cb in &mut cb_futures.iter_mut() { - callbacks.push(cb(matrix_room.clone(), event.clone())); + callbacks.push(cb(matrix_room.clone(), Arc::clone(&event))); } callbacks diff --git a/src/base_client.rs b/src/base_client.rs index 93b8b6b7..30a1d0aa 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -21,17 +21,17 @@ use std::result::Result as StdResult; use crate::api::r0 as api; use crate::error::Result; use crate::events::collections::all::{RoomEvent, StateEvent}; +use crate::events::presence::PresenceEvent; use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, member::{MemberEvent, MembershipState}, name::NameEvent, }; -use crate::events::presence::PresenceEvent; use crate::events::EventResult; use crate::identifiers::RoomAliasId; +use crate::models::Room; use crate::session::Session; -use crate::models::{Room}; use std::sync::{Arc, RwLock}; #[cfg(feature = "encryption")] diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs index 7bce3fc1..93133a2d 100644 --- a/src/event_emitter/mod.rs +++ b/src/event_emitter/mod.rs @@ -13,32 +13,35 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; - -use crate::api::r0 as api; -use crate::events::collections::all::{Event, RoomEvent, StateEvent}; -use crate::events::room::{ - aliases::AliasesEvent, - canonical_alias::CanonicalAliasEvent, - member::{MemberEvent, MemberEventContent, MembershipState}, - name::NameEvent, -}; -use crate::events::EventResult; -use crate::identifiers::RoomAliasId; -use crate::session::Session; +use crate::events::collections::all::RoomEvent; use crate::models::Room; -use js_int::{Int, UInt}; -#[cfg(feature = "encryption")] -use tokio::sync::Mutex; - -#[cfg(feature = "encryption")] -use crate::crypto::{OlmMachine, OneTimeKeys}; -#[cfg(feature = "encryption")] -use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; +// JUST AN IDEA +// +/// This is just a thought I had. Making users impl a trait instead of writing callbacks for events +/// could give the chance for really good documentation for each event? +/// It would look something like this +/// +/// ```rust,ignore +/// use matrix-sdk::{AsyncClient, EventEmitter}; +/// +/// struct MyAppClient; +/// +/// impl EventEmitter for MyAppClient { +/// fn on_room_member(&mut self, room: &Room, event: &RoomEvent) { ... } +/// } +/// async fn main() { +/// let cl = AsyncClient::with_emitter(MyAppClient); +/// } +/// ``` +/// +/// And in `AsyncClient::sync` there could be a switch case that calls the corresponding method on +/// the `Box pub trait EventEmitter { - fn on_room_name(&mut self, _: &Room) {} - fn on_room_member(&mut self, _: &Room) {} + fn on_room_name(&mut self, _: &Room, _: &RoomEvent) {} + /// Any event that alters the state of the room's members + fn on_room_member(&mut self, _: &Room, _: &RoomEvent) {} } + + diff --git a/src/lib.rs b/src/lib.rs index 8e3ffb0f..6014c21d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,9 +35,9 @@ pub use ruma_identifiers as identifiers; mod async_client; mod base_client; mod error; +mod event_emitter; mod models; mod session; -mod event_emitter; #[cfg(feature = "encryption")] mod crypto; diff --git a/src/models/mod.rs b/src/models/mod.rs index 4e2d3307..cbc7422e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,51 +1,6 @@ -use crate::events::{ - call::{ - answer::AnswerEvent, candidates::CandidatesEvent, hangup::HangupEvent, invite::InviteEvent, - }, - direct::DirectEvent, - dummy::DummyEvent, - forwarded_room_key::ForwardedRoomKeyEvent, - fully_read::FullyReadEvent, - ignored_user_list::IgnoredUserListEvent, - key::verification::{ - accept::AcceptEvent, cancel::CancelEvent, key::KeyEvent, mac::MacEvent, - request::RequestEvent, start::StartEvent, - }, - presence::PresenceEvent, - push_rules::PushRulesEvent, - receipt::ReceiptEvent, - room::{ - aliases::AliasesEvent, - avatar::AvatarEvent, - canonical_alias::CanonicalAliasEvent, - create::CreateEvent, - encrypted::EncryptedEvent, - encryption::EncryptionEvent, - guest_access::GuestAccessEvent, - history_visibility::HistoryVisibilityEvent, - join_rules::JoinRulesEvent, - member::MemberEvent, - message::{feedback::FeedbackEvent, MessageEvent}, - name::NameEvent, - pinned_events::PinnedEventsEvent, - power_levels::PowerLevelsEvent, - redaction::RedactionEvent, - server_acl::ServerAclEvent, - third_party_invite::ThirdPartyInviteEvent, - tombstone::TombstoneEvent, - topic::TopicEvent, - }, - room_key::RoomKeyEvent, - room_key_request::RoomKeyRequestEvent, - sticker::StickerEvent, - tag::TagEvent, - typing::TypingEvent, - CustomEvent, CustomRoomEvent, CustomStateEvent, -}; - +mod room; mod room_member; mod room_state; -mod room; mod user; pub use room::{Room, RoomName}; @@ -53,145 +8,4 @@ pub use room_member::RoomMember; pub use user::User; pub type Token = String; -pub type RoomId = String; pub type UserId = String; - -pub enum EventWrapper<'ev> { - /// m.call.answer - CallAnswer(&'ev AnswerEvent), - - /// m.call.candidates - CallCandidates(&'ev CandidatesEvent), - - /// m.call.hangup - CallHangup(&'ev HangupEvent), - - /// m.call.invite - CallInvite(&'ev InviteEvent), - - /// m.direct - Direct(&'ev DirectEvent), - - /// m.dummy - Dummy(&'ev DummyEvent), - - /// m.forwarded_room_key - ForwardedRoomKey(&'ev ForwardedRoomKeyEvent), - - /// m.fully_read - FullyRead(&'ev FullyReadEvent), - - /// m.ignored_user_list - IgnoredUserList(&'ev IgnoredUserListEvent), - - /// m.key.verification.accept - KeyVerificationAccept(&'ev AcceptEvent), - - /// m.key.verification.cancel - KeyVerificationCancel(&'ev CancelEvent), - - /// m.key.verification.key - KeyVerificationKey(&'ev KeyEvent), - - /// m.key.verification.mac - KeyVerificationMac(&'ev MacEvent), - - /// m.key.verification.request - KeyVerificationRequest(&'ev RequestEvent), - - /// m.key.verification.start - KeyVerificationStart(&'ev StartEvent), - - /// m.presence - Presence(&'ev PresenceEvent), - - /// m.push_rules - PushRules(&'ev PushRulesEvent), - - /// m.receipt - Receipt(&'ev ReceiptEvent), - - /// m.room.aliases - RoomAliases(&'ev AliasesEvent), - - /// m.room.avatar - RoomAvatar(&'ev AvatarEvent), - - /// m.room.canonical_alias - RoomCanonicalAlias(&'ev CanonicalAliasEvent), - - /// m.room.create - RoomCreate(&'ev CreateEvent), - - /// m.room.encrypted - RoomEncrypted(&'ev EncryptedEvent), - - /// m.room.encryption - RoomEncryption(&'ev EncryptionEvent), - - /// m.room.guest_access - RoomGuestAccess(&'ev GuestAccessEvent), - - /// m.room.history_visibility - RoomHistoryVisibility(&'ev HistoryVisibilityEvent), - - /// m.room.join_rules - RoomJoinRules(&'ev JoinRulesEvent), - - /// m.room.member - RoomMember(&'ev MemberEvent), - - /// m.room.message - RoomMessage(&'ev MessageEvent), - - /// m.room.message.feedback - RoomMessageFeedback(&'ev FeedbackEvent), - - /// m.room.name - RoomName(&'ev NameEvent), - - /// m.room.pinned_events - RoomPinnedEvents(&'ev PinnedEventsEvent), - - /// m.room.power_levels - RoomPowerLevels(&'ev PowerLevelsEvent), - - /// m.room.redaction - RoomRedaction(&'ev RedactionEvent), - - /// m.room.server_acl - RoomServerAcl(&'ev ServerAclEvent), - - /// m.room.third_party_invite - RoomThirdPartyInvite(&'ev ThirdPartyInviteEvent), - - /// m.room.tombstone - RoomTombstone(&'ev TombstoneEvent), - - /// m.room.topic - RoomTopic(&'ev TopicEvent), - - /// m.room_key - RoomKey(&'ev RoomKeyEvent), - - /// m.room_key_request - RoomKeyRequest(&'ev RoomKeyRequestEvent), - - /// m.sticker - Sticker(&'ev StickerEvent), - - /// m.tag - Tag(&'ev TagEvent), - - /// m.typing - Typing(&'ev TypingEvent), - - /// Any basic event that is not part of the specification. - Custom(&'ev CustomEvent), - - /// Any room event that is not part of the specification. - CustomRoom(&'ev CustomRoomEvent), - - /// Any state event that is not part of the specification. - CustomState(&'ev CustomStateEvent), -} diff --git a/src/models/room.rs b/src/models/room.rs index deea05ad..e85c6a13 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; +use super::{RoomMember, User, UserId}; use crate::api::r0 as api; use crate::events::collections::all::{RoomEvent, StateEvent}; use crate::events::room::{ @@ -24,10 +25,12 @@ use crate::events::room::{ member::{MemberEvent, MembershipState}, name::NameEvent, }; -use crate::events::{presence::{PresenceEvent, PresenceEventContent}, EventResult}; +use crate::events::{ + presence::{PresenceEvent, PresenceEventContent}, + EventResult, +}; use crate::identifiers::RoomAliasId; use crate::session::Session; -use super::{RoomId, UserId, RoomMember, User}; #[cfg(feature = "encryption")] use tokio::sync::Mutex; @@ -52,7 +55,7 @@ pub struct RoomName { /// A Matrix rooom. pub struct Room { /// The unique id of the room. - pub room_id: RoomId, + pub room_id: String, /// The name of the room, clients use this to represent a room. pub room_name: RoomName, /// The mxid of our own user. @@ -175,11 +178,11 @@ impl Room { // } // fn handle_leave(&mut self, event: &MemberEvent) -> bool { - + // } /// Handle a room.member updating the room state if necessary. - /// + /// /// Returns true if the joined member list changed, false otherwise. pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { match &event.content.membership { @@ -214,7 +217,7 @@ impl Room { } /// Handle a room.aliases event, updating the room state if necessary. - /// + /// /// Returns true if the room name changed, false otherwise. pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { match event.content.aliases.as_slice() { @@ -225,7 +228,7 @@ impl Room { } /// Handle a room.canonical_alias event, updating the room state if necessary. - /// + /// /// Returns true if the room name changed, false otherwise. pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { match &event.content.alias { @@ -235,7 +238,7 @@ impl Room { } /// Handle a room.name event, updating the room state if necessary. - /// + /// /// Returns true if the room name changed, false otherwise. pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { match event.content.name() { @@ -291,21 +294,29 @@ impl Room { /// * `event` - The event of the room. pub fn receive_presence_event(&mut self, event: &PresenceEvent) -> bool { let PresenceEvent { - content: PresenceEventContent { - avatar_url, - currently_active, - displayname, - last_active_ago, - presence, - status_msg, - }, + content: + PresenceEventContent { + avatar_url, + currently_active, + displayname, + last_active_ago, + presence, + status_msg, + }, sender, } = event; - if let Some(user) = self.members.get_mut(&sender.to_string()).map(|m| &mut m.user) { - if user.display_name == *displayname && user.avatar_url == *avatar_url - && user.presence.as_ref() == Some(presence) && user.status_msg == *status_msg - && user.last_active_ago == *last_active_ago && user.currently_active == *currently_active + if let Some(user) = self + .members + .get_mut(&sender.to_string()) + .map(|m| &mut m.user) + { + if user.display_name == *displayname + && user.avatar_url == *avatar_url + && user.presence.as_ref() == Some(presence) + && user.status_msg == *status_msg + && user.last_active_ago == *last_active_ago + && user.currently_active == *currently_active { false } else { diff --git a/src/models/room_member.rs b/src/models/room_member.rs index c8f67ef8..33105ac0 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; +use super::{User, UserId}; use crate::api::r0 as api; use crate::events::collections::all::{Event, RoomEvent, StateEvent}; use crate::events::room::{ @@ -27,7 +28,6 @@ use crate::events::room::{ use crate::events::EventResult; use crate::identifiers::RoomAliasId; use crate::session::Session; -use super::{UserId, RoomId, User}; use js_int::{Int, UInt}; #[cfg(feature = "encryption")] @@ -44,7 +44,7 @@ pub struct RoomMember { /// The unique mxid of the user. pub user_id: UserId, /// The unique id of the room. - pub room_id: Option, + pub room_id: Option, /// If the member is typing. pub typing: Option, /// The user data for this room member. @@ -58,7 +58,7 @@ pub struct RoomMember { /// The human readable name of this room member. pub name: String, /// The events that created the state of this room member. - pub events: Vec + pub events: Vec, } impl RoomMember { @@ -73,26 +73,30 @@ impl RoomMember { power_level_norm: None, membership: event.content.membership, name: event.state_key.clone(), - events: vec![Event::RoomMember(event.clone())] + events: vec![Event::RoomMember(event.clone())], } } pub fn update(&mut self, event: &MemberEvent) { let MemberEvent { - content: MemberEventContent { - membership, - .. - }, + content: MemberEventContent { membership, .. }, room_id, state_key, .. } = event; let mut events = Vec::new(); - events.extend(self.events.drain(..).chain(Some(Event::RoomMember(event.clone())))); + events.extend( + self.events + .drain(..) + .chain(Some(Event::RoomMember(event.clone()))), + ); *self = Self { - room_id: room_id.as_ref().map(|id| id.to_string()).or(self.room_id.take()), + room_id: room_id + .as_ref() + .map(|id| id.to_string()) + .or(self.room_id.take()), user_id: state_key.clone(), typing: None, user: User::new(event), diff --git a/src/models/room_state.rs b/src/models/room_state.rs index e69de29b..2bbbb490 100644 --- a/src/models/room_state.rs +++ b/src/models/room_state.rs @@ -0,0 +1,15 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + diff --git a/src/models/user.rs b/src/models/user.rs index 060ec88c..f9840e32 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -16,19 +16,19 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; +use super::UserId; use crate::api::r0 as api; use crate::events::collections::all::{Event, RoomEvent, StateEvent}; +use crate::events::presence::{PresenceEvent, PresenceState}; use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, member::{MemberEvent, MembershipState}, name::NameEvent, }; -use crate::events::presence::{PresenceEvent, PresenceState}; use crate::events::EventResult; use crate::identifiers::RoomAliasId; use crate::session::Session; -use super::{UserId, RoomId}; use js_int::UInt; #[cfg(feature = "encryption")] diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 4b942e23..0c213d0c 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -90,38 +90,3 @@ async fn timeline() { println!("{:#?}", &client.base_client().read().await.joined_rooms); } - -#[test] -fn timeline() { - let mut rt = Runtime::new().unwrap(); - - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - - let session = Session { - access_token: "1234".to_owned(), - user_id: UserId::try_from("@example:example.com").unwrap(), - device_id: "DEVICEID".to_owned(), - }; - - let _m = mock( - "GET", - Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), - ) - .with_status(200) - .with_body_from_file("tests/data/sync.json") - .create(); - - let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); - - let sync_settings = SyncSettings::new().timeout(3000).unwrap(); - - let _response = rt.block_on(client.sync(sync_settings)).unwrap(); - - assert_eq!(vec!["tutorial"], rt.block_on(client.get_room_names())); - assert_eq!( - Some("tutorial".into()), - rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost")) - ); - - rt.block_on(async { println!("{:#?}", &client.base_client().read().await.joined_rooms ) }); -} From d4c9bb3cec4456493e58302fc4a90b0416767397 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 09:08:34 -0400 Subject: [PATCH 17/28] cargo fmt --- src/event_emitter/mod.rs | 6 ++---- src/models/room_state.rs | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs index 93133a2d..287d59f3 100644 --- a/src/event_emitter/mod.rs +++ b/src/event_emitter/mod.rs @@ -16,8 +16,8 @@ use crate::events::collections::all::RoomEvent; use crate::models::Room; -// JUST AN IDEA -// +// JUST AN IDEA +// /// This is just a thought I had. Making users impl a trait instead of writing callbacks for events /// could give the chance for really good documentation for each event? @@ -43,5 +43,3 @@ pub trait EventEmitter { /// Any event that alters the state of the room's members fn on_room_member(&mut self, _: &Room, _: &RoomEvent) {} } - - diff --git a/src/models/room_state.rs b/src/models/room_state.rs index 2bbbb490..f4404b09 100644 --- a/src/models/room_state.rs +++ b/src/models/room_state.rs @@ -12,4 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - From 0147e8c0acd94c260c2b2c75be1894e374fa8589 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 15:05:37 -0400 Subject: [PATCH 18/28] update_member, update_power methods for room_member --- src/models/room.rs | 20 ++++++++++---- src/models/room_member.rs | 58 +++++++++++++++++++-------------------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/src/models/room.rs b/src/models/room.rs index e85c6a13..7d1f3933 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -24,6 +24,7 @@ use crate::events::room::{ canonical_alias::CanonicalAliasEvent, member::{MemberEvent, MembershipState}, name::NameEvent, + power_levels::PowerLevelsEvent, }; use crate::events::{ presence::{PresenceEvent, PresenceEventContent}, @@ -188,10 +189,8 @@ impl Room { match &event.content.membership { MembershipState::Invite | MembershipState::Join => self.add_member(event), _ => { - if let Some(member) = self.members.get_mut(&event.sender.to_string()) { - let changed = member.membership == event.content.membership; - member.membership = event.content.membership; - changed + if let Some(member) = self.members.get_mut(&event.state_key) { + member.update_member(event) } else { false } @@ -247,6 +246,17 @@ impl Room { } } + /// Handle a room.power_levels event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub fn handle_power_level(&mut self, event: &PowerLevelsEvent) -> bool { + if let Some(member) = self.members.get_mut(&event.state_key) { + member.update_power(event) + } else { + false + } + } + /// Receive a timeline event for this room and update the room state. /// /// Returns true if the joined member list changed, false otherwise. @@ -263,7 +273,7 @@ impl Room { RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), // power levels of the room members - // RoomEvent::RoomPowerLevels(p) => self.handle_power_level(p), + RoomEvent::RoomPowerLevels(p) => self.handle_power_level(p), _ => false, } } diff --git a/src/models/room_member.rs b/src/models/room_member.rs index 33105ac0..896fd1c8 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -16,7 +16,7 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; -use super::{User, UserId}; +use super::User; use crate::api::r0 as api; use crate::events::collections::all::{Event, RoomEvent, StateEvent}; use crate::events::room::{ @@ -24,9 +24,10 @@ use crate::events::room::{ canonical_alias::CanonicalAliasEvent, member::{MemberEvent, MemberEventContent, MembershipState}, name::NameEvent, + power_levels::PowerLevelsEvent, }; use crate::events::EventResult; -use crate::identifiers::RoomAliasId; +use crate::identifiers::{RoomAliasId, UserId}; use crate::session::Session; use js_int::{Int, UInt}; @@ -66,7 +67,7 @@ impl RoomMember { let user = User::new(event); Self { room_id: event.room_id.as_ref().map(|id| id.to_string()), - user_id: event.state_key.clone(), + user_id: event.sender.clone(), typing: None, user, power_level: None, @@ -77,34 +78,31 @@ impl RoomMember { } } - pub fn update(&mut self, event: &MemberEvent) { - let MemberEvent { - content: MemberEventContent { membership, .. }, - room_id, - state_key, - .. - } = event; + pub fn update_member(&mut self, event: &MemberEvent) -> bool { + let changed = self.membership == event.content.membership; + self.membership = event.content.membership; + changed + } - let mut events = Vec::new(); - events.extend( - self.events - .drain(..) - .chain(Some(Event::RoomMember(event.clone()))), - ); - - *self = Self { - room_id: room_id - .as_ref() - .map(|id| id.to_string()) - .or(self.room_id.take()), - user_id: state_key.clone(), - typing: None, - user: User::new(event), - power_level: None, - power_level_norm: None, - membership: membership.clone(), - name: state_key.clone(), - events, + pub fn update_power(&mut self, event: &PowerLevelsEvent) -> bool { + let mut max_power = event.content.users_default; + for power in event.content.users.values() { + max_power = *power.max(&max_power); } + + let mut changed = false; + if let Some(user_power) = event.content.users.get(&self.user_id) { + changed = self.power_level == Some(*user_power); + self.power_level = Some(*user_power); + } else { + changed = self.power_level == Some(event.content.users_default); + self.power_level = Some(event.content.users_default); + } + + if max_power > Int::from(0) { + self.power_level_norm = Some((self.power_level.unwrap() * Int::from(100)) / max_power); + } + + changed } } From 4c7acd4b182aa263b38cff3e1a3d8f6c3f1ce3b9 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sun, 29 Mar 2020 08:05:40 -0400 Subject: [PATCH 19/28] clean up presence updating and member state, add_presence_callback method for AsyncClient --- src/async_client.rs | 112 +++++++++++++++++++++++++++++++--- src/base_client.rs | 1 + src/models/room.rs | 125 +++----------------------------------- src/models/room_member.rs | 26 ++++++-- src/models/user.rs | 65 +++++++++++++++++++- 5 files changed, 202 insertions(+), 127 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 90b8bdce..e9627682 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -32,6 +32,7 @@ use ruma_api::{Endpoint, Outgoing}; use ruma_events::collections::all::RoomEvent; use ruma_events::room::message::MessageEventContent; use ruma_events::EventResult; +use ruma_events::presence::PresenceEvent; pub use ruma_events::EventType; use ruma_identifiers::RoomId; @@ -46,6 +47,10 @@ type RoomEventCallback = Box< dyn FnMut(Arc>, Arc>) -> BoxFuture<'static, ()> + Send, >; +type PresenceEventCallback = Box< + dyn FnMut(Arc>, Arc>) -> BoxFuture<'static, ()> + Send, +>; + const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); #[derive(Clone)] @@ -61,6 +66,7 @@ pub struct AsyncClient { transaction_id: Arc, /// Event callbacks event_callbacks: Arc>>, + presence_callbacks: Arc>>, } impl std::fmt::Debug for AsyncClient { @@ -245,6 +251,7 @@ impl AsyncClient { base_client: Arc::new(RwLock::new(BaseClient::new(session)?)), transaction_id: Arc::new(AtomicU64::new(0)), event_callbacks: Arc::new(Mutex::new(Vec::new())), + presence_callbacks: Arc::new(Mutex::new(Vec::new())), }) } @@ -339,6 +346,76 @@ impl AsyncClient { futures.push(Box::new(future)); } + /// Add a callback that will be called every time the client receives a room + /// event + /// + /// # Arguments + /// + /// * `callback` - The callback that should be called once a RoomEvent is + /// received. + /// + /// # Examples + /// ``` + /// # use matrix_sdk::events::{ + /// # collections::all::RoomEvent, + /// # room::message::{MessageEvent, MessageEventContent, TextMessageEventContent}, + /// # presence::{PresenceEvent, PresenceEventContent}, + /// # EventResult, + /// # }; + /// # use matrix_sdk::Room; + /// # use std::sync::{Arc, RwLock}; + /// # use matrix_sdk::AsyncClient; + /// # use url::Url; + /// + /// async fn async_cb(room: Arc>, event: Arc>) { + /// let room = room.read().unwrap(); + /// let event = if let EventResult::Ok(event) = &*event { + /// event + /// } else { + /// return; + /// }; + /// let PresenceEvent { + /// content: PresenceEventContent { + /// avatar_url, + /// currently_active, + /// displayname, + /// last_active_ago, + /// presence, + /// status_msg, + /// }, + /// sender, + /// } = event; + /// { + /// let member = room.members.get(&sender.to_string()).unwrap(); + /// println!( + /// "{} is {}", + /// displayname.as_deref().unwrap_or(&sender.to_string()), + /// status_msg.as_deref().unwrap_or("not here") + /// ); + /// } + /// } + /// # fn main() -> Result<(), matrix_sdk::Error> { + /// let homeserver = Url::parse("http://localhost:8080")?; + /// + /// let mut client = AsyncClient::new(homeserver, None)?; + /// + /// client.add_presence_callback(async_cb); + /// # Ok(()) + /// # } + /// ``` + pub fn add_presence_callback( + &mut self, + mut callback: impl FnMut(Arc>, Arc>) -> C + 'static + Send, + ) where + C: Future + Send, + { + let mut futures = self.presence_callbacks.lock().unwrap(); + + let future = move |room, event| callback(room, event).boxed(); + + futures.push(Box::new(future)); + } + /// Login to the server. /// /// # Arguments @@ -424,13 +501,6 @@ impl AsyncClient { *event = e; } - for presence in &response.presence.events { - let mut client = self.base_client.write().await; - if let EventResult::Ok(e) = presence { - client.receive_presence_event(&room_id_string, e); - } - } - let callbacks = { let mut cb_futures = self.event_callbacks.lock().unwrap(); @@ -453,6 +523,34 @@ impl AsyncClient { cb.await; } } + + // After the room has been created and state/timeline events accounted for we use the room_id of the newly created + // to add any presence events that relate to a user in the current room. This is not super + // efficient but we need a room_id so we would loop through now or later. + for presence in &response.presence.events { + let mut client = self.base_client.write().await; + if let EventResult::Ok(e) = presence { + client.receive_presence_event(&room_id_string, e); + } + + let callbacks = { + let mut cb_futures = self.presence_callbacks.lock().unwrap(); + let event = if !cb_futures.is_empty() { + Arc::new(presence.clone()) + } else { + continue; + }; + let mut callbacks = Vec::new(); + for cb in &mut cb_futures.iter_mut() { + callbacks.push(cb(matrix_room.clone(), Arc::clone(&event))); + } + + callbacks + }; + for cb in callbacks { + cb.await; + } + } } let mut client = self.base_client.write().await; diff --git a/src/base_client.rs b/src/base_client.rs index 30a1d0aa..f3c76353 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -236,6 +236,7 @@ impl Client { /// /// * `event` - The event that should be handled by the client. pub fn receive_presence_event(&mut self, room_id: &str, event: &PresenceEvent) -> bool { + // this should be guaranteed to find the room that was just created in the `Client::sync` loop. let mut room = self.get_or_create_room(room_id).write().unwrap(); room.receive_presence_event(event) } diff --git a/src/models/room.rs b/src/models/room.rs index 7d1f3933..b354c575 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -22,7 +22,7 @@ use crate::events::collections::all::{RoomEvent, StateEvent}; use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, - member::{MemberEvent, MembershipState}, + member::{MemberEvent, MembershipChange}, name::NameEvent, power_levels::PowerLevelsEvent, }; @@ -148,48 +148,14 @@ impl Room { true } - // fn remove_member(&mut self, event: &MemberEvent) -> bool { - // if let Some(member) = self.members.get_mut(&event.sender.to_string()) { - // let changed = member.membership == event.content.membership; - // member.membership = event.content.membership; - // changed - // } else { - // false - // } - // } - - // fn update_joined_member(&mut self, event: &MemberEvent) -> bool { - // if let Some(member) = self.members.get_mut(&event.state_key) { - // member.update(event); - // } - - // false - // } - - // fn handle_join(&mut self, event: &MemberEvent) -> bool { - // match &event.prev_content { - // Some(c) => match c.membership { - // MembershipState::Join => self.update_joined_member(event), - // MembershipState::Invite => self.add_member(event), - // MembershipState::Leave => self.remove_member(event), - // _ => false, - // }, - // None => self.add_member(event), - // } - // } - - // fn handle_leave(&mut self, event: &MemberEvent) -> bool { - - // } - /// Handle a room.member updating the room state if necessary. /// /// Returns true if the joined member list changed, false otherwise. pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { - match &event.content.membership { - MembershipState::Invite | MembershipState::Join => self.add_member(event), + match event.membership_change() { + MembershipChange::Invited | MembershipChange::Joined => self.add_member(event), _ => { - if let Some(member) = self.members.get_mut(&event.state_key) { + if let Some(member) = self.members.get_mut(&event.sender.to_string()) { member.update_member(event) } else { false @@ -297,98 +263,27 @@ impl Room { /// Receive a presence event from an `IncomingResponse` and updates the client state. /// - /// Returns true if the joined member list changed, false otherwise. + /// Returns true if the specific users presence has changed, false otherwise. /// /// # Arguments /// - /// * `event` - The event of the room. + /// * `event` - The presence event for a specified room member. pub fn receive_presence_event(&mut self, event: &PresenceEvent) -> bool { - let PresenceEvent { - content: - PresenceEventContent { - avatar_url, - currently_active, - displayname, - last_active_ago, - presence, - status_msg, - }, - sender, - } = event; - if let Some(user) = self .members - .get_mut(&sender.to_string()) + .get_mut(&event.sender.to_string()) .map(|m| &mut m.user) { - if user.display_name == *displayname - && user.avatar_url == *avatar_url - && user.presence.as_ref() == Some(presence) - && user.status_msg == *status_msg - && user.last_active_ago == *last_active_ago - && user.currently_active == *currently_active - { + if user.did_update_presence(event) { false } else { - user.presence_events.push(event.clone()); - *user = User { - display_name: displayname.clone(), - avatar_url: avatar_url.clone(), - presence: Some(presence.clone()), - status_msg: status_msg.clone(), - last_active_ago: *last_active_ago, - currently_active: *currently_active, - // TODO better way of moving vec over - events: user.events.clone(), - presence_events: user.presence_events.clone(), - }; + user.update_presence(event); true } } else { // this is probably an error as we have a `PresenceEvent` for a user - // we dont know about + // we don't know about false } } } - -// pub struct User { -// /// The human readable name of the user. -// pub display_name: Option, -// /// The matrix url of the users avatar. -// pub avatar_url: Option, -// /// The presence of the user, if found. -// pub presence: Option, -// /// The presence status message, if found. -// pub status_msg: Option, -// /// The time, in ms, since the user interacted with the server. -// pub last_active_ago: Option, -// /// If the user should be considered active. -// pub currently_active: Option, -// /// The events that created the state of the current user. -// // TODO do we want to hold the whole state or just update our structures. -// pub events: Vec, -// /// The `PresenceEvent`s connected to this user. -// pub presence_events: Vec, -// } - -// pub struct RoomMember { -// /// The unique mxid of the user. -// pub user_id: UserId, -// /// The unique id of the room. -// pub room_id: Option, -// /// If the member is typing. -// pub typing: Option, -// /// The user data for this room member. -// pub user: User, -// /// The users power level. -// pub power_level: Option, -// /// The normalized power level of this `RoomMember` (0-100). -// pub power_level_norm: Option, -// /// The `MembershipState` of this `RoomMember`. -// pub membership: MembershipState, -// /// The human readable name of this room member. -// pub name: String, -// /// The events that created the state of this room member. -// pub events: Vec -// } diff --git a/src/models/room_member.rs b/src/models/room_member.rs index 896fd1c8..99799bad 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -22,7 +22,7 @@ use crate::events::collections::all::{Event, RoomEvent, StateEvent}; use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, - member::{MemberEvent, MemberEventContent, MembershipState}, + member::{MemberEvent, MemberEventContent, MembershipChange, MembershipState}, name::NameEvent, power_levels::PowerLevelsEvent, }; @@ -73,15 +73,33 @@ impl RoomMember { power_level: None, power_level_norm: None, membership: event.content.membership, + // TODO should this be `sender` ?? name: event.state_key.clone(), events: vec![Event::RoomMember(event.clone())], } } pub fn update_member(&mut self, event: &MemberEvent) -> bool { - let changed = self.membership == event.content.membership; - self.membership = event.content.membership; - changed + use MembershipChange::*; + + match event.membership_change() { + ProfileChanged => { + self.user.display_name = event.content.displayname.clone(); + self.user.avatar_url = event.content.avatar_url.clone(); + true + }, + Banned | Kicked | KickedAndBanned + | InvitationRejected | InvitationRevoked + | Left | Unbanned | Joined | Invited => { + self.membership = event.content.membership; + true + }, + NotImplemented => false, + None => false, + // TODO should this be handled somehow ?? + Error => false, + _ => false, + } } pub fn update_power(&mut self, event: &PowerLevelsEvent) -> bool { diff --git a/src/models/user.rs b/src/models/user.rs index f9840e32..2c5c4d57 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -19,7 +19,7 @@ use std::sync::{Arc, RwLock}; use super::UserId; use crate::api::r0 as api; use crate::events::collections::all::{Event, RoomEvent, StateEvent}; -use crate::events::presence::{PresenceEvent, PresenceState}; +use crate::events::presence::{PresenceEvent, PresenceEventContent, PresenceState}; use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, @@ -74,4 +74,67 @@ impl User { presence_events: Vec::default(), } } + + /// If the current `PresenceEvent` updated the state of this `User`. + /// + /// Returns true if the specific users presence has changed, false otherwise. + /// + /// # Arguments + /// + /// * `presence` - The presence event for a this room member. + pub fn did_update_presence(&self, presence: &PresenceEvent) -> bool { + let PresenceEvent { + content: + PresenceEventContent { + avatar_url, + currently_active, + displayname, + last_active_ago, + presence, + status_msg, + }, + .. + } = presence; + self.display_name == *displayname + && self.avatar_url == *avatar_url + && self.presence.as_ref() == Some(presence) + && self.status_msg == *status_msg + && self.last_active_ago == *last_active_ago + && self.currently_active == *currently_active + } + + /// Updates the `User`s presence. + /// + /// This should only be used if `did_update_presence` was true. + /// + /// # Arguments + /// + /// * `presence` - The presence event for a this room member. + pub fn update_presence(&mut self, presence_ev: &PresenceEvent) { + let PresenceEvent { + content: + PresenceEventContent { + avatar_url, + currently_active, + displayname, + last_active_ago, + presence, + status_msg, + }, + .. + } = presence_ev; + + self.presence_events.push(presence_ev.clone()); + *self = User { + display_name: displayname.clone(), + avatar_url: avatar_url.clone(), + presence: Some(presence.clone()), + status_msg: status_msg.clone(), + last_active_ago: *last_active_ago, + currently_active: *currently_active, + // TODO better way of moving vec over + events: self.events.clone(), + presence_events: self.presence_events.clone(), + } + } } From 05b6f4679ab79e4f2591d6c2a5371a04b0a5b5a7 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sun, 29 Mar 2020 08:07:25 -0400 Subject: [PATCH 20/28] cargo fmt/clippy --- src/async_client.rs | 9 ++++++--- src/models/room_member.rs | 13 ++++++------- src/models/user.rs | 4 ++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index e9627682..6b4c7862 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -30,9 +30,9 @@ use url::Url; use ruma_api::{Endpoint, Outgoing}; use ruma_events::collections::all::RoomEvent; +use ruma_events::presence::PresenceEvent; use ruma_events::room::message::MessageEventContent; use ruma_events::EventResult; -use ruma_events::presence::PresenceEvent; pub use ruma_events::EventType; use ruma_identifiers::RoomId; @@ -48,7 +48,8 @@ type RoomEventCallback = Box< >; type PresenceEventCallback = Box< - dyn FnMut(Arc>, Arc>) -> BoxFuture<'static, ()> + Send, + dyn FnMut(Arc>, Arc>) -> BoxFuture<'static, ()> + + Send, >; const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); @@ -405,7 +406,9 @@ impl AsyncClient { /// ``` pub fn add_presence_callback( &mut self, - mut callback: impl FnMut(Arc>, Arc>) -> C + 'static + Send, + mut callback: impl FnMut(Arc>, Arc>) -> C + + 'static + + Send, ) where C: Future + Send, { diff --git a/src/models/room_member.rs b/src/models/room_member.rs index 99799bad..c6803f5d 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -87,13 +87,12 @@ impl RoomMember { self.user.display_name = event.content.displayname.clone(); self.user.avatar_url = event.content.avatar_url.clone(); true - }, - Banned | Kicked | KickedAndBanned - | InvitationRejected | InvitationRevoked - | Left | Unbanned | Joined | Invited => { - self.membership = event.content.membership; - true - }, + } + Banned | Kicked | KickedAndBanned | InvitationRejected | InvitationRevoked | Left + | Unbanned | Joined | Invited => { + self.membership = event.content.membership; + true + } NotImplemented => false, None => false, // TODO should this be handled somehow ?? diff --git a/src/models/user.rs b/src/models/user.rs index 2c5c4d57..786bad6e 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -104,7 +104,7 @@ impl User { } /// Updates the `User`s presence. - /// + /// /// This should only be used if `did_update_presence` was true. /// /// # Arguments @@ -123,7 +123,7 @@ impl User { }, .. } = presence_ev; - + self.presence_events.push(presence_ev.clone()); *self = User { display_name: displayname.clone(), From c89ae2537eb4b721ef379ac96dfb8370845a8245 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sun, 29 Mar 2020 15:54:26 -0400 Subject: [PATCH 21/28] add account data, current room_id calc, unread, push ruleset, ignored users --- src/async_client.rs | 5 +++ src/base_client.rs | 91 ++++++++++++++++++++++++++++++++++++++++++++- src/models/room.rs | 43 ++++++++++++--------- 3 files changed, 121 insertions(+), 18 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 6b4c7862..6a125dec 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -284,6 +284,11 @@ impl AsyncClient { self.base_client.read().await.calculate_room_names() } + /// Calculate the room names this client knows about. + pub async fn current_room_id(&self) -> Option { + self.base_client.read().await.current_room_id() + } + /// Add a callback that will be called every time the client receives a room /// event /// diff --git a/src/base_client.rs b/src/base_client.rs index f3c76353..23d501ac 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -14,6 +14,7 @@ // limitations under the License. use std::collections::HashMap; +use std::convert::TryFrom; #[cfg(feature = "encryption")] use std::result::Result as StdResult; @@ -22,6 +23,10 @@ use crate::api::r0 as api; use crate::error::Result; use crate::events::collections::all::{RoomEvent, StateEvent}; use crate::events::presence::PresenceEvent; +// `NonRoomEvent` is what it is aliased as +use crate::events::collections::only::Event as NonRoomEvent; +use crate::events::ignored_user_list::IgnoredUserListEvent; +use crate::events::push_rules::{ Ruleset, PushRulesEvent}; use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, @@ -29,11 +34,13 @@ use crate::events::room::{ name::NameEvent, }; use crate::events::EventResult; -use crate::identifiers::RoomAliasId; +use crate::identifiers::{RoomAliasId, UserId as Uid}; use crate::models::Room; use crate::session::Session; use std::sync::{Arc, RwLock}; +use js_int::UInt; + #[cfg(feature = "encryption")] use tokio::sync::Mutex; @@ -57,6 +64,27 @@ pub struct RoomName { aliases: Vec, } +#[derive(Clone, Debug, Default)] +pub struct CurrentRoom { + last_active: Option, + current_room_id: Option, +} + +impl CurrentRoom { + pub(crate) fn comes_after(&self, user: &Uid, event: &PresenceEvent) -> bool { + if user == &event.sender { + event.content.last_active_ago < self.last_active + } else { + false + } + } + + pub(crate) fn update(&mut self, room_id: &str, event: &PresenceEvent) { + self.last_active = event.content.last_active_ago; + self.current_room_id = Some(RoomId::try_from(room_id).expect("room id failed CurrentRoom::update")); + } +} + #[derive(Debug)] /// A no IO Client implementation. /// @@ -70,6 +98,13 @@ pub struct Client { pub sync_token: Option, /// A map of the rooms our user is joined in. pub joined_rooms: HashMap>>, + /// The most recent room the logged in user used by `RoomId`. + pub current_room_id: CurrentRoom, + /// A list of ignored users. + pub ignored_users: Vec, + /// The push ruleset for the logged in user. + pub push_ruleset: Option, + #[cfg(feature = "encryption")] olm: Arc>>, } @@ -92,6 +127,9 @@ impl Client { session, sync_token: None, joined_rooms: HashMap::new(), + current_room_id: CurrentRoom::default(), + ignored_users: Vec::new(), + push_ruleset: None, #[cfg(feature = "encryption")] olm: Arc::new(Mutex::new(olm)), }) @@ -147,6 +185,10 @@ impl Client { .collect() } + pub(crate) fn current_room_id(&self) -> Option { + self.current_room_id.current_room_id.clone() + } + pub(crate) fn get_or_create_room(&mut self, room_id: &str) -> &mut Arc> { #[allow(clippy::or_fun_call)] self.joined_rooms @@ -162,6 +204,32 @@ impl Client { )))) } + /// Handle a m.ignored_user_list event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub(crate) fn handle_ignored_users(&mut self, event: &IgnoredUserListEvent) -> bool { + // TODO use actual UserId instead of string? + if self.ignored_users == event.content.ignored_users.iter().map(|u| u.to_string()).collect::>() { + false + } else { + self.ignored_users = event.content.ignored_users.iter().map(|u| u.to_string()).collect(); + true + } + } + + /// Handle a m.ignored_user_list event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub(crate) fn handle_push_rules(&mut self, event: &PushRulesEvent) -> bool { + // TODO this is basically a stub + if self.push_ruleset.as_ref() == Some(&event.content.global) { + false + } else { + self.push_ruleset = Some(event.content.global.clone()); + true + } + } + /// Receive a timeline event for a joined room and update the client state. /// /// If the event was a encrypted room event and decryption was successful @@ -236,11 +304,32 @@ impl Client { /// /// * `event` - The event that should be handled by the client. pub fn receive_presence_event(&mut self, room_id: &str, event: &PresenceEvent) -> bool { + let user_id = &self.session.as_ref().expect("to receive events you must be logged in").user_id; + if self.current_room_id.comes_after(user_id, event) { + self.current_room_id.update(room_id, event); + } // this should be guaranteed to find the room that was just created in the `Client::sync` loop. let mut room = self.get_or_create_room(room_id).write().unwrap(); room.receive_presence_event(event) } + /// Receive a presence event from an `IncomingResponse` and updates the client state. + /// + /// This will only update the user if found in the current room looped through by `AsyncClient::sync`. + /// Returns true if the specific users presence has changed, false otherwise. + /// + /// # Arguments + /// + /// * `event` - The presence event for a specified room member. + pub fn receive_account_data(&mut self, room_id: &str, event: &NonRoomEvent) -> bool { + match event { + NonRoomEvent::IgnoredUserList(iu) => self.handle_ignored_users(iu), + NonRoomEvent::Presence(p) => self.receive_presence_event(room_id, p), + NonRoomEvent::PushRules(pr) => self.handle_push_rules(pr), + _ => false, + } + } + /// Receive a response from a sync call. /// /// # Arguments diff --git a/src/models/room.rs b/src/models/room.rs index b354c575..bdbea9a8 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -17,7 +17,7 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; use super::{RoomMember, User, UserId}; -use crate::api::r0 as api; + use crate::events::collections::all::{RoomEvent, StateEvent}; use crate::events::room::{ aliases::AliasesEvent, @@ -33,6 +33,8 @@ use crate::events::{ use crate::identifiers::RoomAliasId; use crate::session::Session; +use js_int::UInt; + #[cfg(feature = "encryption")] use tokio::sync::Mutex; @@ -69,6 +71,10 @@ pub struct Room { pub typing_users: Vec, /// A flag indicating if the room is encrypted. pub encrypted: bool, + /// Number of unread notifications with highlight flag set. + pub unread_highlight: Option, + /// Number of unread notifications. + pub unread_notifications: Option, } impl RoomName { @@ -133,6 +139,8 @@ impl Room { members: HashMap::new(), typing_users: Vec::new(), encrypted: false, + unread_highlight: None, + unread_notifications: None, } } @@ -148,22 +156,6 @@ impl Room { true } - /// Handle a room.member updating the room state if necessary. - /// - /// Returns true if the joined member list changed, false otherwise. - pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { - match event.membership_change() { - MembershipChange::Invited | MembershipChange::Joined => self.add_member(event), - _ => { - if let Some(member) = self.members.get_mut(&event.sender.to_string()) { - member.update_member(event) - } else { - false - } - } - } - } - /// Add to the list of `RoomAliasId`s. fn room_aliases(&mut self, alias: &RoomAliasId) -> bool { self.room_name.push_alias(alias.clone()); @@ -181,6 +173,22 @@ impl Room { true } + /// Handle a room.member updating the room state if necessary. + /// + /// Returns true if the joined member list changed, false otherwise. + pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { + match event.membership_change() { + MembershipChange::Invited | MembershipChange::Joined => self.add_member(event), + _ => { + if let Some(member) = self.members.get_mut(&event.sender.to_string()) { + member.update_member(event) + } else { + false + } + } + } + } + /// Handle a room.aliases event, updating the room state if necessary. /// /// Returns true if the room name changed, false otherwise. @@ -263,6 +271,7 @@ impl Room { /// Receive a presence event from an `IncomingResponse` and updates the client state. /// + /// This will only update the user if found in the current room looped through by `AsyncClient::sync`. /// Returns true if the specific users presence has changed, false otherwise. /// /// # Arguments From 14a8d04a03437953c174fc43451903a8045ee6b5 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sun, 29 Mar 2020 16:24:31 -0400 Subject: [PATCH 22/28] account data loop --- src/async_client.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/async_client.rs b/src/async_client.rs index 6a125dec..ef6dd300 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -532,8 +532,18 @@ impl AsyncClient { } } + // look at AccountData to further cut down users by collecting ignored users + for account_data in &room.account_data.events { + let mut client = self.base_client.write().await; + if let EventResult::Ok(e) = account_data { + client.receive_account_data(&room_id_string, e); + } + } + + // TODO do we need `IncomingEphemeral` events? + // After the room has been created and state/timeline events accounted for we use the room_id of the newly created - // to add any presence events that relate to a user in the current room. This is not super + // room to add any presence events that relate to a user in the current room. This is not super // efficient but we need a room_id so we would loop through now or later. for presence in &response.presence.events { let mut client = self.base_client.write().await; From 705464fb51dc6fbbc94edd16e2c91124ac5fa8a7 Mon Sep 17 00:00:00 2001 From: Devin R Date: Mon, 30 Mar 2020 07:14:33 -0400 Subject: [PATCH 23/28] added a few todos, cargo fmt/clippy --- src/async_client.rs | 1 + src/base_client.rs | 27 ++++++++++++++++++++++----- src/models/mod.rs | 1 - src/models/room_state.rs | 14 -------------- src/models/user.rs | 2 +- 5 files changed, 24 insertions(+), 21 deletions(-) delete mode 100644 src/models/room_state.rs diff --git a/src/async_client.rs b/src/async_client.rs index ef6dd300..ed1831f7 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -533,6 +533,7 @@ impl AsyncClient { } // look at AccountData to further cut down users by collecting ignored users + // TODO actually use the ignored users for account_data in &room.account_data.events { let mut client = self.base_client.write().await; if let EventResult::Ok(e) = account_data { diff --git a/src/base_client.rs b/src/base_client.rs index 23d501ac..e6f15f2d 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -26,7 +26,7 @@ use crate::events::presence::PresenceEvent; // `NonRoomEvent` is what it is aliased as use crate::events::collections::only::Event as NonRoomEvent; use crate::events::ignored_user_list::IgnoredUserListEvent; -use crate::events::push_rules::{ Ruleset, PushRulesEvent}; +use crate::events::push_rules::{PushRulesEvent, Ruleset}; use crate::events::room::{ aliases::AliasesEvent, canonical_alias::CanonicalAliasEvent, @@ -81,7 +81,8 @@ impl CurrentRoom { pub(crate) fn update(&mut self, room_id: &str, event: &PresenceEvent) { self.last_active = event.content.last_active_ago; - self.current_room_id = Some(RoomId::try_from(room_id).expect("room id failed CurrentRoom::update")); + self.current_room_id = + Some(RoomId::try_from(room_id).expect("room id failed CurrentRoom::update")); } } @@ -209,10 +210,22 @@ impl Client { /// Returns true if the room name changed, false otherwise. pub(crate) fn handle_ignored_users(&mut self, event: &IgnoredUserListEvent) -> bool { // TODO use actual UserId instead of string? - if self.ignored_users == event.content.ignored_users.iter().map(|u| u.to_string()).collect::>() { + if self.ignored_users + == event + .content + .ignored_users + .iter() + .map(|u| u.to_string()) + .collect::>() + { false } else { - self.ignored_users = event.content.ignored_users.iter().map(|u| u.to_string()).collect(); + self.ignored_users = event + .content + .ignored_users + .iter() + .map(|u| u.to_string()) + .collect(); true } } @@ -304,7 +317,11 @@ impl Client { /// /// * `event` - The event that should be handled by the client. pub fn receive_presence_event(&mut self, room_id: &str, event: &PresenceEvent) -> bool { - let user_id = &self.session.as_ref().expect("to receive events you must be logged in").user_id; + let user_id = &self + .session + .as_ref() + .expect("to receive events you must be logged in") + .user_id; if self.current_room_id.comes_after(user_id, event) { self.current_room_id.update(room_id, event); } diff --git a/src/models/mod.rs b/src/models/mod.rs index cbc7422e..8fbc08d4 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,6 +1,5 @@ mod room; mod room_member; -mod room_state; mod user; pub use room::{Room, RoomName}; diff --git a/src/models/room_state.rs b/src/models/room_state.rs deleted file mode 100644 index f4404b09..00000000 --- a/src/models/room_state.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 Damir Jelić -// Copyright 2020 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/src/models/user.rs b/src/models/user.rs index 786bad6e..37c123a1 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -128,7 +128,7 @@ impl User { *self = User { display_name: displayname.clone(), avatar_url: avatar_url.clone(), - presence: Some(presence.clone()), + presence: Some(*presence), status_msg: status_msg.clone(), last_active_ago: *last_active_ago, currently_active: *currently_active, From 4391fb695e80f2c8366dc645f6fa0bd2f762aa0c Mon Sep 17 00:00:00 2001 From: Devin R Date: Mon, 30 Mar 2020 14:18:08 -0400 Subject: [PATCH 24/28] looked over for review --- src/async_client.rs | 8 ++++---- src/models/room_member.rs | 3 --- tests/async_client_tests.rs | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index ed1831f7..039a29ad 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -352,7 +352,7 @@ impl AsyncClient { futures.push(Box::new(future)); } - /// Add a callback that will be called every time the client receives a room + /// Add a callback that will be called every time the client receives a presence /// event /// /// # Arguments @@ -433,9 +433,9 @@ impl AsyncClient { /// * `password` - The password of the user. /// /// * `device_id` - A unique id that will be associated with this session. If - /// not given the homeserver will create one. Can be an exising + /// not given the homeserver will create one. Can be an existing /// device_id from a previous login call. Note that this should be done - /// only if the client also holds the encryption keys for this devcie. + /// only if the client also holds the encryption keys for this device. #[instrument(skip(password))] pub async fn login + std::fmt::Debug>( &mut self, @@ -462,7 +462,7 @@ impl AsyncClient { Ok(response) } - /// Synchronise the client's state with the latest state on the server. + /// Synchronize the client's state with the latest state on the server. /// /// # Arguments /// diff --git a/src/models/room_member.rs b/src/models/room_member.rs index c6803f5d..c5319a6c 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -13,9 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; - use super::User; use crate::api::r0 as api; use crate::events::collections::all::{Event, RoomEvent, StateEvent}; diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 0c213d0c..3811c3f9 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -88,5 +88,5 @@ async fn timeline() { client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost").await ); - println!("{:#?}", &client.base_client().read().await.joined_rooms); + // println!("{:#?}", &client.base_client().read().await.joined_rooms); } From 62159cc6dc8bdea5895358359b6d5c95c63e6a56 Mon Sep 17 00:00:00 2001 From: Devin R Date: Tue, 31 Mar 2020 06:57:29 -0400 Subject: [PATCH 25/28] fix review issues --- src/async_client.rs | 2 +- src/base_client.rs | 21 ++++++++++++------ src/event_emitter/mod.rs | 45 --------------------------------------- src/lib.rs | 1 - src/models/room.rs | 8 +++---- src/models/room_member.rs | 10 ++++++--- 6 files changed, 27 insertions(+), 60 deletions(-) delete mode 100644 src/event_emitter/mod.rs diff --git a/src/async_client.rs b/src/async_client.rs index 039a29ad..fa5c30cc 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -541,7 +541,7 @@ impl AsyncClient { } } - // TODO do we need `IncomingEphemeral` events? + // TODO `IncomingEphemeral` events for typing events // After the room has been created and state/timeline events accounted for we use the room_id of the newly created // room to add any presence events that relate to a user in the current room. This is not super diff --git a/src/base_client.rs b/src/base_client.rs index e6f15f2d..613c189c 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -205,11 +205,16 @@ impl Client { )))) } + pub(crate) fn get_room(&mut self, room_id: &str) -> Option<&mut Arc>> { + #[allow(clippy::or_fun_call)] + self.joined_rooms.get_mut(room_id) + } + /// Handle a m.ignored_user_list event, updating the room state if necessary. /// /// Returns true if the room name changed, false otherwise. pub(crate) fn handle_ignored_users(&mut self, event: &IgnoredUserListEvent) -> bool { - // TODO use actual UserId instead of string? + // FIXME when UserId becomes more like a &str wrapper in ruma-identifiers if self.ignored_users == event .content @@ -306,7 +311,7 @@ impl Client { room.receive_state_event(event) } - /// Receive a presence event from an `IncomingResponse` and updates the client state. + /// Receive a presence event from a sync response and updates the client state. /// /// Returns true if the membership list of the room changed, false /// otherwise. @@ -325,12 +330,16 @@ impl Client { if self.current_room_id.comes_after(user_id, event) { self.current_room_id.update(room_id, event); } - // this should be guaranteed to find the room that was just created in the `Client::sync` loop. - let mut room = self.get_or_create_room(room_id).write().unwrap(); - room.receive_presence_event(event) + // this should be the room that was just created in the `Client::sync` loop. + if let Some(room) = self.get_room(room_id) { + let mut room = room.write().unwrap(); + room.receive_presence_event(event) + } else { + false + } } - /// Receive a presence event from an `IncomingResponse` and updates the client state. + /// Receive a presence event from a sync response and updates the client state. /// /// This will only update the user if found in the current room looped through by `AsyncClient::sync`. /// Returns true if the specific users presence has changed, false otherwise. diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs deleted file mode 100644 index 287d59f3..00000000 --- a/src/event_emitter/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020 Damir Jelić -// Copyright 2020 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::events::collections::all::RoomEvent; -use crate::models::Room; - -// JUST AN IDEA -// - -/// This is just a thought I had. Making users impl a trait instead of writing callbacks for events -/// could give the chance for really good documentation for each event? -/// It would look something like this -/// -/// ```rust,ignore -/// use matrix-sdk::{AsyncClient, EventEmitter}; -/// -/// struct MyAppClient; -/// -/// impl EventEmitter for MyAppClient { -/// fn on_room_member(&mut self, room: &Room, event: &RoomEvent) { ... } -/// } -/// async fn main() { -/// let cl = AsyncClient::with_emitter(MyAppClient); -/// } -/// ``` -/// -/// And in `AsyncClient::sync` there could be a switch case that calls the corresponding method on -/// the `Box -pub trait EventEmitter { - fn on_room_name(&mut self, _: &Room, _: &RoomEvent) {} - /// Any event that alters the state of the room's members - fn on_room_member(&mut self, _: &Room, _: &RoomEvent) {} -} diff --git a/src/lib.rs b/src/lib.rs index 6014c21d..c8813589 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,7 +35,6 @@ pub use ruma_identifiers as identifiers; mod async_client; mod base_client; mod error; -mod event_emitter; mod models; mod session; diff --git a/src/models/room.rs b/src/models/room.rs index bdbea9a8..9e847295 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -69,6 +69,7 @@ pub struct Room { pub members: HashMap, /// A list of users that are currently typing. pub typing_users: Vec, + // TODO when encryption events are handled we store algorithm used and rotation time. /// A flag indicating if the room is encrypted. pub encrypted: bool, /// Number of unread notifications with highlight flag set. @@ -94,7 +95,7 @@ impl RoomName { } pub fn calculate_name(&self, room_id: &str, members: &HashMap) -> String { - // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 + // https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room. // the order in which we check for a name ^^ if let Some(name) = &self.name { name.clone() @@ -103,7 +104,6 @@ impl RoomName { } else if !self.aliases.is_empty() { self.aliases[0].alias().to_string() } else { - // TODO let mut names = members .values() .flat_map(|m| m.user.display_name.clone()) @@ -111,7 +111,7 @@ impl RoomName { .collect::>(); if names.is_empty() { - // TODO implement the rest of matrix-js-sdk handling of room names + // TODO implement the rest of display name for room spec format!("Room {}", room_id) } else { // stabilize order @@ -180,7 +180,7 @@ impl Room { match event.membership_change() { MembershipChange::Invited | MembershipChange::Joined => self.add_member(event), _ => { - if let Some(member) = self.members.get_mut(&event.sender.to_string()) { + if let Some(member) = self.members.get_mut(&event.state_key) { member.update_member(event) } else { false diff --git a/src/models/room_member.rs b/src/models/room_member.rs index c5319a6c..d86eeff0 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::convert::TryFrom; + use super::User; use crate::api::r0 as api; use crate::events::collections::all::{Event, RoomEvent, StateEvent}; @@ -36,8 +38,11 @@ use crate::crypto::{OlmMachine, OneTimeKeys}; #[cfg(feature = "encryption")] use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; +// Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob. + #[derive(Debug)] /// A Matrix room member. +/// pub struct RoomMember { /// The unique mxid of the user. pub user_id: UserId, @@ -64,13 +69,12 @@ impl RoomMember { let user = User::new(event); Self { room_id: event.room_id.as_ref().map(|id| id.to_string()), - user_id: event.sender.clone(), + user_id: UserId::try_from(event.state_key.as_str()).unwrap(), typing: None, user, power_level: None, power_level_norm: None, membership: event.content.membership, - // TODO should this be `sender` ?? name: event.state_key.clone(), events: vec![Event::RoomMember(event.clone())], } @@ -92,7 +96,7 @@ impl RoomMember { } NotImplemented => false, None => false, - // TODO should this be handled somehow ?? + // we ignore the error here as only a buggy or malicious server would send this Error => false, _ => false, } From 0d79c3574e8ac56562a9b43345849af75cb72933 Mon Sep 17 00:00:00 2001 From: Devin R Date: Tue, 31 Mar 2020 07:07:14 -0400 Subject: [PATCH 26/28] remove base_client method --- src/async_client.rs | 6 ------ tests/async_client_tests.rs | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index fa5c30cc..2d0e0097 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -268,12 +268,6 @@ impl AsyncClient { &self.homeserver } - #[doc(hidden)] - /// Access to the underlying `BaseClient`. Used for testing and debugging so far. - pub fn base_client(&self) -> Arc> { - Arc::clone(&self.base_client) - } - /// Calculate the room name from a `RoomId`, returning a string. pub async fn get_room_name(&self, room_id: &str) -> Option { self.base_client.read().await.calculate_room_name(room_id) diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 3811c3f9..e37de610 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -87,6 +87,4 @@ async fn timeline() { Some("tutorial".into()), client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost").await ); - - // println!("{:#?}", &client.base_client().read().await.joined_rooms); } From df58c60d2e552b6eaf315c275a1374ee80290e41 Mon Sep 17 00:00:00 2001 From: Devin R Date: Tue, 31 Mar 2020 09:01:48 -0400 Subject: [PATCH 27/28] add tests in models files, run coverage --- src/async_client.rs | 2 +- src/base_client.rs | 61 ++++++++++++++++++++++++- src/models/room.rs | 54 ++++++++++++++++++++++ src/models/room_member.rs | 90 ++++++++++++++++++++++++++++++++++++- tests/async_client_tests.rs | 32 ++++++++++++- tests/data/sync.json | 20 ++++++++- 6 files changed, 251 insertions(+), 8 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 2d0e0097..43e2c4f3 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -62,7 +62,7 @@ pub struct AsyncClient { /// The underlying HTTP client. http_client: reqwest::Client, /// User session data. - base_client: Arc>, + pub(crate) base_client: Arc>, /// The transaction id. transaction_id: Arc, /// Event callbacks diff --git a/src/base_client.rs b/src/base_client.rs index 613c189c..6333992f 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -71,9 +71,20 @@ pub struct CurrentRoom { } impl CurrentRoom { + // TODO when UserId is isomorphic to &str clean this up. pub(crate) fn comes_after(&self, user: &Uid, event: &PresenceEvent) -> bool { - if user == &event.sender { - event.content.last_active_ago < self.last_active + let u = user.to_string(); + let u = u.split(':').next(); + + let s = event.sender.to_string(); + let s = s.split(':').next(); + + if u == s { + if self.last_active.is_none() { + true + } else { + event.content.last_active_ago < self.last_active + } } else { false } @@ -327,6 +338,7 @@ impl Client { .as_ref() .expect("to receive events you must be logged in") .user_id; + if self.current_room_id.comes_after(user_id, event) { self.current_room_id.update(room_id, event); } @@ -424,3 +436,48 @@ impl Client { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + + use crate::events::room::member::MembershipState; + use crate::identifiers::UserId; + use crate::{AsyncClient, Session, SyncSettings}; + + use mockito::{mock, Matcher}; + use tokio::runtime::Runtime; + use url::Url; + + use std::convert::TryFrom; + use std::str::FromStr; + use std::time::Duration; + + #[tokio::test] + async fn account_data() { + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:example.com").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ) + .with_status(200) + .with_body_from_file("tests/data/sync.json") + .create(); + + let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync(sync_settings).await.unwrap(); + + let bc = &client.base_client.read().await; + assert_eq!(1, bc.ignored_users.len()) + } +} diff --git a/src/models/room.rs b/src/models/room.rs index 9e847295..428d7342 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -296,3 +296,57 @@ impl Room { } } } + +#[cfg(test)] +mod test { + use super::*; + + use crate::events::room::member::MembershipState; + use crate::identifiers::UserId; + use crate::{AsyncClient, Session, SyncSettings}; + + use mockito::{mock, Matcher}; + use tokio::runtime::Runtime; + use url::Url; + + use std::convert::TryFrom; + use std::str::FromStr; + use std::time::Duration; + + #[tokio::test] + async fn user_presence() { + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:example.com").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ) + .with_status(200) + .with_body_from_file("tests/data/sync.json") + .create(); + + let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync(sync_settings).await.unwrap(); + + let rooms = &client.base_client.read().await.joined_rooms; + let room = &rooms + .get("!SVkFJHzfwvuaIEawgC:localhost") + .unwrap() + .read() + .unwrap(); + + assert_eq!(2, room.members.len()); + for (id, member) in &room.members { + assert_eq!(MembershipState::Join, member.membership); + } + } +} diff --git a/src/models/room_member.rs b/src/models/room_member.rs index d86eeff0..9c806c18 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -110,10 +110,10 @@ impl RoomMember { let mut changed = false; if let Some(user_power) = event.content.users.get(&self.user_id) { - changed = self.power_level == Some(*user_power); + changed = self.power_level != Some(*user_power); self.power_level = Some(*user_power); } else { - changed = self.power_level == Some(event.content.users_default); + changed = self.power_level != Some(event.content.users_default); self.power_level = Some(event.content.users_default); } @@ -124,3 +124,89 @@ impl RoomMember { changed } } + +#[cfg(test)] +mod test { + use super::*; + + use crate::identifiers::{EventId, RoomId, UserId}; + use crate::{AsyncClient, Session, SyncSettings}; + + use js_int::{Int, UInt}; + use mockito::{mock, Matcher}; + use tokio::runtime::Runtime; + use url::Url; + + use std::collections::HashMap; + use std::convert::TryFrom; + use std::str::FromStr; + use std::time::Duration; + + use crate::events::room::power_levels::{ + NotificationPowerLevels, PowerLevelsEvent, PowerLevelsEventContent, + }; + + #[tokio::test] + async fn member_power() { + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:example.com").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ) + .with_status(200) + .with_body_from_file("tests/data/sync.json") + .create(); + + let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync(sync_settings).await.unwrap(); + + let mut rooms = client.base_client.write().await.joined_rooms.clone(); + let mut room = rooms + .get_mut("!SVkFJHzfwvuaIEawgC:localhost") + .unwrap() + .write() + .unwrap(); + + for (id, member) in &mut room.members { + let power = power_levels(); + assert!(member.update_power(&power)); + assert_eq!(MembershipState::Join, member.membership); + } + } + + fn power_levels() -> PowerLevelsEvent { + PowerLevelsEvent { + content: PowerLevelsEventContent { + ban: Int::new(40).unwrap(), + events: HashMap::default(), + events_default: Int::new(40).unwrap(), + invite: Int::new(40).unwrap(), + kick: Int::new(40).unwrap(), + redact: Int::new(40).unwrap(), + state_default: Int::new(40).unwrap(), + users: HashMap::default(), + users_default: Int::new(40).unwrap(), + notifications: NotificationPowerLevels { + room: Int::new(35).unwrap(), + }, + }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UInt::new(1520372800469).unwrap(), + prev_content: None, + room_id: RoomId::try_from("!roomid:room.com").ok(), + unsigned: None, + sender: UserId::try_from("@example:example.com").unwrap(), + state_key: "@example:example.com".into(), + } + } +} diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index e37de610..415dec98 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -59,7 +59,7 @@ async fn sync() { } #[tokio::test] -async fn timeline() { +async fn room_names() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); let session = Session { @@ -88,3 +88,33 @@ async fn timeline() { client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost").await ); } + +#[tokio::test] +async fn current_room() { + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:example.com").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ) + .with_status(200) + .with_body_from_file("tests/data/sync.json") + .create(); + + let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync(sync_settings).await.unwrap(); + + assert_eq!( + Some("!SVkFJHzfwvuaIEawgC:localhost".into()), + client.current_room_id().await.map(|id| id.to_string()) + ); +} diff --git a/tests/data/sync.json b/tests/data/sync.json index d873fb47..32cd4629 100644 --- a/tests/data/sync.json +++ b/tests/data/sync.json @@ -12,7 +12,23 @@ "join": { "!SVkFJHzfwvuaIEawgC:localhost": { "account_data": { - "events": [] + "events": [ + { + "content": { + "event_id": "$someplace:example.org" + }, + "room_id": "!roomid:room.com", + "type": "m.fully_read" + }, + { + "content": { + "ignored_users": { + "@someone:example.org": {} + } + }, + "type": "m.ignored_user_list" + } + ] }, "ephemeral": { "events": [ @@ -248,7 +264,7 @@ "content": { "avatar_url": "mxc://localhost:wefuiwegh8742w", "currently_active": false, - "last_active_ago": 2478593, + "last_active_ago": 1, "presence": "online", "status_msg": "Making cupcakes" }, From c3f4a946cb45364eb24bc5b53869006db95a4027 Mon Sep 17 00:00:00 2001 From: Devin R Date: Tue, 31 Mar 2020 15:18:27 -0400 Subject: [PATCH 28/28] remove split on UserId, update tests to correct domain --- src/base_client.rs | 9 +-------- src/models/room.rs | 2 +- src/models/room_member.rs | 2 +- tests/async_client_tests.rs | 6 +++--- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/base_client.rs b/src/base_client.rs index 6333992f..b910d53d 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -73,13 +73,7 @@ pub struct CurrentRoom { impl CurrentRoom { // TODO when UserId is isomorphic to &str clean this up. pub(crate) fn comes_after(&self, user: &Uid, event: &PresenceEvent) -> bool { - let u = user.to_string(); - let u = u.split(':').next(); - - let s = event.sender.to_string(); - let s = s.split(':').next(); - - if u == s { + if user == &event.sender { if self.last_active.is_none() { true } else { @@ -217,7 +211,6 @@ impl Client { } pub(crate) fn get_room(&mut self, room_id: &str) -> Option<&mut Arc>> { - #[allow(clippy::or_fun_call)] self.joined_rooms.get_mut(room_id) } diff --git a/src/models/room.rs b/src/models/room.rs index 428d7342..5e1ba558 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -319,7 +319,7 @@ mod test { let session = Session { access_token: "1234".to_owned(), - user_id: UserId::try_from("@example:example.com").unwrap(), + user_id: UserId::try_from("@example:localhost").unwrap(), device_id: "DEVICEID".to_owned(), }; diff --git a/src/models/room_member.rs b/src/models/room_member.rs index 9c806c18..91c2b71d 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -152,7 +152,7 @@ mod test { let session = Session { access_token: "1234".to_owned(), - user_id: UserId::try_from("@example:example.com").unwrap(), + user_id: UserId::try_from("@example:localhost").unwrap(), device_id: "DEVICEID".to_owned(), }; diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 415dec98..5403eea7 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -35,7 +35,7 @@ async fn sync() { let session = Session { access_token: "1234".to_owned(), - user_id: UserId::try_from("@example:example.com").unwrap(), + user_id: UserId::try_from("@example:localhost").unwrap(), device_id: "DEVICEID".to_owned(), }; @@ -64,7 +64,7 @@ async fn room_names() { let session = Session { access_token: "1234".to_owned(), - user_id: UserId::try_from("@example:example.com").unwrap(), + user_id: UserId::try_from("@example:localhost").unwrap(), device_id: "DEVICEID".to_owned(), }; @@ -95,7 +95,7 @@ async fn current_room() { let session = Session { access_token: "1234".to_owned(), - user_id: UserId::try_from("@example:example.com").unwrap(), + user_id: UserId::try_from("@example:localhost").unwrap(), device_id: "DEVICEID".to_owned(), };