From 20618e7a2098650106ae1443a8dee818a5f13273 Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 27 Mar 2020 16:14:16 -0400 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 bb89db30cb78d8fc9a41d9886e26980973eb6de0 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 28 Mar 2020 07:52:25 -0400 Subject: [PATCH 8/8] 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, } }