From 305633635da4ac1a88f3f6ff6a06c38110c59430 Mon Sep 17 00:00:00 2001 From: Devin R Date: Thu, 23 Apr 2020 06:45:00 -0400 Subject: [PATCH 1/4] room: add tombstone event handling --- src/models/room.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/models/room.rs b/src/models/room.rs index 06d9c69a..ebcc012e 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -28,13 +28,14 @@ use crate::events::room::{ member::{MemberEvent, MembershipChange}, name::NameEvent, power_levels::{NotificationPowerLevels, PowerLevelsEvent, PowerLevelsEventContent}, + tombstone::TombstoneEvent, }; use crate::events::EventType; use crate::identifiers::{RoomAliasId, RoomId, UserId}; use js_int::{Int, UInt}; -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] /// `RoomName` allows the calculation of a text room name. pub struct RoomName { /// The displayed name of the room. @@ -82,8 +83,16 @@ pub struct PowerLevels { pub notifications: Int, } +#[derive(Debug, PartialEq, Eq)] +pub struct Tombstone { + /// A server-defined message. + body: String, + /// The room that is now active. + replacement: RoomId, +} + #[derive(Debug)] -/// A Matrix rooom. +/// A Matrix room. pub struct Room { /// The unique id of the room. pub room_id: RoomId, @@ -106,6 +115,8 @@ pub struct Room { pub unread_highlight: Option, /// Number of unread notifications. pub unread_notifications: Option, + /// The tombstone state of this room. + pub tombstone: Option, } impl RoomName { @@ -175,6 +186,7 @@ impl Room { encrypted: false, unread_highlight: None, unread_notifications: None, + tombstone: None, } } @@ -257,8 +269,8 @@ impl Room { invited_member_count, } = summary; self.room_name.heroes = heroes.clone(); - self.room_name.invited_member_count = invited_member_count.clone(); - self.room_name.joined_member_count = joined_member_count.clone(); + self.room_name.invited_member_count = *invited_member_count; + self.room_name.joined_member_count = *joined_member_count; } /// Handle a room.member updating the room state if necessary. @@ -335,6 +347,14 @@ impl Room { updated } + fn handle_tombstone(&mut self, event: &TombstoneEvent) -> bool { + self.tombstone = Some(Tombstone { + body: event.content.body.clone(), + replacement: event.content.replacement_room.clone(), + }); + true + } + fn handle_encryption_event(&mut self, _event: &EncryptionEvent) -> bool { self.encrypted = true; true @@ -357,6 +377,7 @@ impl Room { RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), // power levels of the room members RoomEvent::RoomPowerLevels(p) => self.handle_power_level(p), + RoomEvent::RoomTombstone(t) => self.handle_tombstone(t), RoomEvent::RoomEncryption(e) => self.handle_encryption_event(e), _ => false, } @@ -376,6 +397,7 @@ impl Room { StateEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), StateEvent::RoomAliases(a) => self.handle_room_aliases(a), StateEvent::RoomPowerLevels(p) => self.handle_power_level(p), + StateEvent::RoomTombstone(t) => self.handle_tombstone(t), StateEvent::RoomEncryption(e) => self.handle_encryption_event(e), _ => false, } From ef4d69b0accd5a68bf08a724567f646c938cfdf4 Mon Sep 17 00:00:00 2001 From: Devin R Date: Thu, 23 Apr 2020 06:53:34 -0400 Subject: [PATCH 2/4] room: fix broken calculate_name, used UInt::max needed min, heroes is always empty --- src/models/room.rs | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/models/room.rs b/src/models/room.rs index ebcc012e..89af2e19 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -139,23 +139,40 @@ impl RoomName { // 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() + let name = name.trim(); + name.to_string() } else if let Some(alias) = &self.canonical_alias { - alias.alias().to_string() - } else if !self.aliases.is_empty() { - self.aliases[0].alias().to_string() + let alias = alias.alias().trim(); + alias.to_string() + } else if !self.aliases.is_empty() && !self.aliases[0].alias().is_empty() { + self.aliases[0].alias().trim().to_string() } else { - let joined = self.joined_member_count.unwrap_or(UInt::max_value()); - let invited = self.invited_member_count.unwrap_or(UInt::max_value()); + let joined = self.joined_member_count.unwrap_or(UInt::min_value()); + let invited = self.invited_member_count.unwrap_or(UInt::min_value()); let heroes = UInt::new(self.heroes.len() as u64).unwrap(); let one = UInt::new(1).unwrap(); - if heroes >= (joined + invited - one) { - let mut names = self.heroes.iter().take(3).cloned().collect::>(); + let invited_joined = if invited + joined == UInt::min_value() { + UInt::min_value() + } else { + invited + joined - one + }; + + // TODO this should use `self.heroes but it is always empty?? + if heroes >= invited_joined { + let mut names = members + .values() + .take(3) + .map(|mem| mem.user_id.localpart().to_string()) + .collect::>(); names.sort(); names.join(", ") - } else if heroes < (joined + invited - one) && invited + joined > one { - let mut names = self.heroes.iter().take(3).cloned().collect::>(); + } else if heroes < invited_joined && invited + joined > one { + let mut names = members + .values() + .take(3) + .map(|mem| mem.user_id.localpart().to_string()) + .collect::>(); names.sort(); // TODO what is the length the spec wants us to use here and in the `else` format!("{}, and {} others", names.join(", "), (joined + invited)) From 07053cfe2637b46c3b998aab91af466235df44ac Mon Sep 17 00:00:00 2001 From: Devin R Date: Thu, 23 Apr 2020 07:05:59 -0400 Subject: [PATCH 3/4] room/ev_emitter: add tombstone to emitted events --- src/base_client.rs | 14 ++++++++++++++ src/event_emitter/mod.rs | 6 ++++++ src/models/room.rs | 1 + src/request_builder.rs | 7 +++---- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/base_client.rs b/src/base_client.rs index 335897b4..d79e94dc 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -637,6 +637,13 @@ impl Client { } } } + RoomEvent::RoomTombstone(tomb) => { + if let Some(ee) = &self.event_emitter { + if let Some(room) = self.get_room(&room_id) { + ee.on_room_tombstone(Arc::clone(&room), &tomb).await; + } + } + } _ => {} } } @@ -693,6 +700,13 @@ impl Client { } } } + StateEvent::RoomTombstone(tomb) => { + if let Some(ee) = &self.event_emitter { + if let Some(room) = self.get_room(&room_id) { + ee.on_room_tombstone(Arc::clone(&room), &tomb).await; + } + } + } _ => {} } } diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs index 2d675521..b233b2da 100644 --- a/src/event_emitter/mod.rs +++ b/src/event_emitter/mod.rs @@ -29,6 +29,7 @@ use crate::events::{ name::NameEvent, power_levels::PowerLevelsEvent, redaction::RedactionEvent, + tombstone::TombstoneEvent, }, }; use crate::models::Room; @@ -98,6 +99,8 @@ pub trait EventEmitter: Send + Sync { async fn on_room_redaction(&self, _: Arc>, _: &RedactionEvent) {} /// Fires when `AsyncClient` receives a `RoomEvent::RoomPowerLevels` event. async fn on_room_power_levels(&self, _: Arc>, _: &PowerLevelsEvent) {} + /// Fires when `AsyncClient` receives a `RoomEvent::Tombstone` event. + async fn on_room_tombstone(&self, _: Arc>, _: &TombstoneEvent) {} // `RoomEvent`s from `IncomingState` /// Fires when `AsyncClient` receives a `StateEvent::RoomMember` event. @@ -167,6 +170,9 @@ mod test { async fn on_room_power_levels(&self, _: Arc>, _: &PowerLevelsEvent) { self.0.lock().await.push("power".to_string()) } + async fn on_room_tombstone(&self, _: Arc>, _: &TombstoneEvent) { + self.0.lock().await.push("tombstone".to_string()) + } async fn on_state_member(&self, _: Arc>, _: &MemberEvent) { self.0.lock().await.push("state member".to_string()) diff --git a/src/models/room.rs b/src/models/room.rs index 89af2e19..efe8c6c8 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -165,6 +165,7 @@ impl RoomName { .take(3) .map(|mem| mem.user_id.localpart().to_string()) .collect::>(); + // stabilize ordering names.sort(); names.join(", ") } else if heroes < invited_joined && invited + joined > one { diff --git a/src/request_builder.rs b/src/request_builder.rs index 8d61d05d..f00ed1a3 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -290,7 +290,7 @@ impl Into for MessagesRequestBuilder { #[cfg(test)] mod test { - use std::collections::{BTreeMap, HashMap}; + use std::collections::BTreeMap; use super::*; use crate::events::room::power_levels::NotificationPowerLevels; @@ -371,9 +371,8 @@ mod test { .from("t47429-4392820_219380_26003_2265".to_string()) .to("t4357353_219380_26003_2265".to_string()) .direction(Direction::Backward) - .limit(UInt::new(10).unwrap()); - // TODO this makes ruma error `Err(IntoHttp(IntoHttpError(Query(Custom("unsupported value")))))`?? - // .filter(RoomEventFilter::default()); + .limit(UInt::new(10).unwrap()) + .filter(RoomEventFilter::default()); let cli = AsyncClient::new(homeserver, Some(session)).unwrap(); assert!(cli.room_messages(builder).await.is_ok()); From 8a8f590788271d6d721d7db169f2a28734715745 Mon Sep 17 00:00:00 2001 From: Devin R Date: Thu, 23 Apr 2020 09:55:59 -0400 Subject: [PATCH 4/4] room: tests for calculate_room --- src/models/room.rs | 87 +++++++++- src/test_builder.rs | 27 ++++ tests/data/events/aliases.json | 15 ++ tests/data/events/name.json | 2 +- tests/data/sync_with_summary.json | 256 ++++++++++++++++++++++++++++++ 5 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 tests/data/events/aliases.json create mode 100644 tests/data/sync_with_summary.json diff --git a/src/models/room.rs b/src/models/room.rs index efe8c6c8..d2e19ef6 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -163,7 +163,12 @@ impl RoomName { let mut names = members .values() .take(3) - .map(|mem| mem.user_id.localpart().to_string()) + .map(|mem| { + mem.display_name + .clone() + .unwrap_or(mem.user_id.localpart().to_string()) + .to_string() + }) .collect::>(); // stabilize ordering names.sort(); @@ -172,7 +177,11 @@ impl RoomName { let mut names = members .values() .take(3) - .map(|mem| mem.user_id.localpart().to_string()) + .map(|mem| { + mem.display_name + .clone() + .unwrap_or(mem.user_id.localpart().to_string()) + }) .collect::>(); names.sort(); // TODO what is the length the spec wants us to use here and in the `else` @@ -246,7 +255,7 @@ impl Room { true } - fn set_name_room(&mut self, name: &str) -> bool { + fn set_room_name(&mut self, name: &str) -> bool { self.room_name.set_name(name); true } @@ -338,7 +347,7 @@ impl Room { /// 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.set_name_room(name), + Some(name) => self.set_room_name(name), _ => false, } } @@ -527,4 +536,74 @@ mod test { .unwrap(); assert_eq!(admin.power_level.unwrap(), js_int::Int::new(100).unwrap()); } + + #[test] + fn calculate_aliases() { + let rid = RoomId::try_from("!roomid:room.com").unwrap(); + let uid = UserId::try_from("@example:localhost").unwrap(); + + let mut bld = EventBuilder::default() + .add_state_event_from_file("./tests/data/events/aliases.json", StateEvent::RoomAliases) + .build_room_runner(&rid, &uid); + + let room = bld.to_room(); + + assert_eq!("tutorial", room.calculate_name()); + } + + #[test] + fn calculate_alias() { + let rid = RoomId::try_from("!roomid:room.com").unwrap(); + let uid = UserId::try_from("@example:localhost").unwrap(); + + let mut bld = EventBuilder::default() + .add_state_event_from_file( + "./tests/data/events/alias.json", + StateEvent::RoomCanonicalAlias, + ) + .build_room_runner(&rid, &uid); + + let room = bld.to_room(); + + assert_eq!("tutorial", room.calculate_name()); + } + + #[test] + fn calculate_name() { + let rid = RoomId::try_from("!roomid:room.com").unwrap(); + let uid = UserId::try_from("@example:localhost").unwrap(); + + let mut bld = EventBuilder::default() + .add_state_event_from_file("./tests/data/events/name.json", StateEvent::RoomName) + .build_room_runner(&rid, &uid); + + let room = bld.to_room(); + + assert_eq!("room name", room.calculate_name()); + } + + #[tokio::test] + async fn calculate_room_names_from_summary() { + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let mut bld = EventBuilder::default().build_with_response( + // this sync has no room.name or room.alias events so only relies on summary + "tests/data/sync_with_summary.json", + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:localhost").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + let client = AsyncClient::new(homeserver, Some(session)).unwrap(); + let client = bld.set_client(client).to_client().await.unwrap(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync(sync_settings).await.unwrap(); + + assert_eq!(vec!["example, example2"], client.get_room_names().await); + } } diff --git a/src/test_builder.rs b/src/test_builder.rs index ff637b1c..b28b0139 100644 --- a/src/test_builder.rs +++ b/src/test_builder.rs @@ -165,6 +165,33 @@ impl EventBuilder { self } + /// Consumes `ResponseBuilder and returns a `TestRunner`. + /// + /// The `TestRunner` responds to requests made by the `AsyncClient`. + pub fn build_with_response(mut self, path: P, method: &str, matcher: M) -> MockTestRunner + where + M: Into, + P: AsRef, + { + let body = fs::read_to_string(path.as_ref()) + .expect(&format!("file not found {:?}", path.as_ref())); + let mock = Some( + mock(method, matcher) + .with_status(200) + .with_body(body) + .create(), + ); + MockTestRunner { + client: None, + ephemeral: Vec::new(), + account_data: Vec::new(), + room_events: Vec::new(), + presence_events: Vec::new(), + state_events: Vec::new(), + mock, + } + } + /// Consumes `ResponseBuilder and returns a `TestRunner`. /// /// The `TestRunner` streams the events to the client and holds methods to make assertions diff --git a/tests/data/events/aliases.json b/tests/data/events/aliases.json new file mode 100644 index 00000000..5b81aa2d --- /dev/null +++ b/tests/data/events/aliases.json @@ -0,0 +1,15 @@ +{ + "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 + } +} diff --git a/tests/data/events/name.json b/tests/data/events/name.json index 79946006..fa70a945 100644 --- a/tests/data/events/name.json +++ b/tests/data/events/name.json @@ -1,6 +1,6 @@ { "content": { - "name": "#tutorial:localhost" + "name": "room name" }, "event_id": "$15139375513VdeRF:localhost", "origin_server_ts": 1513937551461, diff --git a/tests/data/sync_with_summary.json b/tests/data/sync_with_summary.json new file mode 100644 index 00000000..f655b000 --- /dev/null +++ b/tests/data/sync_with_summary.json @@ -0,0 +1,256 @@ +{ + "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": { + "summary": { + "m.heroes": [ + "@alice:example.com", + "@bob:example.com" + ], + "m.joined_member_count": 2, + "m.invited_member_count": 0 + }, + "account_data": { + "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": [ + { + "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": { + "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": { + "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" + } + }, + { + "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": { + "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": [ + { + "content": { + "avatar_url": "mxc://localhost:wefuiwegh8742w", + "currently_active": false, + "last_active_ago": 1, + "presence": "online", + "status_msg": "Making cupcakes" + }, + "sender": "@example:localhost", + "type": "m.presence" + } + ] + } +}