Compare commits
No commits in common. "76214bc79060e72b2e7a594f95fb02871d36f3c1" and "a84d4fd869787467d2284f23343244566bee1220" have entirely different histories.
76214bc790
...
a84d4fd869
|
@ -101,12 +101,6 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayref"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -204,17 +198,6 @@ dependencies = [
|
||||||
"wyz",
|
"wyz",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "blake2b_simd"
|
|
||||||
version = "0.5.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"arrayvec",
|
|
||||||
"constant_time_eq",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -330,12 +313,6 @@ version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
|
checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "constant_time_eq"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
@ -2048,18 +2025,6 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-argon2"
|
|
||||||
version = "0.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"blake2b_simd",
|
|
||||||
"constant_time_eq",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
@ -2467,15 +2432,6 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "substring"
|
|
||||||
version = "1.4.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
|
@ -2554,10 +2510,8 @@ dependencies = [
|
||||||
"phf",
|
"phf",
|
||||||
"rand 0.8.3",
|
"rand 0.8.3",
|
||||||
"refinery",
|
"refinery",
|
||||||
"rust-argon2",
|
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"substring",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
@ -16,7 +16,6 @@ tracing-subscriber = "0.2"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
nom = "5"
|
nom = "5"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rust-argon2 = "0.8"
|
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
@ -31,7 +30,6 @@ matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "
|
||||||
refinery = { version = "0.5", features = ["rusqlite"]}
|
refinery = { version = "0.5", features = ["rusqlite"]}
|
||||||
barrel = { version = "0.6", features = ["sqlite3"] }
|
barrel = { version = "0.6", features = ["sqlite3"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
substring = "1.4"
|
|
||||||
|
|
||||||
[dependencies.sqlx]
|
[dependencies.sqlx]
|
||||||
version = "0.5"
|
version = "0.5"
|
||||||
|
|
|
@ -60,30 +60,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"64e137107139c56a43f7041db933671c210df4fa5110fe481d191fd63b2d3aeb": {
|
|
||||||
"query": "SELECT user_id, password FROM accounts\n WHERE user_id = ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "user_id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "password",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"711d222911c1258365a6a0de1fe00eeec4686fd3589e976e225ad599e7cfc75d": {
|
"711d222911c1258365a6a0de1fe00eeec4686fd3589e976e225ad599e7cfc75d": {
|
||||||
"query": "SELECT count(*) as \"count: i32\" FROM user_variables\n WHERE room_id = ? and user_id = ?",
|
"query": "SELECT count(*) as \"count: i32\" FROM user_variables\n WHERE room_id = ? and user_id = ?",
|
||||||
"describe": {
|
"describe": {
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::db::sqlite::Database;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use crate::matrix;
|
use crate::matrix;
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::stream::{self, StreamExt};
|
||||||
|
use log::{error, info};
|
||||||
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client};
|
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client};
|
||||||
use std::clone::Clone;
|
use std::clone::Clone;
|
||||||
|
|
||||||
|
@ -16,6 +17,13 @@ pub(super) async fn handle_single_result(
|
||||||
room: &Joined,
|
room: &Joined,
|
||||||
event_id: EventId,
|
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);
|
let html = cmd_result.message_html(respond_to);
|
||||||
matrix::send_message(client, room.room_id(), &html, Some(event_id)).await;
|
matrix::send_message(client, room.room_id(), &html, Some(event_id)).await;
|
||||||
}
|
}
|
||||||
|
@ -41,6 +49,10 @@ pub(super) async fn handle_multiple_results(
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
for result in errors.iter() {
|
||||||
|
error!("Command execution error: '{}' - {}", result.0, result.1);
|
||||||
|
}
|
||||||
|
|
||||||
let message = if errors.len() == 0 {
|
let message = if errors.len() == 0 {
|
||||||
format!("{}: Executed {} commands", respond_to, results.len())
|
format!("{}: Executed {} commands", respond_to, results.len())
|
||||||
} else {
|
} else {
|
||||||
|
@ -97,6 +109,10 @@ pub(super) async fn execute(
|
||||||
Err(e) => (command.to_owned(), Err(ExecutionError(e))),
|
Err(e) => (command.to_owned(), Err(ExecutionError(e))),
|
||||||
Ok(ctx) => {
|
Ok(ctx) => {
|
||||||
let cmd_result = execute_command(&ctx).await;
|
let cmd_result = execute_command(&ctx).await;
|
||||||
|
info!(
|
||||||
|
"[{}] {} executed: {}",
|
||||||
|
ctx.room.display_name, sender, command
|
||||||
|
);
|
||||||
(command.to_owned(), cmd_result)
|
(command.to_owned(), cmd_result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use super::{Command, Execution, ExecutionResult};
|
use super::{Command, Execution, ExecutionResult};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::db::Users;
|
use crate::logic::record_room_information;
|
||||||
use crate::error::BotError::{AccountDoesNotExist, AuthenticationError, PasswordCreationError};
|
|
||||||
use crate::logic::{hash_password, record_room_information};
|
|
||||||
use crate::models::User;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use matrix_sdk::identifiers::UserId;
|
use matrix_sdk::identifiers::UserId;
|
||||||
|
|
||||||
|
@ -36,74 +33,3 @@ impl Command for ResyncCommand {
|
||||||
Execution::success(message)
|
Execution::success(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RegisterCommand(pub String);
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Command for RegisterCommand {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"register user account"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_secure(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
|
||||||
let pw_hash = hash_password(&self.0).map_err(|e| PasswordCreationError(e))?;
|
|
||||||
let user = User {
|
|
||||||
username: ctx.username.to_owned(),
|
|
||||||
password: pw_hash,
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.db.upsert_user(&user).await?;
|
|
||||||
Execution::success("User account registered/updated".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CheckCommand(pub String);
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Command for CheckCommand {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"check user password"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_secure(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
|
||||||
let user = ctx.db.authenticate_user(&ctx.username, &self.0).await?;
|
|
||||||
|
|
||||||
match user {
|
|
||||||
Some(_) => Execution::success("Password is correct!".to_string()),
|
|
||||||
None => Err(AuthenticationError.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UnregisterCommand;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Command for UnregisterCommand {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"unregister user account"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_secure(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
|
||||||
let user = ctx.db.get_user(&ctx.username).await?;
|
|
||||||
|
|
||||||
match user {
|
|
||||||
Some(_) => {
|
|
||||||
ctx.db.delete_user(&ctx.username).await?;
|
|
||||||
Execution::success("Your user account has been removed.".to_string())
|
|
||||||
}
|
|
||||||
None => Err(AccountDoesNotExist.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::{error, info};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use BotError::DataError;
|
use BotError::DataError;
|
||||||
|
|
||||||
|
@ -20,9 +19,6 @@ pub enum CommandError {
|
||||||
#[error("invalid command: {0}")]
|
#[error("invalid command: {0}")]
|
||||||
InvalidCommand(String),
|
InvalidCommand(String),
|
||||||
|
|
||||||
#[error("command can only be executed from encrypted direct message")]
|
|
||||||
InsecureExecution,
|
|
||||||
|
|
||||||
#[error("ignored command")]
|
#[error("ignored command")]
|
||||||
IgnoredCommand,
|
IgnoredCommand,
|
||||||
}
|
}
|
||||||
|
@ -103,72 +99,18 @@ pub trait Command: Send + Sync {
|
||||||
fn is_secure(&self) -> bool;
|
fn is_secure(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine if we are allowed to execute this command. Currently the
|
|
||||||
/// rules are that secure commands must be executed in secure rooms
|
|
||||||
/// (encrypted + direct), and anything else can be executed where
|
|
||||||
/// ever. Later, we can add stuff like admin/regular user power
|
|
||||||
/// separation, etc.
|
|
||||||
fn execution_allowed(cmd: &(impl Command + ?Sized), ctx: &Context<'_>) -> Result<(), CommandError> {
|
|
||||||
if cmd.is_secure() {
|
|
||||||
if ctx.is_secure() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(CommandError::InsecureExecution)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to execute a command, and return the content that should
|
/// Attempt to execute a command, and return the content that should
|
||||||
/// go back to Matrix, if the command was executed (successfully or
|
/// go back to Matrix, if the command was executed (successfully or
|
||||||
/// not). If a command is determined to be ignored, this function will
|
/// not). If a command is determined to be ignored, this function will
|
||||||
/// return None, signifying that we should not send a response.
|
/// return None, signifying that we should not send a response.
|
||||||
pub async fn execute_command(ctx: &Context<'_>) -> ExecutionResult {
|
pub async fn execute_command(ctx: &Context<'_>) -> ExecutionResult {
|
||||||
let cmd = parser::parse_command(&ctx.message_body)?;
|
let cmd = parser::parse_command(&ctx.message_body)?;
|
||||||
|
cmd.execute(ctx).await
|
||||||
let result = match execution_allowed(cmd.as_ref(), ctx) {
|
|
||||||
Ok(_) => cmd.execute(ctx).await,
|
|
||||||
Err(e) => Err(ExecutionError(e.into())),
|
|
||||||
};
|
|
||||||
|
|
||||||
log_command(cmd.as_ref(), ctx, &result);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log result of an executed command.
|
|
||||||
fn log_command(cmd: &(impl Command + ?Sized), ctx: &Context, result: &ExecutionResult) {
|
|
||||||
use substring::Substring;
|
|
||||||
let command = match cmd.is_secure() {
|
|
||||||
true => cmd.name(),
|
|
||||||
false => ctx.message_body.substring(0, 30),
|
|
||||||
};
|
|
||||||
|
|
||||||
let dots = match ctx.message_body.len() {
|
|
||||||
_len if _len > 30 => "[...]",
|
|
||||||
_ => "",
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => {
|
|
||||||
info!(
|
|
||||||
"[{}] {} <{}{}> - success",
|
|
||||||
ctx.room.display_name, ctx.username, command, dots
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"[{}] {} <{}{}> - {}",
|
|
||||||
ctx.room.display_name, ctx.username, command, dots, e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use management::RegisterCommand;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
macro_rules! dummy_room {
|
macro_rules! dummy_room {
|
||||||
|
@ -181,100 +123,6 @@ mod tests {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! secure_room {
|
|
||||||
() => {
|
|
||||||
crate::context::RoomContext {
|
|
||||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
|
||||||
display_name: "displayname".to_owned(),
|
|
||||||
secure: true,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn secure_context_secure_command_allows_execution() {
|
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
|
||||||
let db = crate::db::sqlite::Database::new(db_path.path().to_str().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
|
||||||
|
|
||||||
let ctx = Context {
|
|
||||||
db: db,
|
|
||||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
|
||||||
room: secure_room!(),
|
|
||||||
username: "myusername",
|
|
||||||
message_body: "!notacommand",
|
|
||||||
};
|
|
||||||
|
|
||||||
let cmd = RegisterCommand("".to_owned());
|
|
||||||
assert_eq!(execution_allowed(&cmd, &ctx).is_ok(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn secure_context_insecure_command_allows_execution() {
|
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
|
||||||
let db = crate::db::sqlite::Database::new(db_path.path().to_str().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
|
||||||
|
|
||||||
let ctx = Context {
|
|
||||||
db: db,
|
|
||||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
|
||||||
room: secure_room!(),
|
|
||||||
username: "myusername",
|
|
||||||
message_body: "!notacommand",
|
|
||||||
};
|
|
||||||
|
|
||||||
let cmd = variables::GetVariableCommand("".to_owned());
|
|
||||||
assert_eq!(execution_allowed(&cmd, &ctx).is_ok(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn insecure_context_insecure_command_allows_execution() {
|
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
|
||||||
let db = crate::db::sqlite::Database::new(db_path.path().to_str().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
|
||||||
|
|
||||||
let ctx = Context {
|
|
||||||
db: db,
|
|
||||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
|
||||||
room: dummy_room!(),
|
|
||||||
username: "myusername",
|
|
||||||
message_body: "!notacommand",
|
|
||||||
};
|
|
||||||
|
|
||||||
let cmd = variables::GetVariableCommand("".to_owned());
|
|
||||||
assert_eq!(execution_allowed(&cmd, &ctx).is_ok(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn insecure_context_secure_command_denies_execution() {
|
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
|
||||||
let db = crate::db::sqlite::Database::new(db_path.path().to_str().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
|
||||||
|
|
||||||
let ctx = Context {
|
|
||||||
db: db,
|
|
||||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
|
||||||
room: dummy_room!(),
|
|
||||||
username: "myusername",
|
|
||||||
message_body: "!notacommand",
|
|
||||||
};
|
|
||||||
|
|
||||||
let cmd = RegisterCommand("".to_owned());
|
|
||||||
assert_eq!(execution_allowed(&cmd, &ctx).is_err(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_result_extractor_creates_bubble() {
|
fn command_result_extractor_creates_bubble() {
|
||||||
let result = Execution::success("test".to_string());
|
let result = Execution::success("test".to_string());
|
||||||
|
@ -300,7 +148,6 @@ mod tests {
|
||||||
username: "myusername",
|
username: "myusername",
|
||||||
message_body: "!notacommand",
|
message_body: "!notacommand",
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = execute_command(&ctx).await;
|
let result = execute_command(&ctx).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::commands::{
|
||||||
basic_rolling::RollCommand,
|
basic_rolling::RollCommand,
|
||||||
cofd::PoolRollCommand,
|
cofd::PoolRollCommand,
|
||||||
cthulhu::{CthAdvanceRoll, CthRoll},
|
cthulhu::{CthAdvanceRoll, CthRoll},
|
||||||
management::{CheckCommand, RegisterCommand, ResyncCommand, UnregisterCommand},
|
management::ResyncCommand,
|
||||||
misc::HelpCommand,
|
misc::HelpCommand,
|
||||||
variables::{
|
variables::{
|
||||||
DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand,
|
DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand,
|
||||||
|
@ -47,18 +47,6 @@ fn parse_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_register_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|
||||||
Ok(Box::new(RegisterCommand(input.to_owned())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_check_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|
||||||
Ok(Box::new(CheckCommand(input.to_owned())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_unregister_command() -> Result<Box<dyn Command>, BotError> {
|
|
||||||
Ok(Box::new(UnregisterCommand))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_get_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
fn parse_get_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||||
Ok(Box::new(GetVariableCommand(input.to_owned())))
|
Ok(Box::new(GetVariableCommand(input.to_owned())))
|
||||||
}
|
}
|
||||||
|
@ -153,9 +141,6 @@ pub fn parse_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||||
"cthadv" | "ctharoll" => parse_cth_advancement_roll(&cmd_input),
|
"cthadv" | "ctharoll" => parse_cth_advancement_roll(&cmd_input),
|
||||||
"chance" => chance_die(),
|
"chance" => chance_die(),
|
||||||
"help" => help(&cmd_input),
|
"help" => help(&cmd_input),
|
||||||
"register" => parse_register_command(&cmd_input),
|
|
||||||
"check" => parse_check_command(&cmd_input),
|
|
||||||
"unregister" => parse_unregister_command(),
|
|
||||||
_ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()),
|
_ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()),
|
||||||
},
|
},
|
||||||
//All other errors passed up.
|
//All other errors passed up.
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use crate::error::BotError;
|
|
||||||
use crate::models::User;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use errors::DataError;
|
use errors::DataError;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
@ -16,21 +14,6 @@ pub(crate) trait DbState {
|
||||||
async fn set_device_id(&self, device_id: &str) -> Result<(), DataError>;
|
async fn set_device_id(&self, device_id: &str) -> Result<(), DataError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub(crate) trait Users {
|
|
||||||
async fn upsert_user(&self, user: &User) -> Result<(), DataError>;
|
|
||||||
|
|
||||||
async fn get_user(&self, username: &str) -> Result<Option<User>, DataError>;
|
|
||||||
|
|
||||||
async fn delete_user(&self, username: &str) -> Result<(), DataError>;
|
|
||||||
|
|
||||||
async fn authenticate_user(
|
|
||||||
&self,
|
|
||||||
username: &str,
|
|
||||||
raw_password: &str,
|
|
||||||
) -> Result<Option<User>, BotError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub(crate) trait Rooms {
|
pub(crate) trait Rooms {
|
||||||
async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError>;
|
async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError>;
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
use barrel::backend::Sqlite;
|
|
||||||
use barrel::{types, types::Type, Migration};
|
|
||||||
|
|
||||||
fn primary_uuid() -> Type {
|
|
||||||
types::text().unique(true).primary(true).nullable(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn migration() -> String {
|
|
||||||
let mut m = Migration::new();
|
|
||||||
|
|
||||||
//Table of room ID, event ID, event timestamp
|
|
||||||
m.create_table("accounts", move |t| {
|
|
||||||
t.add_column("user_id", primary_uuid());
|
|
||||||
t.add_column("password", types::text().nullable(false));
|
|
||||||
});
|
|
||||||
|
|
||||||
m.make::<Sqlite>()
|
|
||||||
}
|
|
|
@ -7,7 +7,6 @@ use std::str::FromStr;
|
||||||
pub mod migrator;
|
pub mod migrator;
|
||||||
pub mod rooms;
|
pub mod rooms;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod users;
|
|
||||||
pub mod variables;
|
pub mod variables;
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
|
|
|
@ -1,210 +0,0 @@
|
||||||
use super::Database;
|
|
||||||
use crate::db::{errors::DataError, Users};
|
|
||||||
use crate::error::BotError;
|
|
||||||
use crate::models::User;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Users for Database {
|
|
||||||
async fn upsert_user(&self, user: &User) -> Result<(), DataError> {
|
|
||||||
sqlx::query(
|
|
||||||
r#"INSERT INTO accounts (user_id, password) VALUES (?, ?)
|
|
||||||
ON CONFLICT(user_id) DO UPDATE SET password = ?"#,
|
|
||||||
)
|
|
||||||
.bind(&user.username)
|
|
||||||
.bind(&user.password)
|
|
||||||
.bind(&user.password)
|
|
||||||
.execute(&self.conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_user(&self, username: &str) -> Result<(), DataError> {
|
|
||||||
sqlx::query(r#"DELETE FROM accounts WHERE user_id = ?"#)
|
|
||||||
.bind(&username)
|
|
||||||
.execute(&self.conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_user(&self, username: &str) -> Result<Option<User>, DataError> {
|
|
||||||
let user_row = sqlx::query!(
|
|
||||||
r#"SELECT user_id, password FROM accounts
|
|
||||||
WHERE user_id = ?"#,
|
|
||||||
username
|
|
||||||
)
|
|
||||||
.fetch_optional(&self.conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(user_row.map(|u| User {
|
|
||||||
username: u.user_id,
|
|
||||||
password: u.password,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn authenticate_user(
|
|
||||||
&self,
|
|
||||||
username: &str,
|
|
||||||
raw_password: &str,
|
|
||||||
) -> Result<Option<User>, BotError> {
|
|
||||||
let user = self.get_user(username).await?;
|
|
||||||
Ok(user.filter(|u| u.verify_password(raw_password)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::db::sqlite::Database;
|
|
||||||
use crate::db::Users;
|
|
||||||
|
|
||||||
async fn create_db() -> Database {
|
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
|
||||||
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Database::new(db_path.path().to_str().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn create_and_get_user_test() {
|
|
||||||
let db = create_db().await;
|
|
||||||
|
|
||||||
let insert_result = db
|
|
||||||
.upsert_user(&User {
|
|
||||||
username: "myuser".to_string(),
|
|
||||||
password: "abc".to_string(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(insert_result.is_ok());
|
|
||||||
|
|
||||||
let user = db
|
|
||||||
.get_user("myuser")
|
|
||||||
.await
|
|
||||||
.expect("User retrieval query failed");
|
|
||||||
|
|
||||||
assert!(user.is_some());
|
|
||||||
let user = user.unwrap();
|
|
||||||
assert_eq!(user.username, "myuser");
|
|
||||||
assert_eq!(user.password, "abc");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn can_update_user() {
|
|
||||||
let db = create_db().await;
|
|
||||||
|
|
||||||
let insert_result1 = db
|
|
||||||
.upsert_user(&User {
|
|
||||||
username: "myuser".to_string(),
|
|
||||||
password: "abc".to_string(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(insert_result1.is_ok());
|
|
||||||
|
|
||||||
let insert_result2 = db
|
|
||||||
.upsert_user(&User {
|
|
||||||
username: "myuser".to_string(),
|
|
||||||
password: "123".to_string(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(insert_result2.is_ok());
|
|
||||||
|
|
||||||
let user = db
|
|
||||||
.get_user("myuser")
|
|
||||||
.await
|
|
||||||
.expect("User retrieval query failed");
|
|
||||||
|
|
||||||
assert!(user.is_some());
|
|
||||||
let user = user.unwrap();
|
|
||||||
assert_eq!(user.username, "myuser");
|
|
||||||
assert_eq!(user.password, "123"); //From second upsert
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn can_delete_user() {
|
|
||||||
let db = create_db().await;
|
|
||||||
|
|
||||||
let insert_result = db
|
|
||||||
.upsert_user(&User {
|
|
||||||
username: "myuser".to_string(),
|
|
||||||
password: "abc".to_string(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(insert_result.is_ok());
|
|
||||||
|
|
||||||
db.delete_user("myuser")
|
|
||||||
.await
|
|
||||||
.expect("User deletion query failed");
|
|
||||||
|
|
||||||
let user = db
|
|
||||||
.get_user("myuser")
|
|
||||||
.await
|
|
||||||
.expect("User retrieval query failed");
|
|
||||||
|
|
||||||
assert!(user.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn username_not_in_db_returns_none() {
|
|
||||||
let db = create_db().await;
|
|
||||||
let user = db
|
|
||||||
.get_user("does not exist")
|
|
||||||
.await
|
|
||||||
.expect("Get user query failure");
|
|
||||||
|
|
||||||
assert!(user.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn authenticate_user_is_some_with_valid_password() {
|
|
||||||
let db = create_db().await;
|
|
||||||
|
|
||||||
let insert_result = db
|
|
||||||
.upsert_user(&User {
|
|
||||||
username: "myuser".to_string(),
|
|
||||||
password: crate::logic::hash_password("abc").expect("password hash error!"),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(insert_result.is_ok());
|
|
||||||
|
|
||||||
let user = db
|
|
||||||
.authenticate_user("myuser", "abc")
|
|
||||||
.await
|
|
||||||
.expect("User retrieval query failed");
|
|
||||||
|
|
||||||
assert!(user.is_some());
|
|
||||||
let user = user.unwrap();
|
|
||||||
assert_eq!(user.username, "myuser");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
||||||
async fn authenticate_user_is_none_with_wrong_password() {
|
|
||||||
let db = create_db().await;
|
|
||||||
|
|
||||||
let insert_result = db
|
|
||||||
.upsert_user(&User {
|
|
||||||
username: "myuser".to_string(),
|
|
||||||
password: crate::logic::hash_password("abc").expect("password hash error!"),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(insert_result.is_ok());
|
|
||||||
|
|
||||||
let user = db
|
|
||||||
.authenticate_user("myuser", "wrong-password")
|
|
||||||
.await
|
|
||||||
.expect("User retrieval query failed");
|
|
||||||
|
|
||||||
assert!(user.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -78,15 +78,6 @@ pub enum BotError {
|
||||||
|
|
||||||
#[error("identifier error: {0}")]
|
#[error("identifier error: {0}")]
|
||||||
IdentifierError(#[from] matrix_sdk::identifiers::Error),
|
IdentifierError(#[from] matrix_sdk::identifiers::Error),
|
||||||
|
|
||||||
#[error("password creation error: {0}")]
|
|
||||||
PasswordCreationError(argon2::Error),
|
|
||||||
|
|
||||||
#[error("account does not exist, or password incorrect")]
|
|
||||||
AuthenticationError,
|
|
||||||
|
|
||||||
#[error("user account does not exist, try registering")]
|
|
||||||
AccountDoesNotExist,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
|
@ -4,10 +4,8 @@ use crate::error::{BotError, DiceRollingError};
|
||||||
use crate::matrix;
|
use crate::matrix;
|
||||||
use crate::models::RoomInfo;
|
use crate::models::RoomInfo;
|
||||||
use crate::parser::dice::{Amount, Element};
|
use crate::parser::dice::{Amount, Element};
|
||||||
use argon2::{self, Config, Error as ArgonError};
|
|
||||||
use futures::stream::{self, StreamExt, TryStreamExt};
|
use futures::stream::{self, StreamExt, TryStreamExt};
|
||||||
use matrix_sdk::{self, identifiers::RoomId, Client};
|
use matrix_sdk::{self, identifiers::RoomId, Client};
|
||||||
use rand::Rng;
|
|
||||||
use std::slice;
|
use std::slice;
|
||||||
|
|
||||||
/// Record the information about a room, including users in it.
|
/// Record the information about a room, including users in it.
|
||||||
|
@ -88,10 +86,3 @@ pub async fn calculate_dice_amount(amounts: &[Amount], ctx: &Context<'_>) -> Res
|
||||||
|
|
||||||
Ok(dice_amount)
|
Ok(dice_amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hash a password using the argon2 algorithm with a 16 byte salt.
|
|
||||||
pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
|
|
||||||
let salt = rand::thread_rng().gen::<[u8; 16]>();
|
|
||||||
let config = Config::default();
|
|
||||||
argon2::hash_encoded(raw_password.as_bytes(), &salt, &config)
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,40 +6,3 @@ pub struct RoomInfo {
|
||||||
pub room_id: String,
|
pub room_id: String,
|
||||||
pub room_name: String,
|
pub room_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
|
||||||
pub struct User {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
pub fn verify_password(&self, raw_password: &str) -> bool {
|
|
||||||
argon2::verify_encoded(&self.password, raw_password.as_bytes()).unwrap_or(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn verify_password_passes_with_correct_password() {
|
|
||||||
let user = User {
|
|
||||||
username: "myuser".to_string(),
|
|
||||||
password: crate::logic::hash_password("mypassword").expect("Password hashing error!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(user.verify_password("mypassword"), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn verify_password_fails_with_wrong_password() {
|
|
||||||
let user = User {
|
|
||||||
username: "myuser".to_string(),
|
|
||||||
password: crate::logic::hash_password("mypassword").expect("Password hashing error!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(user.verify_password("wrong-password"), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue