Skip to content

Commit

Permalink
Use the new channels.members endpoint
Browse files Browse the repository at this point in the history
Because Rocket.Chat no longer sends the members as part of the channel info
  • Loading branch information
exul committed Jan 28, 2018
1 parent e6fe755 commit eacdd56
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 99 deletions.
9 changes: 5 additions & 4 deletions src/matrix-rocketchat/api/rocketchat/mod.rs
Expand Up @@ -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;

Expand Down Expand Up @@ -51,8 +52,6 @@ pub struct Channel {
pub id: String,
/// Name of the Rocket.Chat room
pub name: Option<String>,
/// List of users in the room
pub usernames: Vec<String>,
}

/// A Rocket.Chat message
Expand All @@ -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")]
Expand All @@ -96,9 +95,11 @@ pub trait RocketchatApi {
fn get_attachments(&self, message_id: &str) -> Result<Vec<Attachment>>;
/// 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<Vec<User>>;
/// 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<u8>, 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<User>;
Expand Down
97 changes: 95 additions & 2 deletions src/matrix-rocketchat/api/rocketchat/v1.rs
Expand Up @@ -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";

Expand Down Expand Up @@ -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>,
}

Expand Down Expand Up @@ -278,6 +281,50 @@ impl<'a> Endpoint<String> 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<User>,
count: i32,
offset: i32,
total: i32,
}

impl<'a> Endpoint<String> 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<RequestData<String>> {
Ok(RequestData::Body("".to_string()))
}

fn headers(&self) -> Option<Headers> {
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 {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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<Vec<User>> {
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);

Expand Down Expand Up @@ -604,6 +674,29 @@ impl super::RocketchatApi for RocketchatApi {
}
}

fn get_members(rocketchat_api: &RocketchatApi, room_id: &str, offset: i32) -> Result<GetRoomMembersResponse> {
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 {}: `{}`",
Expand Down
5 changes: 5 additions & 0 deletions src/matrix-rocketchat/errors.rs
Expand Up @@ -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")
Expand Down
25 changes: 15 additions & 10 deletions src/matrix-rocketchat/handlers/matrix/command_handler.rs
Expand Up @@ -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())])
Expand All @@ -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<String> = 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![
Expand Down Expand Up @@ -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 {
""
Expand Down
55 changes: 48 additions & 7 deletions tests/matrix-rocketchat-test/handlers.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}

Expand All @@ -112,14 +113,11 @@ impl Handler for RocketchatChannelsList {

let mut channels: Vec<String> = 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",
Expand All @@ -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);
}

Expand All @@ -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<Response> {
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,
Expand Down

0 comments on commit eacdd56

Please sign in to comment.