diff --git a/src/matrix-rocketchat/api/matrix/mod.rs b/src/matrix-rocketchat/api/matrix/mod.rs index 22fe467..d3ea308 100644 --- a/src/matrix-rocketchat/api/matrix/mod.rs +++ b/src/matrix-rocketchat/api/matrix/mod.rs @@ -18,7 +18,7 @@ pub mod r0; /// Matrix REST API pub trait MatrixApi: Send + Sync + MatrixApiClone { /// Create a room. - fn create_room(&self, room_name: String, matrix_user_id: UserId) -> Result; + fn create_room(&self, room_name: String) -> Result; /// Forget a room. fn forget_room(&self, matrix_room_id: RoomId) -> Result<()>; /// Get the list of members for this room. @@ -33,6 +33,9 @@ pub trait MatrixApi: Send + Sync + MatrixApiClone { fn register(&self, user_id_local_part: String) -> Result<()>; /// Send a text message to a room. fn send_text_message_event(&self, matrix_room_id: RoomId, matrix_user_id: UserId, body: String) -> Result<()>; + /// Set the default power levels for a room. Only the bot will be able to control the room. + /// The power levels for invite, kick, ban, and redact are all set to 50. + fn set_default_powerlevels(&self, matrix_room_id: RoomId, bot_user_id: UserId) -> Result<()>; /// Set the display name for a user fn set_display_name(&self, matrix_user_id: UserId, name: String) -> Result<()>; /// Set the name for a room @@ -79,10 +82,10 @@ impl MatrixApi { } let supported_versions: GetSupportedVersionsResponse = serde_json::from_str(&body).chain_err(|| { - ErrorKind::InvalidJSON(format!("Could not deserialize response from Matrix supported versions API \ + ErrorKind::InvalidJSON(format!("Could not deserialize response from Matrix supported versions API \ endpoint: `{}`", - body)) - })?; + body)) + })?; debug!(logger, format!("Homeserver supports versions {:?}", supported_versions.versions)); MatrixApi::get_max_supported_version_api(supported_versions.versions, config, logger) } diff --git a/src/matrix-rocketchat/api/matrix/r0.rs b/src/matrix-rocketchat/api/matrix/r0.rs index 8d986b1..5cd5d77 100644 --- a/src/matrix-rocketchat/api/matrix/r0.rs +++ b/src/matrix-rocketchat/api/matrix/r0.rs @@ -19,7 +19,7 @@ use ruma_events::room::message::MessageType; use ruma_identifiers::{EventId, RoomId, UserId}; use serde_json::Map; use slog::Logger; -use serde_json; +use serde_json::{self, Value}; use api::RestApi; use config::Config; @@ -54,11 +54,11 @@ impl MatrixApi { } impl super::MatrixApi for MatrixApi { - fn create_room(&self, room_name: String, matrix_user_id: UserId) -> Result { + fn create_room(&self, room_name: String) -> Result { let endpoint = self.base_url.clone() + &CreateRoomEndpoint::request_path(()); let body_params = create_room::BodyParams { creation_content: None, - invite: vec![matrix_user_id], + invite: vec![], name: Some(room_name), preset: Some(RoomPreset::PrivateChat), room_alias_name: None, @@ -208,17 +208,24 @@ impl super::MatrixApi for MatrixApi { Ok(()) } - fn set_room_name(&self, matrix_room_id: RoomId, name: String) -> Result<()> { + fn set_default_powerlevels(&self, matrix_room_id: RoomId, bot_user_id: UserId) -> Result<()> { let path_params = send_state_event_for_empty_key::PathParams { room_id: matrix_room_id, - event_type: EventType::RoomName, + event_type: EventType::RoomPowerLevels, }; let endpoint = self.base_url.clone() + &SendStateEventForEmptyKeyEndpoint::request_path(path_params); let params = self.params_hash(); let mut body_params = serde_json::Map::new(); - body_params.insert("name", name); + let mut users = serde_json::Map::new(); + users.insert(bot_user_id.to_string(), Value::I64(100)); + body_params.insert("invite", Value::I64(50)); + body_params.insert("kick", Value::I64(50)); + body_params.insert("ban", Value::I64(50)); + body_params.insert("redact", Value::I64(50)); + body_params.insert("users", Value::Object(users)); + body_params.insert("events", Value::Object(serde_json::Map::new())); - let payload = serde_json::to_string(&body_params).chain_err(|| ErrorKind::InvalidJSON("Could not serialize account body params".to_string()))?; + let payload = serde_json::to_string(&body_params).chain_err(|| ErrorKind::InvalidJSON("Could not serialize power levels body params".to_string()))?; let (body, status_code) = RestApi::call_matrix(SendStateEventForEmptyKeyEndpoint::method(), &endpoint, &payload, ¶ms)?; @@ -244,6 +251,26 @@ impl super::MatrixApi for MatrixApi { } Ok(()) } + + fn set_room_name(&self, matrix_room_id: RoomId, name: String) -> Result<()> { + let path_params = send_state_event_for_empty_key::PathParams { + room_id: matrix_room_id, + event_type: EventType::RoomName, + }; + let endpoint = self.base_url.clone() + &SendStateEventForEmptyKeyEndpoint::request_path(path_params); + let params = self.params_hash(); + let mut body_params = serde_json::Map::new(); + body_params.insert("name", name); + + let payload = serde_json::to_string(&body_params).chain_err(|| ErrorKind::InvalidJSON("Could not serialize room name body params".to_string()))?; + + let (body, status_code) = + RestApi::call_matrix(SendStateEventForEmptyKeyEndpoint::method(), &endpoint, &payload, ¶ms)?; + if !status_code.is_success() { + return Err(build_error(&endpoint, &body, &status_code)); + } + Ok(()) + } } fn build_error(endpoint: &str, body: &str, status_code: &StatusCode) -> Error { diff --git a/src/matrix-rocketchat/handlers/events/command_handler.rs b/src/matrix-rocketchat/handlers/events/command_handler.rs index 6af26c1..3d8d5de 100644 --- a/src/matrix-rocketchat/handlers/events/command_handler.rs +++ b/src/matrix-rocketchat/handlers/events/command_handler.rs @@ -355,7 +355,9 @@ impl<'a> CommandHandler<'a> { fn create_room(&self, channel: &Channel, rocketchat_server_id: i32, user_id: UserId) -> Result { let bot_matrix_user_id = self.config.matrix_bot_user_id()?; - let matrix_room_id = self.matrix_api.create_room(channel.name.clone(), user_id)?; + let matrix_room_id = self.matrix_api.create_room(channel.name.clone())?; + self.matrix_api.set_default_powerlevels(matrix_room_id.clone(), bot_matrix_user_id.clone())?; + self.matrix_api.invite(matrix_room_id.clone(), user_id.clone())?; let new_room = NewRoom { matrix_room_id: matrix_room_id.clone(), display_name: channel.name.clone(), diff --git a/tests/admin_commands_bridge.rs b/tests/admin_commands_bridge.rs index a363c2d..f6f591f 100644 --- a/tests/admin_commands_bridge.rs +++ b/tests/admin_commands_bridge.rs @@ -19,6 +19,7 @@ use ruma_client_api::Endpoint; use ruma_client_api::r0::membership::invite_user::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::Endpoint as SendStateEventForEmptyKeyEndpoint; use ruma_client_api::r0::sync::get_member_events::{self, Endpoint as GetMemberEventsEndpoint}; use ruma_identifiers::{RoomId, UserId}; @@ -26,11 +27,15 @@ use ruma_identifiers::{RoomId, UserId}; #[test] fn successfully_bridge_a_rocketchat_room() { let (message_forwarder, receiver) = MessageForwarder::new(); + let (invite_forwarder, invite_receiver) = MessageForwarder::new(); + let (state_forwarder, state_receiver) = MessageForwarder::new(); let mut matrix_router = Router::new(); matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); matrix_router.post(CreateRoomEndpoint::router_path(), handlers::MatrixCreateRoom { room_id: RoomId::try_from("!joined_channel_id:localhost").unwrap() }, "create_room"); + matrix_router.put(SendStateEventForEmptyKeyEndpoint::router_path(), state_forwarder, "send_state_event_for_key"); + matrix_router.post(InviteEndpoint::router_path(), invite_forwarder, "invite_user"); let mut channels = HashMap::new(); channels.insert("joined_channel", vec!["spec_user"]); @@ -54,9 +59,23 @@ fn successfully_bridge_a_rocketchat_room() { UserId::try_from("@spec_user:localhost").unwrap(), "bridge joined_channel".to_string()); + let invite_received_by_matrix = invite_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(invite_received_by_matrix.contains("@spec_user:localhost")); + let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); assert!(message_received_by_matrix.contains("joined_channel is now bridged.")); + let set_room_name_received_by_matrix = state_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(set_room_name_received_by_matrix.contains("Admin Room (Rocket.Chat)")); + + // only moderators and admins can invite other users + let power_levels_received_by_matrix = state_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(power_levels_received_by_matrix.contains("\"invite\":50")); + assert!(power_levels_received_by_matrix.contains("\"kick\":50")); + assert!(power_levels_received_by_matrix.contains("\"ban\":50")); + assert!(power_levels_received_by_matrix.contains("\"redact\":50")); + assert!(power_levels_received_by_matrix.contains("@rocketchat:localhost")); + // spec_user accepts invite from bot user helpers::join(&test.config.as_url, RoomId::try_from("!joined_channel_id:localhost").unwrap(), @@ -175,8 +194,11 @@ fn successfully_bridge_a_rocketchat_room_that_an_other_user_already_bridged() { RoomId::try_from("!joined_channel_id:localhost").unwrap(), UserId::try_from("@other_user:localhost").unwrap()); - let invite_received_by_matrix = invite_receiver.recv_timeout(default_timeout()).unwrap(); - assert!(invite_received_by_matrix.contains("@other_user:localhost")); + 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")); + + let other_user_invite_received_by_matrix = invite_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(other_user_invite_received_by_matrix.contains("@other_user:localhost")); let connection = test.connection_pool.get().unwrap(); let room = Room::find(&connection, &RoomId::try_from("!joined_channel_id:localhost").unwrap()).unwrap(); @@ -398,6 +420,87 @@ fn the_user_gets_a_message_when_creating_the_room_failes() { assert!(message_received_by_matrix.contains("An internal error occurred")); } +#[test] +fn the_user_gets_a_message_when_setting_the_powerlevels_failes() { + let (message_forwarder, receiver) = MessageForwarder::new(); + let mut matrix_router = Router::new(); + matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); + matrix_router.post(CreateRoomEndpoint::router_path(), + handlers::MatrixCreateRoom { room_id: RoomId::try_from("!joined_channel_id:localhost").unwrap() }, + "create_room"); + matrix_router.put(SendStateEventForEmptyKeyEndpoint::router_path(), + handlers::MatrixConditionalErrorResponder { + status: status::InternalServerError, + message: "Could not set power levels".to_string(), + conditional_content: "invite", + }, + "set_power_levels"); + let mut channels = HashMap::new(); + channels.insert("joined_channel", vec!["spec_user"]); + let test = Test::new() + .with_matrix_routes(matrix_router) + .with_rocketchat_mock() + .with_connected_admin_room() + .with_logged_in_user() + .with_custom_channel_list(channels) + .run(); + + // discard welcome message + receiver.recv_timeout(default_timeout()).unwrap(); + // discard connect message + receiver.recv_timeout(default_timeout()).unwrap(); + // discard login message + receiver.recv_timeout(default_timeout()).unwrap(); + + helpers::send_room_message_from_matrix(&test.config.as_url, + RoomId::try_from("!admin:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), + "bridge joined_channel".to_string()); + + let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); + assert!(message_received_by_matrix.contains("An internal error occurred")); +} + +#[test] +fn the_user_gets_a_message_when_inviting_the_user_failes() { + let (message_forwarder, receiver) = MessageForwarder::new(); + let mut matrix_router = Router::new(); + matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); + matrix_router.post(CreateRoomEndpoint::router_path(), + handlers::MatrixCreateRoom { room_id: RoomId::try_from("!joined_channel_id:localhost").unwrap() }, + "create_room"); + matrix_router.post(InviteEndpoint::router_path(), + handlers::MatrixErrorResponder { + status: status::InternalServerError, + message: "Could not invite user".to_string(), + }, + "invite"); + let mut channels = HashMap::new(); + channels.insert("joined_channel", vec!["spec_user"]); + let test = Test::new() + .with_matrix_routes(matrix_router) + .with_rocketchat_mock() + .with_connected_admin_room() + .with_logged_in_user() + .with_custom_channel_list(channels) + .run(); + + // discard welcome message + receiver.recv_timeout(default_timeout()).unwrap(); + // discard connect message + receiver.recv_timeout(default_timeout()).unwrap(); + // discard login message + receiver.recv_timeout(default_timeout()).unwrap(); + + helpers::send_room_message_from_matrix(&test.config.as_url, + RoomId::try_from("!admin:localhost").unwrap(), + UserId::try_from("@spec_user:localhost").unwrap(), + "bridge joined_channel".to_string()); + + let message_received_by_matrix = receiver.recv_timeout(default_timeout()).unwrap(); + assert!(message_received_by_matrix.contains("An internal error occurred")); +} + #[test] fn the_user_gets_a_message_when_the_create_room_response_cannot_be_deserialized() { let (message_forwarder, receiver) = MessageForwarder::new(); diff --git a/tests/forward_rocketchat_to_matrix.rs b/tests/forward_rocketchat_to_matrix.rs index db77c56..e15c457 100644 --- a/tests/forward_rocketchat_to_matrix.rs +++ b/tests/forward_rocketchat_to_matrix.rs @@ -60,9 +60,11 @@ fn successfully_forwards_a_text_message_from_rocketchat_to_matrix_when_the_user_ helpers::simulate_message_from_rocketchat(&test.config.as_url, &payload); - // receive the invite message - let invite_message = invite_receiver.recv_timeout(default_timeout()).unwrap(); - assert!(invite_message.contains("@rocketchat_new_user_id_1:localhost")); + // 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")); + let new_user_invite_message = invite_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(new_user_invite_message.contains("@rocketchat_new_user_id_1:localhost")); // discard admin room join join_receiver.recv_timeout(default_timeout()).unwrap(); @@ -163,9 +165,11 @@ fn successfully_forwards_a_text_message_from_rocketchat_to_matrix_when_the_user_ helpers::simulate_message_from_rocketchat(&test.config.as_url, &payload); - // receive the invite message - let invite_message = invite_receiver.recv_timeout(default_timeout()).unwrap(); - assert!(invite_message.contains("@rocketchat_virtual_spec_user_id_1:localhost")); + // 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")); + let virtual_user_invite_message = invite_receiver.recv_timeout(default_timeout()).unwrap(); + assert!(virtual_user_invite_message.contains("@rocketchat_virtual_spec_user_id_1:localhost")); // discard admin room join join_receiver.recv_timeout(default_timeout()).unwrap(); @@ -356,9 +360,10 @@ fn no_message_is_forwarded_when_inviting_the_user_failes() { let mut matrix_router = Router::new(); matrix_router.put(SendMessageEventEndpoint::router_path(), message_forwarder, "send_message_event"); matrix_router.post(InviteUserEndpoint::router_path(), - handlers::MatrixErrorResponder { + handlers::MatrixConditionalErrorResponder { status: status::InternalServerError, message: "Could not invite user".to_string(), + conditional_content: "new_user", }, "invite_user"); diff --git a/tests/matrix-rocketchat-test/handlers.rs b/tests/matrix-rocketchat-test/handlers.rs index b95b940..f4c4974 100644 --- a/tests/matrix-rocketchat-test/handlers.rs +++ b/tests/matrix-rocketchat-test/handlers.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::io::Read; use rand::{Rng, thread_rng}; use iron::prelude::*; @@ -216,6 +217,30 @@ impl Handler for MatrixErrorResponder { } } +pub struct MatrixConditionalErrorResponder { + pub status: status::Status, + pub message: String, + pub conditional_content: &'static str, +} + +impl Handler for MatrixConditionalErrorResponder { + fn handle(&self, request: &mut Request) -> IronResult { + let mut request_payload = String::new(); + request.body.read_to_string(&mut request_payload).unwrap(); + + if request_payload.contains(self.conditional_content) { + let error_response = MatrixErrorResponse { + errcode: "1234".to_string(), + error: self.message.clone(), + }; + let payload = serde_json::to_string(&error_response).unwrap(); + Ok(Response::with((self.status, payload))) + } else { + Ok(Response::with((status::Ok, "{}".to_string()))) + } + } +} + pub struct InvalidJsonResponse { pub status: status::Status, }