Consider command execution secure when proper conditions are met.
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
- If the room is end-to-end encrypted. - If only the sending user and the bot are present in the room. This lays groundwork for sensitive commands like registering a user account with the bot.
This commit is contained in:
parent
9de74d05a9
commit
34ee2c6e5d
|
@ -30,7 +30,8 @@ async fn main() -> Result<(), BotError> {
|
||||||
.expect("Could not create matrix client"),
|
.expect("Could not create matrix client"),
|
||||||
room: RoomContext {
|
room: RoomContext {
|
||||||
id: &room_id!("!fakeroomid:example.com"),
|
id: &room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "fake room",
|
display_name: "fake room".to_owned(),
|
||||||
|
secure: false,
|
||||||
},
|
},
|
||||||
username: "@localuser:example.com",
|
username: "@localuser:example.com",
|
||||||
message_body: &input,
|
message_body: &input,
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
use crate::commands::{execute_command, ExecutionError, ExecutionResult, ResponseExtractor};
|
||||||
|
use crate::context::{Context, RoomContext};
|
||||||
|
use crate::db::sqlite::Database;
|
||||||
|
use crate::error::BotError;
|
||||||
|
use crate::matrix;
|
||||||
|
use futures::stream::{self, StreamExt};
|
||||||
|
use log::{error, info};
|
||||||
|
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client};
|
||||||
|
use std::clone::Clone;
|
||||||
|
|
||||||
|
/// Handle responding to a single command being executed. Wil print
|
||||||
|
/// out the full result of that command.
|
||||||
|
pub(super) async fn handle_single_result(
|
||||||
|
client: &Client,
|
||||||
|
cmd_result: &ExecutionResult,
|
||||||
|
respond_to: &str,
|
||||||
|
room: &Joined,
|
||||||
|
event_id: EventId,
|
||||||
|
) {
|
||||||
|
if cmd_result.is_err() {
|
||||||
|
error!(
|
||||||
|
"Command execution error: {}",
|
||||||
|
cmd_result.as_ref().err().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = cmd_result.message_html(respond_to);
|
||||||
|
matrix::send_message(client, room.room_id(), &html, Some(event_id)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle responding to multiple commands being executed. Will print
|
||||||
|
/// out how many commands succeeded and failed (if any failed).
|
||||||
|
pub(super) async fn handle_multiple_results(
|
||||||
|
client: &Client,
|
||||||
|
results: &[(String, ExecutionResult)],
|
||||||
|
respond_to: &str,
|
||||||
|
room: &Joined,
|
||||||
|
) {
|
||||||
|
let respond_to = format!(
|
||||||
|
"<a href=\"https://matrix.to/#/{}\">{}</a>",
|
||||||
|
respond_to, respond_to
|
||||||
|
);
|
||||||
|
|
||||||
|
let errors: Vec<(&str, &ExecutionError)> = results
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(cmd, result)| match result {
|
||||||
|
Err(e) => Some((cmd.as_ref(), e)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for result in errors.iter() {
|
||||||
|
error!("Command execution error: '{}' - {}", result.0, result.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = if errors.len() == 0 {
|
||||||
|
format!("{}: Executed {} commands", respond_to, results.len())
|
||||||
|
} else {
|
||||||
|
let failures: Vec<String> = errors
|
||||||
|
.iter()
|
||||||
|
.map(|&(cmd, err)| format!("<strong>{}:</strong> {}", cmd, err))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{}: Executed {} commands ({} failed)\n\nFailures:\n{}",
|
||||||
|
respond_to,
|
||||||
|
results.len(),
|
||||||
|
errors.len(),
|
||||||
|
failures.join("\n")
|
||||||
|
)
|
||||||
|
.replace("\n", "<br/>")
|
||||||
|
};
|
||||||
|
|
||||||
|
matrix::send_message(client, room.room_id(), &message, None).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a context for command execution. Can fai if the room
|
||||||
|
/// context creation fails.
|
||||||
|
async fn create_context<'a>(
|
||||||
|
db: &'a Database,
|
||||||
|
client: &'a Client,
|
||||||
|
room: &'a Joined,
|
||||||
|
sender: &'a str,
|
||||||
|
command: &'a str,
|
||||||
|
) -> Result<Context<'a>, BotError> {
|
||||||
|
let room_ctx = RoomContext::new(room, sender).await?;
|
||||||
|
Ok(Context {
|
||||||
|
db: db.clone(),
|
||||||
|
matrix_client: client,
|
||||||
|
room: room_ctx,
|
||||||
|
username: &sender,
|
||||||
|
message_body: &command,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to execute all commands sent to the bot in a message. This
|
||||||
|
/// asynchronously executes all commands given to it. A Vec of all
|
||||||
|
/// commands and their execution results are returned.
|
||||||
|
pub(super) async fn execute(
|
||||||
|
commands: Vec<&str>,
|
||||||
|
db: &Database,
|
||||||
|
client: &Client,
|
||||||
|
room: &Joined,
|
||||||
|
sender: &str,
|
||||||
|
) -> Vec<(String, ExecutionResult)> {
|
||||||
|
stream::iter(commands)
|
||||||
|
.then(|command| async move {
|
||||||
|
match create_context(db, client, room, sender, command).await {
|
||||||
|
Err(e) => (command.to_owned(), Err(ExecutionError(e))),
|
||||||
|
Ok(ctx) => {
|
||||||
|
let cmd_result = execute_command(&ctx).await;
|
||||||
|
info!(
|
||||||
|
"[{}] {} executed: {}",
|
||||||
|
ctx.room.display_name, sender, command
|
||||||
|
);
|
||||||
|
(command.to_owned(), cmd_result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
}
|
105
src/bot/mod.rs
105
src/bot/mod.rs
|
@ -1,20 +1,18 @@
|
||||||
use crate::commands::{execute_command, ExecutionError, ExecutionResult, ResponseExtractor};
|
use crate::commands::{ExecutionError, ExecutionResult};
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::context::{Context, RoomContext};
|
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::db::DbState;
|
use crate::db::DbState;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use crate::matrix;
|
|
||||||
use crate::state::DiceBotState;
|
use crate::state::DiceBotState;
|
||||||
use dirs;
|
use dirs;
|
||||||
use futures::stream::{self, StreamExt};
|
use log::info;
|
||||||
use log::{error, info};
|
|
||||||
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, ClientConfig, SyncSettings};
|
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, ClientConfig, SyncSettings};
|
||||||
use std::clone::Clone;
|
use std::clone::Clone;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
mod command_execution;
|
||||||
pub mod event_handlers;
|
pub mod event_handlers;
|
||||||
|
|
||||||
/// How many commands can be in one message. If the amount is higher
|
/// How many commands can be in one message. If the amount is higher
|
||||||
|
@ -53,72 +51,6 @@ fn create_client(config: &Config) -> Result<Client, BotError> {
|
||||||
Ok(Client::new_with_config(homeserver_url, client_config)?)
|
Ok(Client::new_with_config(homeserver_url, client_config)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle responding to a single command being executed. Wil print
|
|
||||||
/// out the full result of that command.
|
|
||||||
async fn handle_single_result(
|
|
||||||
client: &Client,
|
|
||||||
cmd_result: &ExecutionResult,
|
|
||||||
respond_to: &str,
|
|
||||||
room: &Joined,
|
|
||||||
event_id: EventId,
|
|
||||||
) {
|
|
||||||
if cmd_result.is_err() {
|
|
||||||
error!(
|
|
||||||
"Command execution error: {}",
|
|
||||||
cmd_result.as_ref().err().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = cmd_result.message_html(respond_to);
|
|
||||||
matrix::send_message(client, room.room_id(), &html, Some(event_id)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle responding to multiple commands being executed. Will print
|
|
||||||
/// out how many commands succeeded and failed (if any failed).
|
|
||||||
async fn handle_multiple_results(
|
|
||||||
client: &Client,
|
|
||||||
results: &[(String, ExecutionResult)],
|
|
||||||
respond_to: &str,
|
|
||||||
room: &Joined,
|
|
||||||
) {
|
|
||||||
let respond_to = format!(
|
|
||||||
"<a href=\"https://matrix.to/#/{}\">{}</a>",
|
|
||||||
respond_to, respond_to
|
|
||||||
);
|
|
||||||
|
|
||||||
let errors: Vec<(&str, &ExecutionError)> = results
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(cmd, result)| match result {
|
|
||||||
Err(e) => Some((cmd.as_ref(), e)),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for result in errors.iter() {
|
|
||||||
error!("Command execution error: '{}' - {}", result.0, result.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = if errors.len() == 0 {
|
|
||||||
format!("{}: Executed {} commands", respond_to, results.len())
|
|
||||||
} else {
|
|
||||||
let failures: Vec<String> = errors
|
|
||||||
.iter()
|
|
||||||
.map(|&(cmd, err)| format!("<strong>{}:</strong> {}", cmd, err))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
format!(
|
|
||||||
"{}: Executed {} commands ({} failed)\n\nFailures:\n{}",
|
|
||||||
respond_to,
|
|
||||||
results.len(),
|
|
||||||
errors.len(),
|
|
||||||
failures.join("\n")
|
|
||||||
)
|
|
||||||
.replace("\n", "<br/>")
|
|
||||||
};
|
|
||||||
|
|
||||||
matrix::send_message(client, room.room_id(), &message, None).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiceBot {
|
impl DiceBot {
|
||||||
/// Create a new dicebot with the given configuration and state
|
/// Create a new dicebot with the given configuration and state
|
||||||
/// actor. This function returns a Result because it is possible
|
/// actor. This function returns a Result because it is possible
|
||||||
|
@ -184,11 +116,9 @@ impl DiceBot {
|
||||||
async fn execute_commands(
|
async fn execute_commands(
|
||||||
&self,
|
&self,
|
||||||
room: &Joined,
|
room: &Joined,
|
||||||
sender_username: &str,
|
sender: &str,
|
||||||
msg_body: &str,
|
msg_body: &str,
|
||||||
) -> Vec<(String, ExecutionResult)> {
|
) -> Vec<(String, ExecutionResult)> {
|
||||||
let room_name: &str = &room.display_name().await.ok().unwrap_or_default();
|
|
||||||
|
|
||||||
let commands: Vec<&str> = msg_body
|
let commands: Vec<&str> = msg_body
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|line| line.starts_with("!"))
|
.filter(|line| line.starts_with("!"))
|
||||||
|
@ -197,22 +127,7 @@ impl DiceBot {
|
||||||
|
|
||||||
//Up to 50 commands allowed, otherwise we send back an error.
|
//Up to 50 commands allowed, otherwise we send back an error.
|
||||||
let results: Vec<(String, ExecutionResult)> = if commands.len() < MAX_COMMANDS_PER_MESSAGE {
|
let results: Vec<(String, ExecutionResult)> = if commands.len() < MAX_COMMANDS_PER_MESSAGE {
|
||||||
stream::iter(commands)
|
command_execution::execute(commands, &self.db, &self.client, room, sender).await
|
||||||
.then(|command| async move {
|
|
||||||
let ctx = Context {
|
|
||||||
db: self.db.clone(),
|
|
||||||
matrix_client: &self.client,
|
|
||||||
room: RoomContext::new_with_name(&room, room_name),
|
|
||||||
username: &sender_username,
|
|
||||||
message_body: &command,
|
|
||||||
};
|
|
||||||
|
|
||||||
let cmd_result = execute_command(&ctx).await;
|
|
||||||
info!("[{}] {} executed: {}", room_name, sender_username, command);
|
|
||||||
(command.to_owned(), cmd_result)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
.await
|
|
||||||
} else {
|
} else {
|
||||||
vec![(
|
vec![(
|
||||||
"".to_owned(),
|
"".to_owned(),
|
||||||
|
@ -232,7 +147,7 @@ impl DiceBot {
|
||||||
) {
|
) {
|
||||||
if results.len() >= 1 {
|
if results.len() >= 1 {
|
||||||
if results.len() == 1 {
|
if results.len() == 1 {
|
||||||
handle_single_result(
|
command_execution::handle_single_result(
|
||||||
&self.client,
|
&self.client,
|
||||||
&results[0].1,
|
&results[0].1,
|
||||||
sender_username,
|
sender_username,
|
||||||
|
@ -241,7 +156,13 @@ impl DiceBot {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
} else if results.len() > 1 {
|
} else if results.len() > 1 {
|
||||||
handle_multiple_results(&self.client, &results, sender_username, &room).await;
|
command_execution::handle_multiple_results(
|
||||||
|
&self.client,
|
||||||
|
&results,
|
||||||
|
sender_username,
|
||||||
|
&room,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,7 +333,8 @@ mod tests {
|
||||||
() => {
|
() => {
|
||||||
crate::context::RoomContext {
|
crate::context::RoomContext {
|
||||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "displayname",
|
display_name: "displayname".to_owned(),
|
||||||
|
secure: false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,8 @@ mod tests {
|
||||||
() => {
|
() => {
|
||||||
crate::context::RoomContext {
|
crate::context::RoomContext {
|
||||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "displayname",
|
display_name: "displayname".to_owned(),
|
||||||
|
secure: false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use matrix_sdk::identifiers::RoomId;
|
use crate::error::BotError;
|
||||||
|
use matrix_sdk::identifiers::{RoomId, UserId};
|
||||||
use matrix_sdk::room::Joined;
|
use matrix_sdk::room::Joined;
|
||||||
use matrix_sdk::Client;
|
use matrix_sdk::Client;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
/// A context carried through the system providing access to things
|
/// A context carried through the system providing access to things
|
||||||
/// like the database.
|
/// like the database.
|
||||||
|
@ -18,19 +20,52 @@ impl Context<'_> {
|
||||||
pub fn room_id(&self) -> &RoomId {
|
pub fn room_id(&self) -> &RoomId {
|
||||||
self.room.id
|
self.room.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_secure(&self) -> bool {
|
||||||
|
self.room.secure
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RoomContext<'a> {
|
pub struct RoomContext<'a> {
|
||||||
pub id: &'a RoomId,
|
pub id: &'a RoomId,
|
||||||
pub display_name: &'a str,
|
pub display_name: String,
|
||||||
|
pub secure: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoomContext<'_> {
|
impl RoomContext<'_> {
|
||||||
pub fn new_with_name<'a>(room: &'a Joined, display_name: &'a str) -> RoomContext<'a> {
|
pub async fn new_with_name<'a>(
|
||||||
RoomContext {
|
room: &'a Joined,
|
||||||
|
display_name: String,
|
||||||
|
sending_user: &str,
|
||||||
|
) -> Result<RoomContext<'a>, BotError> {
|
||||||
|
// TODO is_direct is a hack; should set rooms to Direct
|
||||||
|
// Message upon joining, if other contact has requested it.
|
||||||
|
// Waiting on SDK support.
|
||||||
|
let sending_user = UserId::try_from(sending_user)?;
|
||||||
|
let user_in_room = room.get_member(&sending_user).await.ok().is_some();
|
||||||
|
let is_direct = room.joined_members().await?.len() == 2;
|
||||||
|
|
||||||
|
Ok(RoomContext {
|
||||||
id: room.room_id(),
|
id: room.room_id(),
|
||||||
display_name,
|
display_name,
|
||||||
}
|
secure: room.is_encrypted() && is_direct && user_in_room,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new<'a>(
|
||||||
|
room: &'a Joined,
|
||||||
|
sending_user: &str,
|
||||||
|
) -> Result<RoomContext<'a>, BotError> {
|
||||||
|
Self::new_with_name(
|
||||||
|
&room,
|
||||||
|
room.display_name()
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string(),
|
||||||
|
sending_user,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -427,7 +427,8 @@ mod tests {
|
||||||
() => {
|
() => {
|
||||||
crate::context::RoomContext {
|
crate::context::RoomContext {
|
||||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "displayname",
|
display_name: "displayname".to_owned(),
|
||||||
|
secure: false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,9 @@ pub enum BotError {
|
||||||
|
|
||||||
#[error("could not convert to proper integer type")]
|
#[error("could not convert to proper integer type")]
|
||||||
TryFromIntError(#[from] std::num::TryFromIntError),
|
TryFromIntError(#[from] std::num::TryFromIntError),
|
||||||
|
|
||||||
|
#[error("identifier error: {0}")]
|
||||||
|
IdentifierError(#[from] matrix_sdk::identifiers::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
Loading…
Reference in New Issue