From 11574d3d4d579f7109e67d8a76df63e5300d57b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Br=C3=B6nnimann?= Date: Sun, 24 Sep 2017 00:08:30 +0200 Subject: [PATCH] Store room state for each user individually on the mock matrix server To mimic the room state that is visible on a real homeserver --- assets/translations.yaml | 1 + src/matrix-rocketchat/api/matrix/mod.rs | 4 +- src/matrix-rocketchat/api/matrix/r0.rs | 24 +- src/matrix-rocketchat/db/room.rs | 43 +- src/matrix-rocketchat/errors.rs | 10 +- .../handlers/events/command_handler.rs | 1 - .../handlers/events/forwarder.rs | 1 - .../handlers/events/message_handler.rs | 4 +- .../handlers/events/room_handler.rs | 81 ++-- .../handlers/rocketchat/forwarder.rs | 27 +- src/matrix-rocketchat/log.rs | 2 +- tests/admin_commands_bridge.rs | 30 +- tests/admin_commands_connect.rs | 16 +- tests/admin_commands_help.rs | 4 +- tests/admin_commands_login.rs | 4 +- tests/admin_commands_unbridge.rs | 20 +- tests/admin_room.rs | 43 +- ...ard_matrix_direct_message_to_rocketchat.rs | 88 ++++ tests/forward_matrix_to_rocketchat.rs | 14 +- tests/forward_rocketchat_channel_to_matrix.rs | 29 +- ...ard_rocketchat_direct_message_to_matrix.rs | 29 +- tests/matrix-rocketchat-test/handlers.rs | 416 +++++++++++++----- tests/matrix-rocketchat-test/helpers.rs | 45 +- tests/matrix-rocketchat-test/lib.rs | 42 +- .../message_forwarder.rs | 16 +- 25 files changed, 678 insertions(+), 316 deletions(-) create mode 100644 tests/forward_matrix_direct_message_to_rocketchat.rs diff --git a/assets/translations.yaml b/assets/translations.yaml index b814c9d..6051618 100644 --- a/assets/translations.yaml +++ b/assets/translations.yaml @@ -77,6 +77,7 @@ en: connect_without_rocketchat_server_id: "You have to provide an id to connect to a Rocket.Chat server. It can contain any alphanumeric character and `_`. For example `connect https://rocketchat.example.com my_token rocketchat_example`" connect_with_invalid_rocketchat_server_id: "The provided Rocket.Chat server ID `${rocketchat_server_id}` is not valid, it can only contain lowercase alphanumeric characters and `_`. The maximum length is ${max_rocketchat_server_id_length} characters." internal: "An internal error occurred" + inviter_unknown: "The invite didn't contain a sender, the admin room could not be validated" no_rocketchat_server: "No Rocket.Chat server found when querying ${rocketchat_url} (version information is missing from the response)" other_user_joined: "Another user join the admin room, leaving, please create a new admin room." only_room_creator_can_invite_bot_user: "Only the room creator can invite the Rocket.Chat bot user, please create a new room and invite the Rocket.Chat user to create an admin room." diff --git a/src/matrix-rocketchat/api/matrix/mod.rs b/src/matrix-rocketchat/api/matrix/mod.rs index cedc9b9..11a4ce1 100644 --- a/src/matrix-rocketchat/api/matrix/mod.rs +++ b/src/matrix-rocketchat/api/matrix/mod.rs @@ -24,9 +24,9 @@ pub trait MatrixApi: Send + Sync + MatrixApiClone { /// Forget a room. fn forget_room(&self, matrix_room_id: RoomId) -> Result<()>; /// Get the room id based on the room alias. - fn get_room_alias(&self, matrix_room_alias_id: RoomAliasId, sender_id: Option) -> Result>; + fn get_room_alias(&self, matrix_room_alias_id: RoomAliasId) -> Result>; /// Get a rooms canonical alias. - fn get_room_canonical_alias(&self, matrix_room_id: RoomId, sender_id: Option) -> Result>; + fn get_room_canonical_alias(&self, matrix_room_id: RoomId) -> Result>; /// Get the `user_id` of the user that created the room. fn get_room_creator(&self, matrix_room_id: RoomId) -> Result; /// Get the list of members for this room. diff --git a/src/matrix-rocketchat/api/matrix/r0.rs b/src/matrix-rocketchat/api/matrix/r0.rs index 05f14d4..ff1d463 100644 --- a/src/matrix-rocketchat/api/matrix/r0.rs +++ b/src/matrix-rocketchat/api/matrix/r0.rs @@ -21,9 +21,8 @@ use ruma_events::EventType; use ruma_events::room::member::MemberEvent; use ruma_events::room::message::MessageType; use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId}; -use serde_json::Map; +use serde_json::{self, Map, Value}; use slog::Logger; -use serde_json::{self, Value}; use url; use api::RestApi; @@ -120,17 +119,12 @@ impl super::MatrixApi for MatrixApi { Ok(()) } - fn get_room_alias(&self, matrix_room_alias_id: RoomAliasId, sender_id: Option) -> Result> { + fn get_room_alias(&self, matrix_room_alias_id: RoomAliasId) -> Result> { // the ruma client api path params cannot be used here, because they are not url encoded let encoded_room_alias = url::form_urlencoded::byte_serialize(matrix_room_alias_id.to_string().as_bytes()) .collect::(); let endpoint = self.base_url.clone() + &format!("/_matrix/client/r0/directory/room/{}", &encoded_room_alias); - let user_id; - let mut params = self.params_hash(); - if let Some(matrix_user_id) = sender_id { - user_id = matrix_user_id.to_string(); - params.insert("user_id", &user_id); - } + let params = self.params_hash(); let (body, status_code) = RestApi::call_matrix(GetAliasEndpoint::method(), &endpoint, "{}", ¶ms)?; if status_code == StatusCode::NotFound { @@ -148,22 +142,16 @@ impl super::MatrixApi for MatrixApi { Ok(Some(get_alias_response.room_id.clone())) } - fn get_room_canonical_alias(&self, matrix_room_id: RoomId, sender_id: Option) -> Result> { + fn get_room_canonical_alias(&self, matrix_room_id: RoomId) -> Result> { let path_params = get_state_events_for_empty_key::PathParams { room_id: matrix_room_id, event_type: EventType::RoomCanonicalAlias.to_string(), }; let endpoint = self.base_url.clone() + &GetStateEventsForEmptyKeyEndpoint::request_path(path_params); - let user_id; - let mut params = self.params_hash(); - if let Some(matrix_user_id) = sender_id { - user_id = matrix_user_id.to_string(); - params.insert("user_id", &user_id); - } + let params = self.params_hash(); let (body, status_code) = RestApi::call_matrix(GetStateEventsForEmptyKeyEndpoint::method(), &endpoint, "{}", ¶ms)?; - //TODO: Fix this, do not return None, when the endpoint returns Forbidden - if status_code == StatusCode::NotFound || status_code == StatusCode::Forbidden { + if status_code == StatusCode::NotFound { return Ok(None); } diff --git a/src/matrix-rocketchat/db/room.rs b/src/matrix-rocketchat/db/room.rs index f96e217..cf8a563 100644 --- a/src/matrix-rocketchat/db/room.rs +++ b/src/matrix-rocketchat/db/room.rs @@ -6,7 +6,6 @@ use ruma_identifiers::{RoomAliasId, RoomId, UserId}; use api::{MatrixApi, RocketchatApi}; use config::Config; -use db::UserOnRocketchatServer; use errors::*; use super::RocketchatServer; @@ -25,7 +24,7 @@ impl Room { ) -> Result { let room_alias_id = Room::build_room_alias_id(config, rocketchat_server_id, rocketchat_channel_id)?; - match matrix_api.get_room_alias(room_alias_id, None)? { + match matrix_api.get_room_alias(room_alias_id)? { Some(matrix_room_id) => { let is_user_in_room = Room::user_ids(matrix_api, matrix_room_id, None)?.iter().any(|id| id == matrix_user_id); Ok(is_user_in_room) @@ -40,8 +39,7 @@ impl Room { matrix_api: &MatrixApi, matrix_room_id: RoomId, ) -> Result> { - let alias = - matrix_api.get_room_canonical_alias(matrix_room_id, None)?.map(|alias| alias.to_string()).unwrap_or_default(); + let alias = matrix_api.get_room_canonical_alias(matrix_room_id)?.map(|alias| alias.to_string()).unwrap_or_default(); let rocketchat_server_id = alias.split('#').nth(2).unwrap_or_default(); RocketchatServer::find_by_id(connection, rocketchat_server_id) } @@ -100,7 +98,7 @@ impl Room { match channel_id { Some(channel_id) => { - Room::matrix_id_from_rocketchat_channel_id(config, matrix_api, rocketchat_server_id, &channel_id, None) + Room::matrix_id_from_rocketchat_channel_id(config, matrix_api, rocketchat_server_id, &channel_id) } None => Ok(None), } @@ -112,30 +110,15 @@ impl Room { matrix_api: &MatrixApi, rocketchat_server_id: &str, rocketchat_channel_id: &str, - sender_id: Option, ) -> Result> { let room_alias_id = Room::build_room_alias_id(config, rocketchat_server_id, rocketchat_channel_id)?; - matrix_api.get_room_alias(room_alias_id, sender_id) + matrix_api.get_room_alias(room_alias_id) } /// Check if the room is a direct message room. - pub fn is_direct_message_room( - conn: &SqliteConnection, - matrix_api: &MatrixApi, - room_id: RoomId, - rocketchat_server_id: String, - sender_id: String, - ) -> Result { - match UserOnRocketchatServer::find_by_rocketchat_user_id(conn, rocketchat_server_id, sender_id.clone(), true)? { - Some(sender_matrix_user_id) => { - let alias = matrix_api - .get_room_canonical_alias(room_id, Some(sender_matrix_user_id.matrix_user_id.clone()))? - .map(|alias| alias.to_string()) - .unwrap_or_default(); - Ok(alias.contains(&sender_id)) - } - None => Ok(false), - } + pub fn is_direct_message_room(matrix_api: &MatrixApi, room_id: RoomId) -> Result { + let alias = matrix_api.get_room_canonical_alias(room_id)?.map(|alias| alias.alias().to_string()).unwrap_or_default(); + Ok(alias.ends_with("#dm")) } /// Checks if a room is an admin room. @@ -145,21 +128,17 @@ impl Room { return Ok(false); } + let virtual_user_prefix = format!("@{}", config.sender_localpart); let matrix_bot_user_id = config.matrix_bot_user_id()?; let matrix_user_ids = Room::user_ids(matrix_api, matrix_room_id.clone(), None)?; let bot_user_in_room = matrix_user_ids.iter().any(|id| id == &matrix_bot_user_id); - let room_creator = matrix_api.get_room_creator(matrix_room_id)?; - Ok(room_creator != matrix_bot_user_id && bot_user_in_room) - } - - /// Check if a room is bridged to Rocket.Chat - pub fn is_bridged(connection: &SqliteConnection, matrix_api: &MatrixApi, matrix_room_id: RoomId) -> Result { - Ok(Room::rocketchat_server(connection, matrix_api, matrix_room_id)?.is_some()) + let room_creator = matrix_api.get_room_creator(matrix_room_id)?.to_string(); + Ok(!room_creator.starts_with(&virtual_user_prefix) && bot_user_in_room) } /// Gets the Rocket.Chat channel id for a room that is bridged to Matrix. pub fn rocketchat_channel_id(matrix_api: &MatrixApi, matrix_room_id: RoomId) -> Result> { - let room_canonical_alias = match matrix_api.get_room_canonical_alias(matrix_room_id, None)? { + let room_canonical_alias = match matrix_api.get_room_canonical_alias(matrix_room_id)? { Some(room_canonical_alias) => room_canonical_alias.alias().to_string(), None => return Ok(None), }; diff --git a/src/matrix-rocketchat/errors.rs b/src/matrix-rocketchat/errors.rs index 361e6ac..b5b1407 100644 --- a/src/matrix-rocketchat/errors.rs +++ b/src/matrix-rocketchat/errors.rs @@ -305,9 +305,15 @@ error_chain!{ display("Room {} has more then two members and cannot be used as admin room", room_id) } + InviterUnknown(room_id: RoomId) { + description("Inviter for join event was not found") + display("Could not determine if the admin room {} is valid, because the inviter is unknown", room_id) + } + OnlyRoomCreatorCanInviteBotUser(inviter_id: UserId, room_id: RoomId, creator_id: UserId) { - description("Error when getting the connection pool from the request") - display("The bot user was invited by the user {} but the room {} was created by {}, bot user is leaving", inviter_id, room_id, creator_id) + description("Only the room creator can invite the bot user") + display("The bot user was invited by the user {} but the room {} was created by {}, \ + bot user is leaving", inviter_id, room_id, creator_id) } ConnectionPoolExtractionError { diff --git a/src/matrix-rocketchat/handlers/events/command_handler.rs b/src/matrix-rocketchat/handlers/events/command_handler.rs index 4f72c97..a5c0daa 100644 --- a/src/matrix-rocketchat/handlers/events/command_handler.rs +++ b/src/matrix-rocketchat/handlers/events/command_handler.rs @@ -302,7 +302,6 @@ impl<'a> CommandHandler<'a> { self.matrix_api, &rocketchat_server.id, &channel.id, - Some(user_on_rocketchat_server.matrix_user_id.clone()), )? { Some(matrix_room_id) => { room_handler.bridge_existing_room(matrix_room_id.clone(), event.user_id.clone(), channel_name.to_string())?; diff --git a/src/matrix-rocketchat/handlers/events/forwarder.rs b/src/matrix-rocketchat/handlers/events/forwarder.rs index 8c4d7c0..c97aa73 100644 --- a/src/matrix-rocketchat/handlers/events/forwarder.rs +++ b/src/matrix-rocketchat/handlers/events/forwarder.rs @@ -38,7 +38,6 @@ impl<'a> Forwarder<'a> { match event.content { MessageEventContent::Text(ref text_content) => { - let rocketchat_api = RocketchatApi::new(rocketchat_server.rocketchat_url, self.logger.clone())? .with_credentials( user_on_rocketchat_server.rocketchat_user_id.clone().unwrap_or_default(), diff --git a/src/matrix-rocketchat/handlers/events/message_handler.rs b/src/matrix-rocketchat/handlers/events/message_handler.rs index 45929e0..d3f1b67 100644 --- a/src/matrix-rocketchat/handlers/events/message_handler.rs +++ b/src/matrix-rocketchat/handlers/events/message_handler.rs @@ -43,8 +43,8 @@ impl<'a> MessageHandler<'a> { let matrix_api = self.matrix_api.as_ref(); if Room::is_admin_room(self.matrix_api.as_ref(), self.config, matrix_room_id.clone())? { CommandHandler::new(self.config, self.connection, self.logger, matrix_api).process(event, matrix_room_id)?; - } else if let Some(rocketchat_channel_id) = Room::rocketchat_channel_id(matrix_api, matrix_room_id.clone())? { - Forwarder::new(self.connection, self.logger, matrix_api).process(event, matrix_room_id, rocketchat_channel_id)?; + } else if let Some(channel_id) = Room::rocketchat_channel_id(matrix_api, matrix_room_id.clone())? { + Forwarder::new(self.connection, self.logger, matrix_api).process(event, matrix_room_id, channel_id)?; } else { debug!(self.logger, "Skipping event, because the room {} is not bridged", matrix_room_id); } diff --git a/src/matrix-rocketchat/handlers/events/room_handler.rs b/src/matrix-rocketchat/handlers/events/room_handler.rs index cbebdb7..5fe0f0a 100644 --- a/src/matrix-rocketchat/handlers/events/room_handler.rs +++ b/src/matrix-rocketchat/handlers/events/room_handler.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::convert::TryFrom; use diesel::sqlite::SqliteConnection; @@ -16,6 +17,7 @@ use handlers::ErrorNotifier; use handlers::rocketchat::VirtualUserHandler; use i18n::*; use log; +use serde_json::{self, Value}; use super::CommandHandler; /// Handles room events @@ -60,7 +62,18 @@ impl<'a> RoomHandler<'a> { let msg = format!("Received join event for bot user {} and room {}", matrix_bot_user_id, event.room_id); debug!(self.logger, msg); - self.handle_bot_join(event.room_id.clone(), matrix_bot_user_id)?; + let unsigned: HashMap = serde_json::from_value(event.unsigned.clone().unwrap_or_default()) + .unwrap_or_default(); + let inviter_id = match unsigned.get("prev_sender") { + Some(prev_sender) => { + let raw_id: String = serde_json::from_value(prev_sender.clone()).unwrap_or_else(|_| "".to_string()); + let inviter_id = UserId::try_from(&raw_id).chain_err(|| ErrorKind::InvalidUserId(raw_id))?; + Some(inviter_id) + } + None => None, + }; + + self.handle_bot_join(event.room_id.clone(), matrix_bot_user_id, inviter_id)?; } MembershipState::Join => { let msg = format!("Received join event for user {} and room {}", &state_key, &event.room_id); @@ -141,10 +154,12 @@ impl<'a> RoomHandler<'a> { return Ok(()); } - self.matrix_api.join(matrix_room_id, invited_user_id) + self.matrix_api.join(matrix_room_id, invited_user_id)?; + + Ok(()) } - fn handle_bot_join(&self, matrix_room_id: RoomId, matrix_bot_user_id: UserId) -> Result<()> { + fn handle_bot_join(&self, matrix_room_id: RoomId, matrix_bot_user_id: UserId, inviter_id: Option) -> Result<()> { let is_admin_room = match Room::is_admin_room(self.matrix_api, self.config, matrix_room_id.clone()) { Ok(is_admin_room) => is_admin_room, Err(err) => { @@ -152,43 +167,36 @@ impl<'a> RoomHandler<'a> { self.logger, "Could not determine if the room that the bot user was invited to is an admin room or not, bot is leaving" ); - self.leave_and_forget_room(matrix_room_id, matrix_bot_user_id)?; + self.handle_admin_room_setup_error(&err, matrix_room_id, matrix_bot_user_id); return Err(err); } }; if is_admin_room { - let user_ids: Vec = Room::user_ids(self.matrix_api, matrix_room_id.clone(), None)? - .into_iter() - .filter(|id| id != &matrix_bot_user_id) - .collect(); - let inviter_id = user_ids.first().expect("The user that sent the invitation has to be in the room"); - self.setup_admin_room(matrix_room_id.clone(), matrix_bot_user_id.clone(), inviter_id)?; } + // leave direct message room, the bot only joined it to be able to read the room members + if Room::is_direct_message_room(self.matrix_api, matrix_room_id.clone())? { + self.matrix_api.leave_room(matrix_room_id.clone(), matrix_bot_user_id)?; + } + Ok(()) } - fn setup_admin_room(&self, matrix_room_id: RoomId, matrix_bot_user_id: UserId, inviter_id: &UserId) -> Result<()> { + fn setup_admin_room(&self, matrix_room_id: RoomId, matrix_bot_user_id: UserId, inviter_id: Option) -> Result<()> { debug!(self.logger, "Setting up a new admin room with id {}", matrix_room_id); - if let Err(err) = self.is_admin_room_valid(matrix_room_id.clone(), inviter_id) { - info!(self.logger, "Admin room {} is not valid, bot will leave and forget the room", matrix_room_id); - let error_notifier = ErrorNotifier { - config: self.config, - connection: self.connection, - logger: self.logger, - matrix_api: self.matrix_api, - }; - if let Err(err) = error_notifier.send_message_to_user(&err, matrix_room_id.clone(), inviter_id) { - log::log_error(self.logger, &err); - } - - if let Err(err) = self.leave_and_forget_room(matrix_room_id.clone(), matrix_bot_user_id.clone()) { - log::log_error(self.logger, &err); + let inviter_id = match inviter_id { + Some(inviter_id) => inviter_id, + None => { + bail_error!(ErrorKind::InviterUnknown(matrix_room_id.clone()), t!(["errors", "inviter_unknown"])); } + }; + if let Err(err) = self.is_admin_room_valid(matrix_room_id.clone(), &inviter_id) { + info!(self.logger, "Admin room {} is not valid, bot will leave and forget the room", matrix_room_id); + self.handle_admin_room_setup_error(&err, matrix_room_id, matrix_bot_user_id); return Ok(()); } @@ -247,11 +255,8 @@ impl<'a> RoomHandler<'a> { } fn admin_room_language(&self, matrix_room_id: RoomId) -> Result { - let matrix_bot_user_id = self.config.matrix_bot_user_id()?; - let user_ids: Vec = - Room::user_ids(self.matrix_api, matrix_room_id, None)?.into_iter().filter(|id| id != &matrix_bot_user_id).collect(); - let user_id = user_ids.first().expect("An admin room always contains another user"); - let user = User::find(self.connection, user_id)?; + let user_id = self.matrix_api.get_room_creator(matrix_room_id.clone())?; + let user = User::find(self.connection, &user_id)?; Ok(user.language.clone()) } @@ -336,4 +341,20 @@ impl<'a> RoomHandler<'a> { Ok(()) } + + fn handle_admin_room_setup_error(&self, err: &Error, matrix_room_id: RoomId, matrix_bot_user_id: UserId) { + let error_notifier = ErrorNotifier { + config: self.config, + connection: self.connection, + logger: self.logger, + matrix_api: self.matrix_api, + }; + if let Err(err) = error_notifier.send_message_to_user(err, matrix_room_id.clone(), &matrix_bot_user_id) { + log::log_error(self.logger, &err); + } + + if let Err(err) = self.leave_and_forget_room(matrix_room_id.clone(), matrix_bot_user_id.clone()) { + log::log_error(self.logger, &err); + } + } } diff --git a/src/matrix-rocketchat/handlers/rocketchat/forwarder.rs b/src/matrix-rocketchat/handlers/rocketchat/forwarder.rs index 833ec83..898e2cd 100644 --- a/src/matrix-rocketchat/handlers/rocketchat/forwarder.rs +++ b/src/matrix-rocketchat/handlers/rocketchat/forwarder.rs @@ -54,12 +54,18 @@ impl<'a> Forwarder<'a> { return Ok(()); } + let is_direct_message_room = message.channel_id.contains(&message.user_id); + let channel_id = if is_direct_message_room { + format!("{}#dm", message.channel_id) + } else { + message.channel_id.clone() + }; + let matrix_room_id = match Room::matrix_id_from_rocketchat_channel_id( self.config, self.matrix_api, &rocketchat_server.id, - &message.channel_id, - Some(user_on_rocketchat_server.matrix_user_id.clone()), + &channel_id, )? { Some(matrix_room_id) => matrix_room_id, None => { @@ -77,14 +83,7 @@ impl<'a> Forwarder<'a> { } }; - if Room::is_direct_message_room( - self.connection, - self.matrix_api, - matrix_room_id.clone(), - rocketchat_server.id.clone(), - message.user_id.clone(), - )? - { + if is_direct_message_room { if Room::direct_message_room_matrix_user( self.config, self.matrix_api, @@ -182,13 +181,19 @@ impl<'a> Forwarder<'a> { let room_display_name_suffix = t!(["defaults", "direct_message_room_display_name_suffix"]).l(&direct_message_receiver.language); let room_display_name = format!("{} {}", message.user_name, room_display_name_suffix); + let dm_channel_id = format!("{}#dm", direct_message_channel.id.clone()); let matrix_room_id = room_handler.create_room( - direct_message_channel.id.clone(), + dm_channel_id, rocketchat_server.id.clone(), direct_message_sender.matrix_user_id.clone(), user_on_rocketchat_server.matrix_user_id.clone(), Some(room_display_name), )?; + + // invite the bot user into the direct message room to be able to read the room members + // the bot will leave as soon as the AS gets the join event + let invitee_id = self.config.matrix_bot_user_id()?; + self.matrix_api.invite(matrix_room_id.clone(), invitee_id.clone(), direct_message_sender.matrix_user_id.clone())?; debug!(self.logger, "Direct message room {} successfully created", &matrix_room_id); Ok(Some(matrix_room_id)) diff --git a/src/matrix-rocketchat/log.rs b/src/matrix-rocketchat/log.rs index ce65cdf..735c512 100644 --- a/src/matrix-rocketchat/log.rs +++ b/src/matrix-rocketchat/log.rs @@ -52,4 +52,4 @@ fn build_message(err: &Error) -> String { } msg -} \ No newline at end of file +} diff --git a/tests/admin_commands_bridge.rs b/tests/admin_commands_bridge.rs index 6f8fe5b..d3fd97d 100644 --- a/tests/admin_commands_bridge.rs +++ b/tests/admin_commands_bridge.rs @@ -19,7 +19,7 @@ use matrix_rocketchat_test::{DEFAULT_LOGGER, MessageForwarder, Test, default_tim use router::Router; use ruma_client_api::Endpoint; use ruma_client_api::r0::alias::get_alias::Endpoint as GetAliasEndpoint; -use ruma_client_api::r0::membership::invite_user::Endpoint as InviteEndpoint; +use ruma_client_api::r0::membership::invite_user::{self, Endpoint as InviteEndpoint}; use ruma_client_api::r0::room::create_room::Endpoint as CreateRoomEndpoint; use ruma_client_api::r0::send::send_message_event::Endpoint as SendMessageEventEndpoint; use ruma_client_api::r0::send::send_state_event_for_empty_key::{self, Endpoint as SendStateEventForEmptyKeyEndpoint}; @@ -30,7 +30,7 @@ use ruma_identifiers::{RoomId, UserId}; fn successfully_bridge_a_rocketchat_room() { let test = Test::new(); let (message_forwarder, receiver) = MessageForwarder::new(); - let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = handlers::MatrixInviteUser::with_forwarder(test.config.as_url.clone()); let (state_forwarder, state_receiver) = handlers::SendRoomState::with_forwarder(); let (create_room_forwarder, create_room_receiver) = handlers::MatrixCreateRoom::with_forwarder(test.config.as_url.clone()); let mut matrix_router = test.default_matrix_routes(); @@ -70,6 +70,9 @@ fn successfully_bridge_a_rocketchat_room() { assert!(create_room_message.contains("\"name\":\"joined_channel\"")); assert!(create_room_message.contains("\"room_alias_name\":\"rocketchat#rc_id#joined_channel_id\"")); + // discard rocketchat user invite into admin room + invite_receiver.recv_timeout(default_timeout()).unwrap(); + let invite_spec_user = invite_receiver.recv_timeout(default_timeout()).unwrap(); assert!(invite_spec_user.contains("@spec_user:localhost")); let invite_virtual_spec_user = invite_receiver.recv_timeout(default_timeout()).unwrap(); @@ -119,7 +122,7 @@ fn successfully_bridge_a_rocketchat_room() { fn successfully_bridge_a_rocketchat_room_that_an_other_user_already_bridged() { let test = Test::new(); let (message_forwarder, receiver) = MessageForwarder::new(); - let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = handlers::MatrixInviteUser::with_forwarder(test.config.as_url.clone()); let spec_user_id = UserId::try_from("@spec_user:localhost").unwrap(); let other_user_id = UserId::try_from("@other_user:localhost").unwrap(); @@ -165,10 +168,10 @@ fn successfully_bridge_a_rocketchat_room_that_an_other_user_already_bridged() { let matrix_api = MatrixApi::new(&test.config, DEFAULT_LOGGER.clone()).unwrap(); matrix_api.create_room(Some("other_admin_room".to_string()), None, &other_user_id).unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_admin_room_id:localhost").unwrap(), - other_user_id.clone(), bot_user_id.clone(), + other_user_id.clone(), ); // connect other admin room @@ -227,6 +230,15 @@ fn successfully_bridge_a_rocketchat_room_that_an_other_user_already_bridged() { helpers::join(&test.config, RoomId::try_from("!joined_channel_id:localhost").unwrap(), other_user_id.clone()); + // bot user invite for admin room + let rocketchat_user_invite_received_by_matrix = invite_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(rocketchat_user_invite_received_by_matrix.contains("@rocketchat:localhost")); + + // bot user invite for other admin room + let rocketchat_user_invite_received_by_matrix = invite_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(rocketchat_user_invite_received_by_matrix.contains("@rocketchat:localhost")); + + // joined channel invites let spec_user_invite_received_by_matrix = invite_receiver.recv_timeout(default_timeout()).unwrap(); assert!(spec_user_invite_received_by_matrix.contains("@spec_user:localhost")); @@ -247,7 +259,7 @@ fn successfully_bridge_a_rocketchat_room_that_an_other_user_already_bridged() { fn susccessfully_bridge_a_rocketchat_room_that_was_unbridged_before() { let test = Test::new(); let (message_forwarder, receiver) = MessageForwarder::new(); - let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = handlers::MatrixInviteUser::with_forwarder(test.config.as_url.clone()); let mut matrix_router = test.default_matrix_routes(); matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); matrix_router.post(InviteEndpoint::router_path(), invite_forwarder, "invite_user"); @@ -299,6 +311,9 @@ fn susccessfully_bridge_a_rocketchat_room_that_was_unbridged_before() { let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); assert!(message_received_by_matrix.contains("joined_channel is now bridged.")); + // discard rocketchat user invite into admin room + invite_receiver.recv_timeout(default_timeout()).unwrap(); + let invite_received_by_matrix = invite_receiver.recv_timeout(default_timeout()).unwrap(); assert!(invite_received_by_matrix.contains("@spec_user:localhost")); @@ -698,8 +713,9 @@ fn the_user_gets_a_message_when_inviting_the_user_failes() { let (message_forwarder, receiver) = MessageForwarder::new(); let mut matrix_router = test.default_matrix_routes(); matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); + let invite_path = invite_user::PathParams { room_id: RoomId::try_from("!joined_channel_id:localhost").unwrap() }; matrix_router.post( - InviteEndpoint::router_path(), + InviteEndpoint::request_path(invite_path), handlers::MatrixErrorResponder { status: status::InternalServerError, message: "Could not invite user".to_string(), diff --git a/tests/admin_commands_connect.rs b/tests/admin_commands_connect.rs index 8f9b960..6ce1aac 100644 --- a/tests/admin_commands_connect.rs +++ b/tests/admin_commands_connect.rs @@ -307,10 +307,10 @@ fn attempt_to_connect_with_a_rocketchat_server_id_that_is_already_in_use() { .unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_admin_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); helpers::send_room_message_from_matrix( @@ -370,10 +370,10 @@ fn connect_an_existing_server() { matrix_api.create_room(Some("other_admin_room".to_string()), None, &other_user_id).unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_admin_room_id:localhost").unwrap(), - UserId::try_from("@other_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@other_user:localhost").unwrap(), ); helpers::send_room_message_from_matrix( @@ -420,10 +420,10 @@ fn attempt_to_connect_to_an_existing_server_with_a_token() { matrix_api.create_room(Some("other_admin_room".to_string()), None, &other_user_id).unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_admin_room_id:localhost").unwrap(), - UserId::try_from("@other_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@other_user:localhost").unwrap(), ); @@ -520,10 +520,10 @@ fn attempt_to_connect_a_server_with_a_token_that_is_already_in_use() { matrix_api.create_room(Some("other_admin_room".to_string()), None, &other_user_id).unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_admin_room_id:localhost").unwrap(), - UserId::try_from("@other_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@other_user:localhost").unwrap(), ); diff --git a/tests/admin_commands_help.rs b/tests/admin_commands_help.rs index 7c45720..f2791e5 100644 --- a/tests/admin_commands_help.rs +++ b/tests/admin_commands_help.rs @@ -71,10 +71,10 @@ fn help_command_when_not_connected_and_someone_else_has_connected_a_server_alrea matrix_api.create_room(Some("other_admin_room".to_string()), None, &other_user_id).unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_admin_room_id:localhost").unwrap(), - UserId::try_from("@other_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@other_user:localhost").unwrap(), ); // other user connects the Rocket.Chat server diff --git a/tests/admin_commands_login.rs b/tests/admin_commands_login.rs index d46d3f9..19cd9f0 100644 --- a/tests/admin_commands_login.rs +++ b/tests/admin_commands_login.rs @@ -358,10 +358,10 @@ fn the_user_can_login_again_on_the_same_server_with_a_new_admin_room() { .unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_admin_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); helpers::send_room_message_from_matrix( diff --git a/tests/admin_commands_unbridge.rs b/tests/admin_commands_unbridge.rs index 623c13f..f32bd7a 100644 --- a/tests/admin_commands_unbridge.rs +++ b/tests/admin_commands_unbridge.rs @@ -69,20 +69,15 @@ fn successfully_unbridge_a_rocketchat_room() { ); // discard welcome message for spec user - let a = receiver.recv_timeout(default_timeout()).unwrap(); - println!("A: {}", a); + receiver.recv_timeout(default_timeout()).unwrap(); // discard connect message for spec user - let b = receiver.recv_timeout(default_timeout()).unwrap(); - println!("B: {}", b); + receiver.recv_timeout(default_timeout()).unwrap(); // discard login message for spec user - let c = receiver.recv_timeout(default_timeout()).unwrap(); - println!("C: {}", c); + receiver.recv_timeout(default_timeout()).unwrap(); // discard bridged message - let d = receiver.recv_timeout(default_timeout()).unwrap(); - println!("D: {}", d); + receiver.recv_timeout(default_timeout()).unwrap(); // discard message from virtual user - let foo = receiver.recv_timeout(default_timeout()).unwrap(); - println!("FOO: {}", foo); + receiver.recv_timeout(default_timeout()).unwrap(); let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); assert!(message_received_by_matrix.contains("bridged_channel is now unbridged.")); @@ -141,10 +136,10 @@ fn do_not_allow_to_unbridge_a_channel_with_other_matrix_users() { .create_room(Some("other_admin_room".to_string()), None, &UserId::try_from("@other_user:localhost").unwrap()) .unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_admin_room_id:localhost").unwrap(), - UserId::try_from("@other_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@other_user:localhost").unwrap(), ); // connect other admin room @@ -204,7 +199,6 @@ fn do_not_allow_to_unbridge_a_channel_with_other_matrix_users() { ); let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); - println!("MSG: {}", message_received_by_matrix); assert!(message_received_by_matrix.contains("Cannot unbdrige room bridged_channel, because Matrix users")); assert!(message_received_by_matrix.contains("@spec_user:localhost")); assert!(message_received_by_matrix.contains("@other_user:localhost")); diff --git a/tests/admin_room.rs b/tests/admin_room.rs index 53f8c96..0ae6c3c 100644 --- a/tests/admin_room.rs +++ b/tests/admin_room.rs @@ -81,10 +81,10 @@ fn attempt_to_create_an_admin_room_with_other_users_in_it() { ); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!admin_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); @@ -187,10 +187,10 @@ fn the_bot_user_leaves_the_admin_room_when_getting_the_room_members_failes() { ); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!admin_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); let leave_room_message = leave_room_receiver.recv_timeout(default_timeout()).unwrap(); @@ -224,10 +224,10 @@ fn the_bot_user_leaves_the_admin_room_when_the_room_members_cannot_be_deserializ ); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!admin_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); let leave_room_message = leave_room_receiver.recv_timeout(default_timeout()).unwrap(); @@ -356,10 +356,10 @@ fn the_user_does_not_get_a_message_when_an_leaving_the_room_failes_for_the_bot_u ); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!admin_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); @@ -398,10 +398,10 @@ fn the_user_does_not_get_a_message_when_forgetting_the_room_failes_for_the_bot_u ); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!admin_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); // discard user readable error message that triggers the bot leave @@ -428,6 +428,13 @@ fn bot_leaves_when_a_third_user_joins_the_admin_room() { let user_ids = Room::user_ids(&(*matrix_api), RoomId::try_from("!admin_room_id:localhost").unwrap(), None).unwrap(); assert_eq!(user_ids.len(), 2); + helpers::invite( + &test.config, + RoomId::try_from("!admin_room_id:localhost").unwrap(), + UserId::try_from("@other_user:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), + ); + helpers::join( &test.config, RoomId::try_from("!admin_room_id:localhost").unwrap(), @@ -551,10 +558,10 @@ fn ignore_invites_from_rooms_on_other_homeservers_if_accept_remote_invites_is_se let test = test.with_matrix_routes(matrix_router).run(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_server_room_id:other-homeserver.com").unwrap(), - UserId::try_from("@spec_user:other-homeserver.com").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:other-homeserver.com").unwrap(), ); // the room doesn't get a message, because the bot user ignores the invite @@ -608,10 +615,10 @@ fn accept_invites_from_rooms_on_other_homeservers_if_accept_remote_invites_is_se let test = test.with_matrix_routes(matrix_router).run(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!other_server_room_id:other-homeserver.com").unwrap(), - UserId::try_from("@spec_user:other-homeserver.com").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:other-homeserver.com").unwrap(), ); let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); @@ -708,10 +715,10 @@ fn the_user_does_get_a_message_when_getting_the_room_creator_cannot_be_deseriali ); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!admin_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@rocketchat:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); @@ -748,10 +755,10 @@ fn join_events_for_rooms_that_are_not_accessible_by_the_bot_user_are_ignored() { ); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!some_room_id:localhost").unwrap(), - UserId::try_from("@spec_user:localhost").unwrap(), UserId::try_from("@third_user:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), ); helpers::join( diff --git a/tests/forward_matrix_direct_message_to_rocketchat.rs b/tests/forward_matrix_direct_message_to_rocketchat.rs new file mode 100644 index 0000000..82b9aa9 --- /dev/null +++ b/tests/forward_matrix_direct_message_to_rocketchat.rs @@ -0,0 +1,88 @@ +#![feature(try_from)] + +extern crate iron; +extern crate matrix_rocketchat; +extern crate matrix_rocketchat_test; +extern crate reqwest; +extern crate router; +extern crate ruma_client_api; +extern crate ruma_identifiers; +extern crate serde_json; + +use std::collections::HashMap; +use std::convert::TryFrom; + +use iron::status; +use matrix_rocketchat::api::rocketchat::Message; +use matrix_rocketchat::api::rocketchat::v1::{DIRECT_MESSAGES_LIST_PATH, POST_CHAT_MESSAGE_PATH}; +use matrix_rocketchat_test::{MessageForwarder, RS_TOKEN, Test, default_timeout, handlers, helpers}; +use router::Router; +use ruma_client_api::Endpoint; +use ruma_client_api::r0::send::send_message_event::Endpoint as SendMessageEventEndpoint; +use ruma_identifiers::{RoomId, UserId}; +use serde_json::to_string; + +#[test] +fn successfully_forwards_a_direct_message_to_rocketchat() { + let test = Test::new(); + let (matrix_message_forwarder, matrix_receiver) = MessageForwarder::new(); + let mut matrix_router = test.default_matrix_routes(); + matrix_router.put(SendMessageEventEndpoint::router_path(), matrix_message_forwarder, "send_message_event"); + let mut rocketchat_router = Router::new(); + let mut direct_messages = HashMap::new(); + direct_messages.insert("spec_user_id_other_user_id", vec!["spec_user", "other_user"]); + let direct_messages_list_handler = handlers::RocketchatDirectMessagesList { + direct_messages: direct_messages, + status: status::Ok, + }; + rocketchat_router.get(DIRECT_MESSAGES_LIST_PATH, direct_messages_list_handler, "direct_messages_list"); + let (rocketchat_message_forwarder, rocketchat_receiver) = MessageForwarder::new(); + rocketchat_router.post(POST_CHAT_MESSAGE_PATH, rocketchat_message_forwarder, "post_chat_message"); + + let test = test.with_matrix_routes(matrix_router) + .with_rocketchat_mock() + .with_custom_rocketchat_routes(rocketchat_router) + .with_connected_admin_room() + .with_logged_in_user() + .run(); + + // direct message from Rocket.Chat to trigger the room creation + let direct_message_from_rocketchat = Message { + message_id: "spec_id_1".to_string(), + token: Some(RS_TOKEN.to_string()), + channel_id: "spec_user_id_other_user_id".to_string(), + channel_name: None, + user_id: "other_user_id".to_string(), + user_name: "other_user".to_string(), + text: "Hey there".to_string(), + }; + let direct_message_from_rocketchat_payload = to_string(&direct_message_from_rocketchat).unwrap(); + + helpers::simulate_message_from_rocketchat(&test.config.as_url, &direct_message_from_rocketchat_payload); + + helpers::join( + &test.config, + RoomId::try_from("!other_userDMRocketChat_id:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), + ); + + // discard welcome message + matrix_receiver.recv_timeout(default_timeout()).unwrap(); + // discard connect message + matrix_receiver.recv_timeout(default_timeout()).unwrap(); + // discard login message + matrix_receiver.recv_timeout(default_timeout()).unwrap(); + + let message_received_by_matrix = matrix_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(message_received_by_matrix.contains("Hey there")); + + helpers::send_room_message_from_matrix( + &test.config.as_url, + RoomId::try_from("!other_userDMRocketChat_id:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), + "It's so nice to hear from you after such a long time".to_string(), + ); + + let message_received_by_rocketchat = rocketchat_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(message_received_by_rocketchat.contains("It's so nice to hear from you after such a long time")); +} diff --git a/tests/forward_matrix_to_rocketchat.rs b/tests/forward_matrix_to_rocketchat.rs index 188e74f..0780c98 100644 --- a/tests/forward_matrix_to_rocketchat.rs +++ b/tests/forward_matrix_to_rocketchat.rs @@ -150,10 +150,10 @@ fn ignore_messages_from_rooms_with_empty_room_canonical_alias() { matrix_api.put_canonical_room_alias(RoomId::try_from("!room_id:localhost").unwrap(), None).unwrap(); helpers::invite( - &test.config.as_url, + &test.config, RoomId::try_from("!room_id:localhost").unwrap(), - UserId::try_from("@rocketchat:localhost").unwrap(), UserId::try_from("@spec_user:localhost").unwrap(), + UserId::try_from("@rocketchat:localhost").unwrap(), ); helpers::join( @@ -291,6 +291,10 @@ fn the_user_gets_a_message_when_when_getting_the_canonical_room_alias_failes() { receiver.recv_timeout(default_timeout()).unwrap(); // discard login message receiver.recv_timeout(default_timeout()).unwrap(); + // discard first error message, because the bot doesn't + // know if it's a direct message room since getting the + // canonical room name fails + receiver.recv_timeout(default_timeout()).unwrap(); // discard room bridged message receiver.recv_timeout(default_timeout()).unwrap(); @@ -326,9 +330,13 @@ fn the_user_gets_a_message_when_when_getting_the_canonical_room_alias_response_c // discard welcome message receiver.recv_timeout(default_timeout()).unwrap(); + // discard login message + receiver.recv_timeout(default_timeout()).unwrap(); // discard connect message receiver.recv_timeout(default_timeout()).unwrap(); - // discard login message + // discard first error message, because the bot doesn't + // know if it's a direct message room since getting the + // canonical room name fails receiver.recv_timeout(default_timeout()).unwrap(); // discard room bridged message receiver.recv_timeout(default_timeout()).unwrap(); diff --git a/tests/forward_rocketchat_channel_to_matrix.rs b/tests/forward_rocketchat_channel_to_matrix.rs index f8151fb..3933f20 100644 --- a/tests/forward_rocketchat_channel_to_matrix.rs +++ b/tests/forward_rocketchat_channel_to_matrix.rs @@ -12,7 +12,7 @@ extern crate serde_json; use std::collections::HashMap; use std::convert::TryFrom; -use iron::status; +use iron::{Chain, status}; use matrix_rocketchat::api::{MatrixApi, RestApi}; use matrix_rocketchat::api::rocketchat::Message; use matrix_rocketchat::db::{Room, User, UserOnRocketchatServer}; @@ -30,7 +30,7 @@ use serde_json::to_string; fn successfully_forwards_a_text_message_from_rocketchat_to_matrix_when_the_user_is_not_registered_on_matrix() { let test = Test::new(); let (message_forwarder, receiver) = MessageForwarder::new(); - let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = handlers::MatrixInviteUser::with_forwarder(test.config.as_url.clone()); let (join_forwarder, join_receiver) = handlers::MatrixJoinRoom::with_forwarder(test.config.as_url.clone()); let (set_display_name_forwarder, set_display_name_receiver) = MessageForwarder::new(); let mut matrix_router = test.default_matrix_routes(); @@ -59,6 +59,8 @@ fn successfully_forwards_a_text_message_from_rocketchat_to_matrix_when_the_user_ helpers::simulate_message_from_rocketchat(&test.config.as_url, &payload); + // discard admin room invite + invite_receiver.recv_timeout(default_timeout()).unwrap(); // receive the invite messages let spec_user_invite_message = invite_receiver.recv_timeout(default_timeout()).unwrap(); assert!(spec_user_invite_message.contains("@spec_user:localhost")); @@ -136,7 +138,7 @@ fn successfully_forwards_a_text_message_from_rocketchat_to_matrix_when_the_user_ fn successfully_forwards_a_text_message_from_rocketchat_to_matrix_when_the_user_is_registered_on_matrix() { let test = Test::new(); let (message_forwarder, receiver) = MessageForwarder::new(); - let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = handlers::MatrixInviteUser::with_forwarder(test.config.as_url.clone()); let (join_forwarder, join_receiver) = handlers::MatrixJoinRoom::with_forwarder(test.config.as_url.clone()); let (set_display_name_forwarder, set_display_name_receiver) = MessageForwarder::new(); let mut matrix_router = test.default_matrix_routes(); @@ -166,6 +168,8 @@ fn successfully_forwards_a_text_message_from_rocketchat_to_matrix_when_the_user_ helpers::simulate_message_from_rocketchat(&test.config.as_url, &payload); + // discard admin room invite + invite_receiver.recv_timeout(default_timeout()).unwrap(); // receive the invite messages let spec_user_invite_message = invite_receiver.recv_timeout(default_timeout()).unwrap(); assert!(spec_user_invite_message.contains("@spec_user:localhost")); @@ -369,15 +373,16 @@ fn no_message_is_forwarded_when_inviting_the_user_failes() { let (message_forwarder, receiver) = MessageForwarder::new(); let mut matrix_router = test.default_matrix_routes(); matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); - matrix_router.post( - InviteUserEndpoint::router_path(), - handlers::MatrixConditionalErrorResponder { - status: status::InternalServerError, - message: "Could not invite user".to_string(), - conditional_content: "new_user", - }, - "invite_user", - ); + let invite_handler = handlers::MatrixInviteUser { as_url: test.config.as_url.clone() }; + let conditional_error = handlers::MatrixConditionalErrorResponder { + status: status::InternalServerError, + message: "Could not invite user".to_string(), + conditional_content: "new_user", + }; + let mut invite_with_error = Chain::new(invite_handler); + invite_with_error.link_before(conditional_error); + matrix_router.post(InviteUserEndpoint::router_path(), invite_with_error, "invite_user_spec_channel"); + let test = test.with_matrix_routes(matrix_router) .with_rocketchat_mock() diff --git a/tests/forward_rocketchat_direct_message_to_matrix.rs b/tests/forward_rocketchat_direct_message_to_matrix.rs index acf4e75..60ddf98 100644 --- a/tests/forward_rocketchat_direct_message_to_matrix.rs +++ b/tests/forward_rocketchat_direct_message_to_matrix.rs @@ -35,7 +35,7 @@ fn successfully_forwards_a_direct_message() { let test = Test::new(); let (create_room_forwarder, create_room_receiver) = handlers::MatrixCreateRoom::with_forwarder(test.config.as_url.clone()); let (register_forwarder, register_receiver) = handlers::MatrixRegister::with_forwarder(); - let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = handlers::MatrixInviteUser::with_forwarder(test.config.as_url.clone()); let (message_forwarder, receiver) = MessageForwarder::new(); let mut matrix_router = test.default_matrix_routes(); matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); @@ -81,7 +81,7 @@ fn successfully_forwards_a_direct_message() { create_room_receiver.recv_timeout(default_timeout()).unwrap(); let create_room_message = create_room_receiver.recv_timeout(default_timeout()).unwrap(); - assert!(create_room_message.contains("\"room_alias_name\":\"rocketchat#rc_id#spec_user_id_other_user_id\"")); + assert!(create_room_message.contains("\"room_alias_name\":\"rocketchat#rc_id#spec_user_id_other_user_id#dm\"")); assert!(create_room_message.contains("\"name\":\"other_user (DM Rocket.Chat)\"")); // discard bot registration @@ -90,8 +90,8 @@ fn successfully_forwards_a_direct_message() { let register_message = register_receiver.recv_timeout(default_timeout()).unwrap(); assert!(register_message.contains("\"username\":\"rocketchat_other_user_id_rc_id\"")); - let spec_user_invite = invite_receiver.recv_timeout(default_timeout()).unwrap(); - assert!(spec_user_invite.contains("\"user_id\":\"@spec_user:localhost\"")); + // discard admin room invite + invite_receiver.recv_timeout(default_timeout()).unwrap(); // discard welcome message receiver.recv_timeout(default_timeout()).unwrap(); @@ -100,6 +100,9 @@ fn successfully_forwards_a_direct_message() { // discard login message receiver.recv_timeout(default_timeout()).unwrap(); + let spec_user_invite = invite_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(spec_user_invite.contains("\"user_id\":\"@spec_user:localhost\"")); + let first_message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); assert!(first_message_received_by_matrix.contains("Hey there")); @@ -183,7 +186,10 @@ fn the_bot_user_stays_in_the_direct_message_room_if_the_user_leaves() { UserId::try_from("@spec_user:localhost").unwrap(), ); - // spec user leaves + // discard bot leave + assert!(leave_receiver.recv_timeout(default_timeout()).is_ok()); + + // discard spec user leave assert!(leave_receiver.recv_timeout(default_timeout()).is_ok()); // no more calls to the leave and forget endpoints, because the virtual user stays in the room @@ -216,7 +222,7 @@ fn successfully_forwards_a_direct_message_to_a_room_that_was_bridged_before() { let mut matrix_router = test.default_matrix_routes(); let (message_forwarder, receiver) = MessageForwarder::new(); - let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = handlers::MatrixInviteUser::with_forwarder(test.config.as_url.clone()); matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); matrix_router.post(InviteEndpoint::router_path(), invite_forwarder, "invite_user"); @@ -250,6 +256,9 @@ fn successfully_forwards_a_direct_message_to_a_room_that_was_bridged_before() { let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); assert!(message_received_by_matrix.contains("Hey there")); + // discard admin room invite + invite_receiver.recv_timeout(default_timeout()).unwrap(); + let initial_invite = invite_receiver.recv_timeout(default_timeout()).unwrap(); assert!(initial_invite.contains("@spec_user:localhost")); @@ -278,6 +287,9 @@ fn successfully_forwards_a_direct_message_to_a_room_that_was_bridged_before() { helpers::simulate_message_from_rocketchat(&test.config.as_url, &direct_message_payload); + // discard bot invite into direct message room + invite_receiver.recv_timeout(default_timeout()).unwrap(); + let invite_to_rejoin = invite_receiver.recv_timeout(default_timeout()).unwrap(); assert!(invite_to_rejoin.contains("@spec_user:localhost")); @@ -306,7 +318,7 @@ fn do_not_forwards_a_direct_message_to_a_room_if_the_user_is_no_longer_logged_in let mut matrix_router = test.default_matrix_routes(); let (message_forwarder, receiver) = MessageForwarder::new(); - let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = handlers::MatrixInviteUser::with_forwarder(test.config.as_url.clone()); matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); matrix_router.post(InviteEndpoint::router_path(), invite_forwarder, "invite_user"); @@ -340,6 +352,9 @@ fn do_not_forwards_a_direct_message_to_a_room_if_the_user_is_no_longer_logged_in let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); assert!(message_received_by_matrix.contains("Hey there")); + // discard admin room invite + invite_receiver.recv_timeout(default_timeout()).unwrap(); + let initial_invite = invite_receiver.recv_timeout(default_timeout()).unwrap(); assert!(initial_invite.contains("@spec_user:localhost")); diff --git a/tests/matrix-rocketchat-test/handlers.rs b/tests/matrix-rocketchat-test/handlers.rs index 4a6bc8b..42d4851 100644 --- a/tests/matrix-rocketchat-test/handlers.rs +++ b/tests/matrix-rocketchat-test/handlers.rs @@ -14,14 +14,15 @@ use persistent::Write; use router::Router; use ruma_client_api::r0::alias::get_alias; use ruma_client_api::r0::account::register; +use ruma_client_api::r0::membership::invite_user; use ruma_client_api::r0::room::create_room; use ruma_client_api::r0::sync::get_member_events; use ruma_events::EventType; use ruma_events::room::member::{MemberEvent, MemberEventContent, MembershipState}; use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId}; use serde_json; -use super::{DEFAULT_LOGGER, Message, MessageForwarder, RoomAliasMap, RoomsStatesMap, TestError, UsernameList, UsersInRoomMap, - extract_payload, helpers}; +use super::{DEFAULT_LOGGER, Message, MessageForwarder, PendingInvites, RoomAliasMap, RoomsStatesMap, TestError, UsernameList, + UsersInRooms, extract_payload, helpers}; #[derive(Serialize)] pub struct RocketchatInfo { @@ -315,21 +316,19 @@ impl Handler for MatrixCreateRoom { let test_room_id = format!("!{}_id:localhost", &room_id_local_part); let mut room_id = RoomId::try_from(&test_room_id).unwrap(); - - // check if the room id aleady exists, if it does, append `_next` to it - if get_state_from_room(request, room_id.clone(), "creator".to_string()).is_some() { - let next_room_id = format!("!{}_next_id:localhost", &room_id_local_part); - room_id = RoomId::try_from(&next_room_id).unwrap(); + let user_id = user_id_from_request(request); + + // scope to release the mutex + { + // check if the room id already exists, if it does, append `_next` to it + let users_in_rooms_mutex = request.get::>().unwrap(); + let users_in_rooms = users_in_rooms_mutex.lock().unwrap(); + if users_in_rooms.get(&room_id).is_some() { + let next_room_id = format!("!{}_next_id:localhost", &room_id_local_part); + room_id = RoomId::try_from(&next_room_id).unwrap(); + } } - let url: Url = request.url.clone().into(); - let mut query_pairs = url.query_pairs(); - let (_, user_id_param) = query_pairs.find(|&(ref key, _)| key == "user_id").unwrap_or(( - Cow::from("user_id"), - Cow::from("@rocketchat:localhost"), - )); - let user_id = UserId::try_from(user_id_param.borrow()).unwrap(); - if let Err(err) = add_membership_event_to_room(request, user_id.clone(), room_id.clone(), MembershipState::Join) { debug!(DEFAULT_LOGGER, format!("{}", err)); let payload = r#"{ @@ -340,7 +339,15 @@ impl Handler for MatrixCreateRoom { return Ok(Response::with((status::Conflict, payload.to_string()))); } - add_state_to_room(request, room_id.clone(), "creator".to_string(), user_id.to_string()); + if let Err(err) = add_state_to_room(request, &user_id, room_id.clone(), "creator".to_string(), user_id.to_string()) { + debug!(DEFAULT_LOGGER, format!("{}", err)); + let payload = r#"{ + "errcode":"M_FORBIDDEN", + "error":"ERR_MSG" + }"# + .replace("ERR_MSG", err); + return Ok(Response::with((status::Forbidden, payload.to_string()))); + } if let Some(room_alias_name) = create_room_payload.room_alias_name { let room_alias_id = RoomAliasId::try_from(&format!("#{}:localhost", room_alias_name)).unwrap(); @@ -354,10 +361,25 @@ impl Handler for MatrixCreateRoom { return Ok(Response::with((status::Conflict, payload.to_string()))); } - add_state_to_room(request, room_id.clone(), "alias".to_string(), room_alias_id.to_string()); + if let Err(err) = add_state_to_room( + request, + &user_id, + room_id.clone(), + "alias".to_string(), + room_alias_id.to_string(), + ) + { + debug!(DEFAULT_LOGGER, format!("{}", err)); + let payload = r#"{ + "errcode":"M_FORBIDDEN", + "error":"ERR_MSG" + }"# + .replace("ERR_MSG", err); + return Ok(Response::with((status::Forbidden, payload.to_string()))); + } } - helpers::send_join_event_from_matrix(&self.as_url, room_id.clone(), user_id); + helpers::send_join_event_from_matrix(&self.as_url, room_id.clone(), user_id, None); let response = create_room::Response { room_id: room_id }; let payload = serde_json::to_string(&response).unwrap(); @@ -384,6 +406,7 @@ impl Handler for SendRoomState { let url_room_id = params.find("room_id").unwrap(); let decoded_room_id = percent_decode(url_room_id.as_bytes()).decode_utf8().unwrap(); let room_id = RoomId::try_from(&decoded_room_id).unwrap(); + let user_id = user_id_from_request(request); let request_payload = extract_payload(request); let room_states_payload: serde_json::Value = serde_json::from_str(&request_payload).unwrap(); @@ -391,7 +414,16 @@ impl Handler for SendRoomState { match room_states_payload { serde_json::Value::Object(room_states) => { for (k, v) in room_states { - add_state_to_room(request, room_id.clone(), k, v.to_string().trim_matches('"').to_string()); + let value = v.to_string().trim_matches('"').to_string(); + if let Err(err) = add_state_to_room(request, &user_id, room_id.clone(), k, value) { + debug!(DEFAULT_LOGGER, format!("{}", err)); + let payload = r#"{ + "errcode":"M_FORBIDDEN", + "error":"ERR_MSG" + }"# + .replace("ERR_MSG", err); + return Ok(Response::with((status::Forbidden, payload.to_string()))); + } } } _ => panic!("JSON type not covered"), @@ -417,12 +449,32 @@ impl Handler for RoomMembers { let decoded_room_id = percent_decode(url_room_id.as_bytes()).decode_utf8().unwrap(); let room_id = RoomId::try_from(&decoded_room_id).unwrap(); - let mutex = request.get::>().unwrap(); - let user_in_room_map = mutex.lock().unwrap(); - let empty_users = Vec::new(); - let user_ids = &user_in_room_map.get(&room_id).unwrap_or(&empty_users); + let url: Url = request.url.clone().into(); + let mut query_pairs = url.query_pairs(); + let (_, user_id_param) = query_pairs.find(|&(ref key, _)| key == "user_id").unwrap_or(( + Cow::from("user_id"), + Cow::from("@rocketchat:localhost"), + )); + let user_id = UserId::try_from(user_id_param.borrow()).unwrap(); - let member_events = build_member_events_from_user_ids(user_ids, room_id); + let mutex = request.get::>().unwrap(); + let mut users_in_rooms = mutex.lock().unwrap(); + let mut empty_users_in_room = HashMap::new(); + + let users_in_room_for_users = users_in_rooms.get_mut(&room_id).unwrap_or(&mut empty_users_in_room); + let users_in_room_for_user = match users_in_room_for_users.get(&user_id) { + Some(&(_, ref users_in_room_for_user)) => users_in_room_for_user, + None => { + let payload = r#"{ + "errcode":"M_GUEST_ACCESS_FORBIDDEN", + "error":"User is not in room" + }"#; + + return Ok(Response::with((status::Forbidden, payload.to_string()))); + } + }; + + let member_events = build_member_events_from_user_ids(&users_in_room_for_user, room_id); let response = get_member_events::Response { chunk: member_events }; let payload = serde_json::to_string(&response).unwrap(); @@ -549,14 +601,27 @@ impl Handler for GetRoomState { let url_event_type = params.find("event_type").unwrap(); let event_type = percent_decode(url_event_type.as_bytes()).decode_utf8().unwrap(); let event_type_value: serde_json::Value = event_type.clone().into(); + let user_id = user_id_from_request(request); - let state_option = match serde_json::from_value::(event_type_value).unwrap() { - EventType::RoomCreate => get_state_from_room(request, room_id, "creator".to_string()), - EventType::RoomCanonicalAlias => get_state_from_room(request, room_id, "alias".to_string()), - EventType::RoomTopic => get_state_from_room(request, room_id, "topic".to_string()), + let state_result = match serde_json::from_value::(event_type_value).unwrap() { + EventType::RoomCreate => get_state_from_room(request, room_id, user_id.clone(), "creator".to_string()), + EventType::RoomCanonicalAlias => get_state_from_room(request, room_id, user_id.clone(), "alias".to_string()), + EventType::RoomTopic => get_state_from_room(request, room_id, user_id.clone(), "topic".to_string()), _ => panic!("Event type {} not covered", event_type), }; + let state_option = match state_result { + Ok(state_option) => state_option, + Err(err) => { + let payload = r#"{ + "errcode":"M_GUEST_ACCESS_FORBIDDEN", + "error":"ERR_MSG" + }"# + .replace("ERR_MSG", err); + return Ok(Response::with((status::Forbidden, payload.to_string()))); + } + }; + let (k, v) = match state_option { Some((k, v)) => (k, v), None => { @@ -620,6 +685,30 @@ impl Handler for MatrixJoinRoom { )); let user_id = UserId::try_from(user_id_param.borrow()).unwrap(); + let inviter_id; + // scope to release the mutex + { + let mutex = request.get::>().unwrap(); + let mut pending_invites_for_rooms = mutex.lock().unwrap(); + let mut empty_invites = HashMap::new(); + let pending_invites_for_room = pending_invites_for_rooms.get_mut(&room_id).unwrap_or(&mut empty_invites); + inviter_id = match pending_invites_for_room.get(&user_id) { + Some(inviter_id) => inviter_id.clone(), + None => { + debug!( + DEFAULT_LOGGER, + format!("Matrix mock server: Join failed, because user {} is not invited to room {}", user_id, room_id) + ); + + let payload = r#"{ + "errcode":"M_UNKNOWN", + "error":"User not invited" + }"#; + return Ok(Response::with((status::Conflict, payload.to_string()))); + } + }; + } + if let Err(err) = add_membership_event_to_room(request, user_id.clone(), room_id.clone(), MembershipState::Join) { debug!(DEFAULT_LOGGER, format!("{}", err)); let payload = r#"{ @@ -630,7 +719,54 @@ impl Handler for MatrixJoinRoom { return Ok(Response::with((status::Conflict, payload.to_string()))); } - helpers::send_join_event_from_matrix(&self.as_url, room_id, user_id); + helpers::send_join_event_from_matrix(&self.as_url, room_id, user_id, Some(inviter_id)); + + Ok(Response::with((status::Ok, "{}"))) + } +} + +pub struct MatrixInviteUser { + pub as_url: String, +} + +impl MatrixInviteUser { + pub fn with_forwarder(as_url: String) -> (Chain, Receiver) { + let (message_forwarder, receiver) = MessageForwarder::new(); + let mut chain = Chain::new(MatrixInviteUser { as_url: as_url }); + chain.link_before(message_forwarder);; + (chain, receiver) + } +} + +impl Handler for MatrixInviteUser { + fn handle(&self, request: &mut Request) -> IronResult { + debug!(DEFAULT_LOGGER, "Matrix mock server got invite user to room request"); + let params = request.extensions.get::().unwrap().clone(); + let url_room_id = params.find("room_id").unwrap(); + let decoded_room_id = percent_decode(url_room_id.as_bytes()).decode_utf8().unwrap(); + let room_id = RoomId::try_from(&decoded_room_id).unwrap(); + + let url: Url = request.url.clone().into(); + let mut query_pairs = url.query_pairs(); + let (_, user_id_param) = query_pairs.find(|&(ref key, _)| key == "user_id").unwrap_or(( + Cow::from("user_id"), + Cow::from("@rocketchat:localhost"), + )); + let inviter_id = UserId::try_from(user_id_param.borrow()).unwrap(); + + let request_payload = extract_payload(request); + let invite_payload: invite_user::BodyParams = serde_json::from_str(&request_payload).unwrap(); + let invitee_id = invite_payload.user_id.clone(); + + // scope to release the mutex, because when sending the invite event the AS will send a + // join request immediately + { + let mutex = request.get::>().unwrap(); + let mut pending_invites_for_rooms = mutex.lock().unwrap(); + add_pending_invite(&mut pending_invites_for_rooms, room_id.clone(), inviter_id.clone(), invitee_id.clone()); + } + + helpers::send_invite_event_from_matrix(&self.as_url, room_id, invitee_id, inviter_id); Ok(Response::with((status::Ok, "{}"))) } @@ -684,8 +820,8 @@ impl Handler for MatrixLeaveRoom { pub struct EmptyJson {} impl Handler for EmptyJson { - fn handle(&self, _request: &mut Request) -> IronResult { - debug!(DEFAULT_LOGGER, "Matrix mock server got empty json request"); + fn handle(&self, request: &mut Request) -> IronResult { + debug!(DEFAULT_LOGGER, "Matrix mock server got empty json request for URL {}", request.url); Ok(Response::with((status::Ok, "{}"))) } } @@ -796,35 +932,20 @@ pub struct InvalidJsonResponse { } impl Handler for InvalidJsonResponse { - fn handle(&self, _request: &mut Request) -> IronResult { - debug!(DEFAULT_LOGGER, "Matrix mock server got invali JSON responder request"); + fn handle(&self, request: &mut Request) -> IronResult { + debug!(DEFAULT_LOGGER, "Matrix mock server got invalid JSON responder request for URL {}", request.url); Ok(Response::with((self.status, "invalid json"))) } } -pub struct MembersOnly {} +pub struct PermissionCheck {} -impl BeforeMiddleware for MembersOnly { +impl BeforeMiddleware for PermissionCheck { fn before(&self, request: &mut Request) -> IronResult<()> { - let params = request.extensions.get::().unwrap().clone(); - let room_id = match params.find("room_id") { - Some(url_room_id) => { - let decoded_room_id = percent_decode(url_room_id.as_bytes()).decode_utf8().unwrap(); - RoomId::try_from(&decoded_room_id).unwrap() - } - None => return Ok(()), - }; - - let url: Url = request.url.clone().into(); - let mut query_pairs = url.query_pairs(); - let (_, user_id_param) = query_pairs.find(|&(ref key, _)| key == "user_id").unwrap_or(( - Cow::from("user_id"), - Cow::from("@rocketchat:localhost"), - )); - let user_id = UserId::try_from(user_id_param.borrow()).unwrap(); + let user_id = user_id_from_request(request); if !user_id.to_string().starts_with("@rocketchat") { - info!(DEFAULT_LOGGER, "Received request for {} with user that the AS cannot masquerade as {}", url, user_id); + info!(DEFAULT_LOGGER, "Received request for {} with user that the AS can't masquerade as {}", request.url, user_id); let response = MatrixErrorResponse { errcode: "M_FORBIDDEN".to_string(), error: "Application service cannot masquerade as this user.".to_string(), @@ -835,82 +956,157 @@ impl BeforeMiddleware for MembersOnly { return Err(err.into()); } - if !is_user_in_room(request, &user_id, &room_id) { - info!(DEFAULT_LOGGER, "Received request for {} that is not accessible for the user {}", url, user_id); - let response = MatrixErrorResponse { - errcode: "M_FORBIDDEN".to_string(), - error: "Guest access not allowed".to_string(), - }; - let payload = serde_json::to_string(&response).unwrap(); - - let err = IronError::new(TestError("MembersOnly Error".to_string()), (status::Forbidden, payload)); - return Err(err.into()); - } - Ok(()) } } - -fn add_state_to_room(request: &mut Request, room_id: RoomId, state_key: String, state_value: String) { +fn add_state_to_room( + request: &mut Request, + user_id: &UserId, + room_id: RoomId, + state_key: String, + state_value: String, +) -> Result<(), &'static str> { debug!(DEFAULT_LOGGER, "Matrix mock server adds room state {} with value {}", state_key, state_value); - let mutex = request.get::>().unwrap(); - let mut rooms_states = mutex.lock().unwrap(); + let users_in_rooms_mutex = request.get::>().unwrap(); + let users_in_rooms = users_in_rooms_mutex.lock().unwrap(); + let empty_users_in_room = HashMap::new(); + let users_in_room = users_in_rooms.get(&room_id).unwrap_or(&empty_users_in_room); + if !users_in_room.contains_key(user_id) { + debug!(DEFAULT_LOGGER, "Matrix mock server: User {} not in room {}", user_id, room_id); + return Err("User not in room"); + } - if !rooms_states.contains_key(&room_id) { - rooms_states.insert(room_id.clone(), HashMap::new()); + let rooms_states_mutex = request.get::>().unwrap(); + let mut rooms_states_for_users = rooms_states_mutex.lock().unwrap(); + + if !rooms_states_for_users.contains_key(&room_id) { + rooms_states_for_users.insert(room_id.clone(), HashMap::new()); + } + + let room_states_for_users = rooms_states_for_users.get_mut(&room_id).unwrap(); + for (_, membership_with_room_states) in room_states_for_users { + let &(membership_state, _) = users_in_room.get(user_id).unwrap(); + let room_states = membership_with_room_states; + if membership_state == MembershipState::Join { + room_states.insert(state_key.clone(), state_value.clone()); + } } - let mut room_states = rooms_states.get_mut(&room_id).unwrap(); - room_states.insert(state_key, state_value); + Ok(()) } -fn get_state_from_room(request: &mut Request, room_id: RoomId, state_key: String) -> Option<(String, String)> { +fn get_state_from_room( + request: &mut Request, + room_id: RoomId, + user_id: UserId, + state_key: String, +) -> Result, &'static str> { debug!(DEFAULT_LOGGER, "Matrix mock server gets room state {}", state_key); let mutex = request.get::>().unwrap(); - let mut rooms_states = mutex.lock().unwrap(); - let room_states = match rooms_states.get_mut(&room_id) { + let mut rooms_states_for_users = mutex.lock().unwrap(); + + let users_with_room_states = match rooms_states_for_users.get_mut(&room_id) { + Some(users_with_room_states) => users_with_room_states, + None => { + return Ok(None); + } + }; + + let room_states: &mut HashMap = match users_with_room_states.get_mut(&user_id) { Some(room_states) => room_states, None => { - return None; + debug!(DEFAULT_LOGGER, "Matrix mock server: User {} not in room {}", user_id, room_id); + return Err("User not in room"); } }; let room_state = match room_states.get(&state_key) { Some(room_state) => room_state, None => { - return None; + return Ok(None); } }; - Some((state_key.clone(), room_state.to_string())) + debug!(DEFAULT_LOGGER, "Matrix mock server found state {} for key {}", room_state, state_key); + Ok(Some((state_key.clone(), room_state.to_string()))) } fn add_membership_event_to_room( request: &mut Request, user_id: UserId, room_id: RoomId, - membership_event: MembershipState, + membership_state: MembershipState, ) -> Result<(), &'static str> { - let mutex = request.get::>().unwrap(); - let mut user_in_room_map = mutex.lock().unwrap(); - if !user_in_room_map.contains_key(&room_id) { - user_in_room_map.insert(room_id.clone(), Vec::new()); - } - - let mut users = user_in_room_map.get_mut(&room_id).unwrap(); - if users.iter().any(|&(ref id, membership)| id == &user_id && membership == membership_event) { - match membership_event { - MembershipState::Join => return Err("User is already in room"), - MembershipState::Leave => return Err("User not in room"), - _ => return Err("Unknown state"), + let mutex = request.get::>().unwrap(); + let mut users_in_rooms = mutex.lock().unwrap(); + let empty_users_in_room = HashMap::new(); + + if !users_in_rooms.contains_key(&room_id) { + users_in_rooms.insert(room_id.clone(), empty_users_in_room); + } + + let users_in_room_for_users = users_in_rooms.get_mut(&room_id).unwrap(); + + for (id, membership_with_room_states) in users_in_room_for_users.iter() { + let &(membership, _) = membership_with_room_states; + if id == &user_id && membership == membership_state { + match membership_state { + MembershipState::Join => return Err("User is already in room"), + MembershipState::Leave => return Err("User not in room"), + _ => return Err("Unknown membership state"), + } + } + } + + //TODO: This is way to complicated, but we have to track the state events for the rooms for + //joined users and users that left the room differently. Needs refactoring. + let mut existing_users = Vec::new(); + let mut existing_user_in_room = None; + for (id, membership_with_users) in users_in_room_for_users.iter() { + let &(membership, ref users) = membership_with_users; + if membership == MembershipState::Join { + existing_users = users.clone(); + existing_user_in_room = Some(id.clone()); + break; + } + } + + let mut existing_states = HashMap::new(); + if membership_state == MembershipState::Join { + let rooms_states_mutex = request.get::>().unwrap(); + let mut rooms_states_for_users = rooms_states_mutex.lock().unwrap(); + if !rooms_states_for_users.contains_key(&room_id) { + rooms_states_for_users.insert(room_id.clone(), HashMap::new()); + } + let room_states_for_users = rooms_states_for_users.get_mut(&room_id).unwrap(); + + if let Some(existing_user_in_room) = existing_user_in_room { + existing_states = room_states_for_users.get(&existing_user_in_room).unwrap_or(&HashMap::new()).clone(); + } + + room_states_for_users.insert(user_id.clone(), existing_states); + } + + if !users_in_room_for_users.contains_key(&user_id) { + users_in_room_for_users.insert(user_id.clone(), (membership_state.clone(), existing_users)); + } + + // update the users own membership state + let users = users_in_room_for_users.get(&user_id).unwrap().1.clone(); + users_in_room_for_users.insert(user_id.clone(), (membership_state, users)); + + // update the memberships state for all users that are currently in the room + for (_, membership_with_users) in users_in_room_for_users { + let &mut (ref mut membership, ref mut users) = membership_with_users; + if membership == &MembershipState::Join { + users.retain(|&(ref id, _)| id != &user_id); + users.push((user_id.clone(), membership_state.clone())); } } - users.retain(|&(ref id, _)| id != &user_id); - users.push((user_id, membership_event)); Ok(()) } @@ -929,7 +1125,7 @@ fn add_alias_to_room(request: &mut Request, room_id: RoomId, room_alias: RoomAli room_alias_map.insert(room_id.clone(), Vec::new()); } - let mut aliases = room_alias_map.get_mut(&room_id).unwrap(); + let aliases = room_alias_map.get_mut(&room_id).unwrap(); debug!(DEFAULT_LOGGER, "Matrix mock server adds alias {} to room {}", room_alias, room_id);; aliases.push(room_alias); @@ -976,17 +1172,29 @@ fn room_id_from_alias_map( None } -fn is_user_in_room(request: &mut Request, user_id: &UserId, room_id: &RoomId) -> bool { - let mutex = request.get::>().unwrap(); - let user_in_room_map = mutex.lock().unwrap(); - let empty_users = Vec::new(); - let user_ids = &user_in_room_map.get(room_id).unwrap_or(&empty_users); +fn user_id_from_request(request: &mut Request) -> UserId { + let url: Url = request.url.clone().into(); + let mut query_pairs = url.query_pairs(); + let (_, user_id_param) = query_pairs.find(|&(ref key, _)| key == "user_id").unwrap_or(( + Cow::from("user_id"), + Cow::from("@rocketchat:localhost"), + )); + UserId::try_from(user_id_param.borrow()).unwrap() +} - for &(ref id, ref state) in user_ids.iter() { - if id == user_id && state == &MembershipState::Join { - return true; - } +fn add_pending_invite( + pending_invites_for_rooms: &mut MutexGuard>>, + room_id: RoomId, + inviter_id: UserId, + invitee_id: UserId, +) { + let empty_pending_invites_for_room = HashMap::new(); + + if !pending_invites_for_rooms.contains_key(&room_id) { + pending_invites_for_rooms.insert(room_id.clone(), empty_pending_invites_for_room); } - false + let pending_invites_for_room = pending_invites_for_rooms.get_mut(&room_id).unwrap(); + + pending_invites_for_room.insert(invitee_id, inviter_id); } diff --git a/tests/matrix-rocketchat-test/helpers.rs b/tests/matrix-rocketchat-test/helpers.rs index 4f07359..6db3b45 100644 --- a/tests/matrix-rocketchat-test/helpers.rs +++ b/tests/matrix-rocketchat-test/helpers.rs @@ -12,10 +12,28 @@ use ruma_events::collections::all::Event; use ruma_events::room::member::{MemberEvent, MemberEventContent, MembershipState}; use ruma_events::room::message::{MessageEvent, MessageEventContent, MessageType, TextMessageEventContent}; use ruma_identifiers::{EventId, RoomId, UserId}; -use serde_json::to_string; +use serde_json::{Map, Value, to_string}; use super::{DEFAULT_LOGGER, HS_TOKEN}; -pub fn invite(as_url: &str, room_id: RoomId, sender_id: UserId, user_id: UserId) { +pub fn invite(config: &Config, room_id: RoomId, user_id: UserId, sender_id: UserId) { + let matrix_api = MatrixApi::new(config, DEFAULT_LOGGER.clone()).unwrap(); + matrix_api.invite(room_id, user_id, sender_id).unwrap(); +} + +pub fn join(config: &Config, room_id: RoomId, user_id: UserId) { + let matrix_api = MatrixApi::new(config, DEFAULT_LOGGER.clone()).unwrap(); + matrix_api.join(room_id, user_id).unwrap(); +} + +pub fn create_room(config: &Config, room_name: &str, sender_id: UserId, user_id: UserId) { + let matrix_api = MatrixApi::new(&config, DEFAULT_LOGGER.clone()).unwrap(); + matrix_api.create_room(Some(room_name.to_string()), None, &sender_id).unwrap(); + + let room_id = RoomId::try_from(&format!("!{}_id:localhost", room_name)).unwrap(); + invite(&config, room_id, user_id, sender_id); +} + +pub fn send_invite_event_from_matrix(as_url: &str, room_id: RoomId, user_id: UserId, inviter_id: UserId) { let invite_event = MemberEvent { content: MemberEventContent { avatar_url: None, @@ -30,7 +48,7 @@ pub fn invite(as_url: &str, room_id: RoomId, sender_id: UserId, user_id: UserId) room_id: room_id.clone(), state_key: format!("{}", user_id), unsigned: None, - user_id: sender_id, + user_id: inviter_id, }; let events = Events { events: vec![Box::new(Event::RoomMember(invite_event))] }; @@ -40,20 +58,15 @@ pub fn invite(as_url: &str, room_id: RoomId, sender_id: UserId, user_id: UserId) simulate_message_from_matrix(as_url, &invite_payload); } -pub fn join(config: &Config, room_id: RoomId, user_id: UserId) { - let matrix_api = MatrixApi::new(config, DEFAULT_LOGGER.clone()).unwrap(); - matrix_api.join(room_id, user_id).unwrap(); -} +pub fn send_join_event_from_matrix(as_url: &str, room_id: RoomId, user_id: UserId, inviter_id: Option) { + let mut unsigned: Option = None; -pub fn create_room(config: &Config, room_name: &str, sender_id: UserId, user_id: UserId) { - let matrix_api = MatrixApi::new(&config, DEFAULT_LOGGER.clone()).unwrap(); - matrix_api.create_room(Some(room_name.to_string()), None, &sender_id).unwrap(); - - let room_id = RoomId::try_from(&format!("!{}_id:localhost", room_name)).unwrap(); - invite(&config.as_url, room_id, sender_id, user_id); -} + if let Some(inviter_id) = inviter_id { + let mut unsigned_content = Map::new(); + unsigned_content.insert("prev_sender".to_string(), Value::String(inviter_id.to_string())); + unsigned = Some(Value::Object(unsigned_content)) + } -pub fn send_join_event_from_matrix(as_url: &str, room_id: RoomId, user_id: UserId) { let join_event = MemberEvent { content: MemberEventContent { avatar_url: None, @@ -67,7 +80,7 @@ pub fn send_join_event_from_matrix(as_url: &str, room_id: RoomId, user_id: UserI prev_content: None, room_id: room_id, state_key: format!("{}", &user_id), - unsigned: None, + unsigned: unsigned, user_id: user_id, }; diff --git a/tests/matrix-rocketchat-test/lib.rs b/tests/matrix-rocketchat-test/lib.rs index 133ef96..82d6ae9 100644 --- a/tests/matrix-rocketchat-test/lib.rs +++ b/tests/matrix-rocketchat-test/lib.rs @@ -56,6 +56,7 @@ use ruma_identifiers::{RoomAliasId, RoomId, UserId}; use ruma_client_api::Endpoint; use ruma_client_api::r0::alias::get_alias::Endpoint as GetAliasEndpoint; use ruma_client_api::r0::alias::delete_alias::Endpoint as DeleteAliasEndpoint; +use ruma_client_api::r0::membership::invite_user::Endpoint as InviteUserEndpoint; use ruma_client_api::r0::account::register::Endpoint as RegisterEndpoint; use ruma_client_api::r0::membership::join_room_by_id::Endpoint as JoinRoomByIdEndpoint; use ruma_client_api::r0::membership::leave_room::Endpoint as LeaveRoomEndpoint; @@ -78,7 +79,7 @@ pub const HS_TOKEN: &'static str = "ht"; /// Rocket.Chat token used in the tests pub const RS_TOKEN: &'static str = "rt"; /// Number of threads that iron uses when running tests -pub const IRON_THREADS: usize = 3; +pub const IRON_THREADS: usize = 4; /// The version the mock Rocket.Chat server announces pub const DEFAULT_ROCKETCHAT_VERSION: &'static str = "0.49.0"; @@ -91,7 +92,8 @@ lazy_static! { Some("debug") => Level::Debug, _ => Level::Info, }; - slog::Logger::root(LevelFilter::new(slog_term::streamer().full().build(), log_level).fuse(), o!("version" => env!("CARGO_PKG_VERSION"), "place" => file_line_logger_format)) + slog::Logger::root(LevelFilter::new(slog_term::streamer().full().build(), log_level).fuse(), + o!("version" => env!("CARGO_PKG_VERSION"), "place" => file_line_logger_format)) }; } @@ -114,7 +116,7 @@ pub use message_forwarder::{Message, MessageForwarder}; pub struct UsernameList; #[derive(Copy, Clone)] -pub struct UsersInRoomMap; +pub struct UsersInRooms; #[derive(Copy, Clone)] pub struct RoomsStatesMap; @@ -122,22 +124,29 @@ pub struct RoomsStatesMap; #[derive(Copy, Clone)] pub struct RoomAliasMap; +#[derive(Copy, Clone)] +pub struct PendingInvites; + impl Key for UsernameList { type Value = Vec; } -impl Key for UsersInRoomMap { - type Value = HashMap>; +impl Key for UsersInRooms { + type Value = HashMap)>>; } impl Key for RoomsStatesMap { - type Value = HashMap>; + type Value = HashMap>>; } impl Key for RoomAliasMap { type Value = HashMap>; } +impl Key for PendingInvites { + type Value = HashMap>; +} + #[derive(Debug)] struct TestError(String); @@ -318,9 +327,10 @@ impl Test { thread::spawn(move || { let mut chain = Chain::new(router); chain.link_before(Write::::one(Vec::new())); - chain.link_before(Write::::one(HashMap::new())); + chain.link_before(Write::::one(HashMap::new())); chain.link_before(Write::::one(HashMap::new())); chain.link_before(Write::::one(HashMap::new())); + chain.link_before(Write::::one(HashMap::new())); let mut server = Iron::new(chain); server.threads = IRON_THREADS; let listening = server.http(&hs_socket_addr).unwrap(); @@ -416,12 +426,7 @@ impl Test { let rocketchat_user_id = UserId::try_from("@rocketchat:localhost").unwrap(); matrix_api.create_room(Some("admin_room".to_string()), None, &spec_user_id).unwrap(); - helpers::invite( - &self.config.as_url, - RoomId::try_from("!admin_room_id:localhost").unwrap(), - spec_user_id, - rocketchat_user_id, - ); + helpers::invite(&self.config, RoomId::try_from("!admin_room_id:localhost").unwrap(), rocketchat_user_id, spec_user_id); } fn create_connected_admin_room(&self) { @@ -468,11 +473,11 @@ impl Test { router.get("/_matrix/client/versions", handlers::MatrixVersion { versions: default_matrix_api_versions() }, "versions"); let mut get_state_event = Chain::new(handlers::GetRoomState {}); - get_state_event.link_before(handlers::MembersOnly {}); + get_state_event.link_before(handlers::PermissionCheck {}); router.get(GetStateEventsForEmptyKeyEndpoint::router_path(), get_state_event, "get_state_events_for_empty_key"); let mut get_members = Chain::new(handlers::RoomMembers {}); - get_members.link_before(handlers::MembersOnly {}); + get_members.link_before(handlers::PermissionCheck {}); router.get(GetMemberEventsEndpoint::router_path(), get_members, "room_members"); router.post(RegisterEndpoint::router_path(), handlers::MatrixRegister {}, "register"); @@ -483,12 +488,15 @@ impl Test { "create_room", ); + let invite_user_handler = handlers::MatrixInviteUser { as_url: self.config.as_url.clone() }; + router.post(InviteUserEndpoint::router_path(), invite_user_handler, "invite_user"); + let mut send_room_state = Chain::new(handlers::SendRoomState {}); - send_room_state.link_before(handlers::MembersOnly {}); + send_room_state.link_before(handlers::PermissionCheck {}); router.put(SendStateEventForEmptyKeyEndpoint::router_path(), send_room_state, "send_room_state"); let mut get_room_alias = Chain::new(handlers::GetRoomAlias {}); - get_room_alias.link_before(handlers::MembersOnly {}); + get_room_alias.link_before(handlers::PermissionCheck {}); router.get(GetAliasEndpoint::router_path(), get_room_alias, "get_room_alias"); router.delete(DeleteAliasEndpoint::router_path(), handlers::DeleteRoomAlias {}, "delete_room_alias"); diff --git a/tests/matrix-rocketchat-test/message_forwarder.rs b/tests/matrix-rocketchat-test/message_forwarder.rs index e532bbd..6454fec 100644 --- a/tests/matrix-rocketchat-test/message_forwarder.rs +++ b/tests/matrix-rocketchat-test/message_forwarder.rs @@ -1,5 +1,6 @@ -use std::convert::TryFrom; use std::borrow::{Borrow, Cow}; +use std::convert::TryFrom; +use std::collections::HashMap; use std::io::Read; use std::sync::Mutex; use std::sync::mpsc::{Receiver, Sender, channel}; @@ -12,10 +13,11 @@ use iron::url::percent_encoding::percent_decode; use matrix_rocketchat::errors::MatrixErrorResponse; use persistent::Write; use router::Router; +use ruma_events::room::member::MembershipState; use ruma_identifiers::{RoomId, UserId}; use serde_json; -use super::{TestError, UsersInRoomMap, extract_payload}; +use super::{TestError, UsersInRooms, extract_payload}; /// Forwards a message from an iron handler to a channel so that it can be received outside of the /// iron handler. @@ -91,12 +93,12 @@ fn validate_message_forwarding_for_user(request: &mut Request, url: Url) -> Iron Cow::from("@rocketchat:localhost"), )); let user_id = UserId::try_from(user_id_param.borrow()).unwrap(); - let mutex = request.get::>().unwrap(); - let user_in_room_map = mutex.lock().unwrap(); - let empty_users = Vec::new(); - let user_ids = &user_in_room_map.get(&room_id).unwrap_or(&empty_users); + let mutex = request.get::>().unwrap(); + let users_in_rooms = mutex.lock().unwrap(); + let empty_users = HashMap::new(); + let users_in_room = &users_in_rooms.get(&room_id).unwrap_or(&empty_users); - if !user_ids.iter().any(|&(ref id, _)| id == &user_id) { + if !users_in_room.iter().any(|(id, &(membership, _))| id == &user_id && membership == MembershipState::Join) { let matrix_err = MatrixErrorResponse { errcode: "M_FORBIDDEN".to_string(), error: format!("{} not in room {}", user_id, room_id),