From eacdd5662bb42ca4b8642bcf72d8297325c537d4 Mon Sep 17 00:00:00 2001 From: Andreas Studer Date: Sun, 28 Jan 2018 20:26:07 +0100 Subject: [PATCH] Use the new channels.members endpoint Because Rocket.Chat no longer sends the members as part of the channel info --- src/matrix-rocketchat/api/rocketchat/mod.rs | 9 +- src/matrix-rocketchat/api/rocketchat/v1.rs | 97 ++++++++++++++++- src/matrix-rocketchat/errors.rs | 5 + .../handlers/matrix/command_handler.rs | 25 +++-- tests/matrix-rocketchat-test/handlers.rs | 55 ++++++++-- tests/matrix-rocketchat-test/lib.rs | 102 +++++------------- 6 files changed, 194 insertions(+), 99 deletions(-) diff --git a/src/matrix-rocketchat/api/rocketchat/mod.rs b/src/matrix-rocketchat/api/rocketchat/mod.rs index bbc4143..9c7ff69 100644 --- a/src/matrix-rocketchat/api/rocketchat/mod.rs +++ b/src/matrix-rocketchat/api/rocketchat/mod.rs @@ -14,6 +14,7 @@ use i18n::*; /// Rocket.Chat REST API v1 pub mod v1; +const MAX_REQUESTS_PER_ENDPOINT_CALL: i32 = 1000; const MIN_MAJOR_VERSION: i32 = 0; const MIN_MINOR_VERSION: i32 = 60; @@ -51,8 +52,6 @@ pub struct Channel { pub id: String, /// Name of the Rocket.Chat room pub name: Option, - /// List of users in the room - pub usernames: Vec, } /// A Rocket.Chat message @@ -75,7 +74,7 @@ pub struct Message { } /// A Rocket.Chat user -#[derive(Deserialize, Debug, Serialize)] +#[derive(Clone, Deserialize, Debug, Serialize)] pub struct User { /// ID of the Rocket.Chat user #[serde(rename = "_id")] @@ -96,9 +95,11 @@ pub trait RocketchatApi { fn get_attachments(&self, message_id: &str) -> Result>; /// Login a user on the Rocket.Chat server fn login(&self, username: &str, password: &str) -> Result<(String, String)>; + /// Get all members of a channel + fn members(&self, room_id: &str) -> Result>; /// Post a chat message fn post_chat_message(&self, text: &str, room_id: &str) -> Result<()>; - /// Post a message with an attchment + /// Post a message with an attachment fn post_file_message(&self, file: Vec, filename: &str, mime_type: Mime, room_id: &str) -> Result<()>; /// Get information like user_id, status, etc. about a user fn users_info(&self, username: &str) -> Result; diff --git a/src/matrix-rocketchat/api/rocketchat/v1.rs b/src/matrix-rocketchat/api/rocketchat/v1.rs index 23808b2..e83ae1a 100644 --- a/src/matrix-rocketchat/api/rocketchat/v1.rs +++ b/src/matrix-rocketchat/api/rocketchat/v1.rs @@ -27,6 +27,8 @@ pub const DIRECT_MESSAGES_LIST_PATH: &str = "/api/v1/dm.list"; pub const GET_CHAT_MESSAGE_PATH: &str = "/api/v1/chat.getMessage"; /// Post chat message endpoint path pub const POST_CHAT_MESSAGE_PATH: &str = "/api/v1/chat.postMessage"; +/// Room members endpoint path +pub const GET_ROOM_MEMBERS_PATH: &str = "/api/v1/channels.members"; /// Upload a file endpoint path pub const UPLOAD_PATH: &str = "/api/v1/rooms.upload"; @@ -206,7 +208,8 @@ pub struct PostChatMessageEndpoint<'a> { /// Payload of the post chat message endpoint #[derive(Serialize)] pub struct PostChatMessagePayload<'a> { - #[serde(rename = "roomId")] room_id: &'a str, + #[serde(rename = "roomId")] + room_id: &'a str, text: Option<&'a str>, } @@ -278,6 +281,50 @@ impl<'a> Endpoint for PostFileMessageEndpoint<'a> { } } +/// V1 get room members endpoint +pub struct GetRoomMembersEndpoint<'a> { + base_url: String, + user_id: String, + auth_token: String, + query_params: HashMap<&'static str, &'a str>, +} + +/// Response payload from the Rocket.Chat room members endpoint. +#[derive(Deserialize)] +/// +pub struct GetRoomMembersResponse { + members: Vec, + count: i32, + offset: i32, + total: i32, +} + +impl<'a> Endpoint for GetRoomMembersEndpoint<'a> { + fn method(&self) -> Method { + Method::Get + } + + fn url(&self) -> String { + self.base_url.clone() + GET_ROOM_MEMBERS_PATH + } + + fn payload(&self) -> Result> { + Ok(RequestData::Body("".to_string())) + } + + fn headers(&self) -> Option { + let mut headers = Headers::new(); + headers.set(ContentType::json()); + headers.set_raw("X-User-Id", vec![self.user_id.clone().into_bytes()]); + headers.set_raw("X-Auth-Token", vec![self.auth_token.clone().into_bytes()]); + Some(headers) + } + + fn query_params(&self) -> HashMap<&'static str, &str> { + self.query_params.clone() + } +} + /// Response payload from the Rocket.Chat channels.list endpoint. #[derive(Deserialize)] pub struct ChannelsListResponse { @@ -496,7 +543,7 @@ impl super::RocketchatApi for RocketchatApi { files.push(rocketchat_attachment); } } - }else{ + } else { info!(self.logger, "No attachments found for message ID {}", message_id); } @@ -525,6 +572,29 @@ impl super::RocketchatApi for RocketchatApi { Ok((login_response.data.user_id, login_response.data.auth_token)) } + fn members(&self, room_id: &str) -> Result> { + debug!(self.logger, "Getting rooms members for room {} from Rocket.Chat server", room_id); + + let mut users = Vec::new(); + let mut offset = 0; + for i in 0..super::MAX_REQUESTS_PER_ENDPOINT_CALL { + if i == super::MAX_REQUESTS_PER_ENDPOINT_CALL { + bail_error!(ErrorKind::TooManyRequests(GET_ROOM_MEMBERS_PATH.to_string())) + } + + let mut members_response = get_members(&self, room_id, offset)?; + users.append(&mut members_response.members); + let subtotal = members_response.count + members_response.offset; + if subtotal == members_response.total { + break; + } + + offset = subtotal; + } + + Ok(users) + } + fn post_chat_message(&self, text: &str, room_id: &str) -> Result<()> { debug!(self.logger, "Forwarding message to to Rocket.Chat room {}", room_id); @@ -604,6 +674,29 @@ impl super::RocketchatApi for RocketchatApi { } } +fn get_members(rocketchat_api: &RocketchatApi, room_id: &str, offset: i32) -> Result { + let offset_param = offset.to_string(); + let mut query_params = HashMap::new(); + query_params.insert("roomId", room_id); + query_params.insert("offset", &offset_param); + let room_members_endpoint = GetRoomMembersEndpoint { + base_url: rocketchat_api.base_url.clone(), + user_id: rocketchat_api.user_id.clone(), + auth_token: rocketchat_api.auth_token.clone(), + query_params: query_params, + }; + + let (body, status_code) = RestApi::call_rocketchat(&room_members_endpoint)?; + if !status_code.is_success() { + return Err(build_error(&room_members_endpoint.url(), &body, &status_code)); + } + + let room_members_response: GetRoomMembersResponse = serde_json::from_str(&body).chain_err(|| { + ErrorKind::InvalidJSON(format!("Could not deserialize response from Rocket.Chat room members API endpoint: `{}`", body)) + })?; + Ok(room_members_response) +} + fn build_error(endpoint: &str, body: &str, status_code: &StatusCode) -> Error { let json_error_msg = format!( "Could not deserialize error from Rocket.Chat API endpoint {} with status code {}: `{}`", diff --git a/src/matrix-rocketchat/errors.rs b/src/matrix-rocketchat/errors.rs index 277a18d..0e1d7b2 100644 --- a/src/matrix-rocketchat/errors.rs +++ b/src/matrix-rocketchat/errors.rs @@ -373,6 +373,11 @@ error_chain!{ display("The mime type of the file is missing") } + TooManyRequests(endpoint: String) { + description("Too many requests to API endpoint") + display("Too many requests to API endpoint {}", endpoint) + } + InternalServerError { description("An internal error") display("An internal error occurred") diff --git a/src/matrix-rocketchat/handlers/matrix/command_handler.rs b/src/matrix-rocketchat/handlers/matrix/command_handler.rs index a539ab6..934cb8f 100644 --- a/src/matrix-rocketchat/handlers/matrix/command_handler.rs +++ b/src/matrix-rocketchat/handlers/matrix/command_handler.rs @@ -256,7 +256,8 @@ impl<'a> CommandHandler<'a> { }; let username = rocketchat_api.current_username()?; - if !rocketchat_channel.usernames.iter().any(|u| u == &username) { + let users = rocketchat_api.members(&rocketchat_channel.id)?; + if !users.iter().any(|u| u.username == username) { bail_error!( ErrorKind::RocketchatJoinFirst(channel_name.to_string()), t!(["errors", "rocketchat_join_first"]).with_vars(vec![("channel_name", channel_name.to_string())]) @@ -270,13 +271,16 @@ impl<'a> CommandHandler<'a> { room.bridge_for_user(event.user_id.clone(), channel_name.to_string())?; room_id } - None => channel.bridge( - rocketchat_api.as_ref(), - &Some(channel_name.to_string()), - &rocketchat_channel.usernames, - &bot_user_id, - &event.user_id, - )?, + None => { + let usernames: Vec = users.into_iter().map(|u| u.username).collect(); + channel.bridge( + rocketchat_api.as_ref(), + &Some(channel_name.to_string()), + &usernames, + &bot_user_id, + &event.user_id, + )? + } }; let message = t!(["admin_room", "room_successfully_bridged"]).with_vars(vec![ @@ -373,10 +377,11 @@ impl<'a> CommandHandler<'a> { let mut channel_list = "".to_string(); for c in channels { - let channel = Channel::new(self.config, self.logger, self.matrix_api, c.id, rocketchat_server_id); + let channel = Channel::new(self.config, self.logger, self.matrix_api, c.id.clone(), rocketchat_server_id); + let users = rocketchat_api.members(&c.id)?; let formatter = if channel.is_bridged_for_user(user_id)? { "**" - } else if c.usernames.iter().any(|username| username == &display_name) { + } else if users.iter().any(|u| u.username == display_name) { "*" } else { "" diff --git a/tests/matrix-rocketchat-test/handlers.rs b/tests/matrix-rocketchat-test/handlers.rs index 4115142..e7304c4 100644 --- a/tests/matrix-rocketchat-test/handlers.rs +++ b/tests/matrix-rocketchat-test/handlers.rs @@ -11,6 +11,7 @@ use iron::prelude::*; use iron::url::Url; use iron::url::percent_encoding::percent_decode; use iron::{status, BeforeMiddleware, Chain, Handler}; +use matrix_rocketchat::api::rocketchat::User; use matrix_rocketchat::api::rocketchat::v1::Message as RocketchatMessage; use matrix_rocketchat::errors::{MatrixErrorResponse, RocketchatErrorResponse}; use persistent::Write; @@ -102,7 +103,7 @@ impl Handler for RocketchatMe { } pub struct RocketchatChannelsList { - pub channels: HashMap<&'static str, Vec<&'static str>>, + pub channels: Vec<&'static str>, pub status: status::Status, } @@ -112,14 +113,11 @@ impl Handler for RocketchatChannelsList { let mut channels: Vec = Vec::new(); - for (channel_name, user_names) in self.channels.iter() { + for channel_name in self.channels.iter() { let channel = r#"{ "_id": "CHANNEL_NAME_id", "name": "CHANNEL_NAME", "t": "c", - "usernames": [ - "CHANNEL_USERNAMES" - ], "msgs": 0, "u": { "_id": "spec_user_id", @@ -129,8 +127,7 @@ impl Handler for RocketchatChannelsList { "ro": false, "sysMes": true, "_updatedAt": "2017-02-12T13:20:22.092Z" - }"#.replace("CHANNEL_NAME", channel_name) - .replace("CHANNEL_USERNAMES", &user_names.join("\",\"")); + }"#.replace("CHANNEL_NAME", channel_name); channels.push(channel); } @@ -140,6 +137,50 @@ impl Handler for RocketchatChannelsList { } } +pub struct RocketchatRoomMembers { + pub channels: HashMap<&'static str, Vec<&'static str>>, + pub status: status::Status, +} + +impl Handler for RocketchatRoomMembers { + fn handle(&self, request: &mut Request) -> IronResult { + debug!(DEFAULT_LOGGER, "Rocket.Chat mock server got room members request"); + + let url: Url = request.url.clone().into(); + let mut query_pairs = url.query_pairs(); + let (_, room_id_param) = query_pairs.find(|&(ref key, _)| key == "roomId").unwrap().to_owned(); + // convert id to room name, because the list consists of room names and in the tests the room id + // is constructed by appending _id + let room_name = room_id_param.replace("_id", ""); + let room_name_ref: &str = room_name.as_ref(); + + debug!(DEFAULT_LOGGER, "Looking up room {}", room_name_ref); + let payload = match self.channels.get(room_name_ref) { + Some(user_names) => { + let mut users = Vec::new(); + for user_name in user_names { + let user = User { + id: format!("{}_id", user_name), + username: user_name.to_string(), + }; + users.push(user); + } + + let members = serde_json::to_string(&users).unwrap(); + format!( + "{{\"members\": {}, \"count\": {}, \"offset\": 0,\"total\": {}, \"success\": true}}", + members, + members.len(), + members.len() + ) + } + None => "foo".to_string(), + }; + + Ok(Response::with((self.status, payload))) + } +} + pub struct RocketchatDirectMessagesList { pub direct_messages: HashMap<&'static str, Vec<&'static str>>, pub status: status::Status, diff --git a/tests/matrix-rocketchat-test/lib.rs b/tests/matrix-rocketchat-test/lib.rs index 34b1b11..9180233 100644 --- a/tests/matrix-rocketchat-test/lib.rs +++ b/tests/matrix-rocketchat-test/lib.rs @@ -48,7 +48,7 @@ use iron::prelude::*; use iron::typemap::Key; use matrix_rocketchat::{Config, Server}; use matrix_rocketchat::api::MatrixApi; -use matrix_rocketchat::api::rocketchat::v1::{CHANNELS_LIST_PATH, LOGIN_PATH, ME_PATH, USERS_INFO_PATH}; +use matrix_rocketchat::api::rocketchat::v1::{CHANNELS_LIST_PATH, GET_ROOM_MEMBERS_PATH, LOGIN_PATH, ME_PATH, USERS_INFO_PATH}; use matrix_rocketchat::models::ConnectionPool; use persistent::Write; use r2d2::Pool; @@ -384,11 +384,7 @@ impl Test { }, "me", ); - router.get( - USERS_INFO_PATH, - handlers::RocketchatUsersInfo {}, - "users_info", - ); + router.get(USERS_INFO_PATH, handlers::RocketchatUsersInfo {}, "users_info"); } let mut channels = match self.channels.clone() { @@ -406,10 +402,19 @@ impl Test { CHANNELS_LIST_PATH, handlers::RocketchatChannelsList { status: status::Ok, - channels: channels, + channels: channels.keys().map(|k| *k).collect(), }, "channels_list", ); + + router.get( + GET_ROOM_MEMBERS_PATH, + handlers::RocketchatRoomMembers { + status: status::Ok, + channels: channels, + }, + "get_room_members", + ); } thread::spawn(move || { @@ -454,16 +459,9 @@ impl Test { let matrix_api = MatrixApi::new(&self.config, DEFAULT_LOGGER.clone()).unwrap(); let spec_user_id = UserId::try_from("@spec_user:localhost").unwrap(); let rocketchat_user_id = UserId::try_from("@rocketchat:localhost").unwrap(); - matrix_api - .create_room(Some("admin_room".to_string()), None, &spec_user_id) - .unwrap(); + matrix_api.create_room(Some("admin_room".to_string()), None, &spec_user_id).unwrap(); - helpers::invite( - &self.config, - RoomId::try_from("!admin_room_id:localhost").unwrap(), - rocketchat_user_id, - spec_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) { @@ -501,30 +499,18 @@ impl Test { pub fn default_matrix_routes(&self) -> Router { let mut router = Router::new(); - router.get( - SyncEventsEndpoint::router_path(), - handlers::MatrixSync {}, - "sync", - ); + router.get(SyncEventsEndpoint::router_path(), handlers::MatrixSync {}, "sync"); let join_room_handler = handlers::MatrixJoinRoom { as_url: self.config.as_url.clone(), send_inviter: true, }; - router.post( - JoinRoomByIdEndpoint::router_path(), - join_room_handler, - "join_room", - ); + router.post(JoinRoomByIdEndpoint::router_path(), join_room_handler, "join_room"); let leave_room_handler = handlers::MatrixLeaveRoom { as_url: self.config.as_url.clone(), }; - router.post( - LeaveRoomEndpoint::router_path(), - leave_room_handler, - "leave_room", - ); + router.post(LeaveRoomEndpoint::router_path(), leave_room_handler, "leave_room"); router.get( "/_matrix/client/versions", @@ -544,37 +530,17 @@ impl Test { let mut get_state_events = Chain::new(handlers::MatrixState {}); get_state_events.link_before(handlers::PermissionCheck {}); - router.get( - GetStateEventsEndpoint::router_path(), - get_state_events, - "get_state_events", - ); + router.get(GetStateEventsEndpoint::router_path(), get_state_events, "get_state_events"); let mut get_members = Chain::new(handlers::RoomMembers {}); get_members.link_before(handlers::PermissionCheck {}); - router.get( - GetMemberEventsEndpoint::router_path(), - get_members, - "room_members", - ); + router.get(GetMemberEventsEndpoint::router_path(), get_members, "room_members"); - router.post( - RegisterEndpoint::router_path(), - handlers::MatrixRegister {}, - "register", - ); + router.post(RegisterEndpoint::router_path(), handlers::MatrixRegister {}, "register"); - router.get( - GetDisplaynameEndpoint::router_path(), - handlers::MatrixGetDisplayName {}, - "get_displayname", - ); + router.get(GetDisplaynameEndpoint::router_path(), handlers::MatrixGetDisplayName {}, "get_displayname"); - router.put( - SetDisplaynameEndpoint::router_path(), - handlers::MatrixSetDisplayName {}, - "set_displayname", - ); + router.put(SetDisplaynameEndpoint::router_path(), handlers::MatrixSetDisplayName {}, "set_displayname"); router.post( CreateRoomEndpoint::router_path(), @@ -587,33 +553,17 @@ impl Test { let invite_user_handler = handlers::MatrixInviteUser { as_url: self.config.as_url.clone(), }; - router.post( - InviteUserEndpoint::router_path(), - invite_user_handler, - "invite_user", - ); + 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::PermissionCheck {}); - router.put( - SendStateEventForEmptyKeyEndpoint::router_path(), - send_room_state, - "send_room_state", - ); + 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::PermissionCheck {}); - router.get( - GetAliasEndpoint::router_path(), - get_room_alias, - "get_room_alias", - ); + router.get(GetAliasEndpoint::router_path(), get_room_alias, "get_room_alias"); - router.delete( - DeleteAliasEndpoint::router_path(), - handlers::DeleteRoomAlias {}, - "delete_room_alias", - ); + router.delete(DeleteAliasEndpoint::router_path(), handlers::DeleteRoomAlias {}, "delete_room_alias"); router.post("*", handlers::EmptyJson {}, "default_post"); router.put("*", handlers::EmptyJson {}, "default_put");