Skip to content

Commit

Permalink
Store room state for each user individually on the mock matrix server
Browse files Browse the repository at this point in the history
To mimic the room state that is visible on a real homeserver
  • Loading branch information
exul committed Oct 7, 2017
1 parent cc34c81 commit 11574d3
Show file tree
Hide file tree
Showing 25 changed files with 678 additions and 316 deletions.
1 change: 1 addition & 0 deletions assets/translations.yaml
Expand Up @@ -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."
Expand Down
4 changes: 2 additions & 2 deletions src/matrix-rocketchat/api/matrix/mod.rs
Expand Up @@ -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<UserId>) -> Result<Option<RoomId>>;
fn get_room_alias(&self, matrix_room_alias_id: RoomAliasId) -> Result<Option<RoomId>>;
/// Get a rooms canonical alias.
fn get_room_canonical_alias(&self, matrix_room_id: RoomId, sender_id: Option<UserId>) -> Result<Option<RoomAliasId>>;
fn get_room_canonical_alias(&self, matrix_room_id: RoomId) -> Result<Option<RoomAliasId>>;
/// Get the `user_id` of the user that created the room.
fn get_room_creator(&self, matrix_room_id: RoomId) -> Result<UserId>;
/// Get the list of members for this room.
Expand Down
24 changes: 6 additions & 18 deletions src/matrix-rocketchat/api/matrix/r0.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -120,17 +119,12 @@ impl super::MatrixApi for MatrixApi {
Ok(())
}

fn get_room_alias(&self, matrix_room_alias_id: RoomAliasId, sender_id: Option<UserId>) -> Result<Option<RoomId>> {
fn get_room_alias(&self, matrix_room_alias_id: RoomAliasId) -> Result<Option<RoomId>> {
// 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::<String>();
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, "{}", &params)?;
if status_code == StatusCode::NotFound {
Expand All @@ -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<UserId>) -> Result<Option<RoomAliasId>> {
fn get_room_canonical_alias(&self, matrix_room_id: RoomId) -> Result<Option<RoomAliasId>> {
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, "{}", &params)?;
//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);
}

Expand Down
43 changes: 11 additions & 32 deletions src/matrix-rocketchat/db/room.rs
Expand Up @@ -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;

Expand All @@ -25,7 +24,7 @@ impl Room {
) -> Result<bool> {
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)
Expand All @@ -40,8 +39,7 @@ impl Room {
matrix_api: &MatrixApi,
matrix_room_id: RoomId,
) -> Result<Option<RocketchatServer>> {
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)
}
Expand Down Expand Up @@ -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),
}
Expand All @@ -112,30 +110,15 @@ impl Room {
matrix_api: &MatrixApi,
rocketchat_server_id: &str,
rocketchat_channel_id: &str,
sender_id: Option<UserId>,
) -> Result<Option<RoomId>> {
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<bool> {
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<bool> {
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.
Expand All @@ -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<bool> {
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<Option<String>> {
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),
};
Expand Down
10 changes: 8 additions & 2 deletions src/matrix-rocketchat/errors.rs
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion src/matrix-rocketchat/handlers/events/command_handler.rs
Expand Up @@ -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())?;
Expand Down
1 change: 0 additions & 1 deletion src/matrix-rocketchat/handlers/events/forwarder.rs
Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions src/matrix-rocketchat/handlers/events/message_handler.rs
Expand Up @@ -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);
}
Expand Down
81 changes: 51 additions & 30 deletions src/matrix-rocketchat/handlers/events/room_handler.rs
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::convert::TryFrom;

use diesel::sqlite::SqliteConnection;
Expand All @@ -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
Expand Down Expand Up @@ -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<String, Value> = 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);
Expand Down Expand Up @@ -141,54 +154,49 @@ 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<UserId>) -> 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) => {
warn!(
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<UserId> = 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<UserId>) -> 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(());
}

Expand Down Expand Up @@ -247,11 +255,8 @@ impl<'a> RoomHandler<'a> {
}

fn admin_room_language(&self, matrix_room_id: RoomId) -> Result<String> {
let matrix_bot_user_id = self.config.matrix_bot_user_id()?;
let user_ids: Vec<UserId> =
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())
}

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

0 comments on commit 11574d3

Please sign in to comment.