Replace application-level database connectivity.
- Some database methods not yet implemented. - Unit tests create temp files that are not cleaned up (but they should be).
This commit is contained in:
parent
6b6e59da2e
commit
cf9ce63892
|
@ -11,3 +11,4 @@ bot-db*
|
||||||
bigboy
|
bigboy
|
||||||
.#*
|
.#*
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
.tmp*
|
||||||
|
|
|
@ -2512,6 +2512,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"sled",
|
"sled",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
|
@ -50,3 +50,6 @@ features = ['derive']
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1"
|
version = "1"
|
||||||
features = [ "full" ]
|
features = [ "full" ]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
|
@ -2,7 +2,7 @@ use matrix_sdk::identifiers::room_id;
|
||||||
use tenebrous_dicebot::commands;
|
use tenebrous_dicebot::commands;
|
||||||
use tenebrous_dicebot::commands::ResponseExtractor;
|
use tenebrous_dicebot::commands::ResponseExtractor;
|
||||||
use tenebrous_dicebot::context::{Context, RoomContext};
|
use tenebrous_dicebot::context::{Context, RoomContext};
|
||||||
use tenebrous_dicebot::db::Database;
|
use tenebrous_dicebot::db::sqlite::Database;
|
||||||
use tenebrous_dicebot::error::BotError;
|
use tenebrous_dicebot::error::BotError;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ async fn main() -> Result<(), BotError> {
|
||||||
let homeserver = Url::parse("http://example.com")?;
|
let homeserver = Url::parse("http://example.com")?;
|
||||||
|
|
||||||
let context = Context {
|
let context = Context {
|
||||||
db: Database::new_temp()?,
|
db: Database::new_temp().await?,
|
||||||
matrix_client: &matrix_sdk::Client::new(homeserver)
|
matrix_client: &matrix_sdk::Client::new(homeserver)
|
||||||
.expect("Could not create matrix client"),
|
.expect("Could not create matrix client"),
|
||||||
room: RoomContext {
|
room: RoomContext {
|
||||||
|
|
|
@ -5,7 +5,7 @@ use log::error;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use tenebrous_dicebot::bot::DiceBot;
|
use tenebrous_dicebot::bot::DiceBot;
|
||||||
use tenebrous_dicebot::config::*;
|
use tenebrous_dicebot::config::*;
|
||||||
use tenebrous_dicebot::db::Database;
|
use tenebrous_dicebot::db::sqlite::Database;
|
||||||
use tenebrous_dicebot::error::BotError;
|
use tenebrous_dicebot::error::BotError;
|
||||||
use tenebrous_dicebot::migrator;
|
use tenebrous_dicebot::migrator;
|
||||||
use tenebrous_dicebot::state::DiceBotState;
|
use tenebrous_dicebot::state::DiceBotState;
|
||||||
|
@ -29,12 +29,10 @@ async fn run() -> Result<(), BotError> {
|
||||||
.expect("Need a config as an argument");
|
.expect("Need a config as an argument");
|
||||||
|
|
||||||
let cfg = Arc::new(read_config(config_path)?);
|
let cfg = Arc::new(read_config(config_path)?);
|
||||||
let db = Database::new(&cfg.database_path())?;
|
let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path());
|
||||||
|
let db = Database::new(&sqlite_path).await?;
|
||||||
let state = Arc::new(RwLock::new(DiceBotState::new(&cfg)));
|
let state = Arc::new(RwLock::new(DiceBotState::new(&cfg)));
|
||||||
|
|
||||||
db.migrate(cfg.migration_version())?;
|
|
||||||
|
|
||||||
let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path());
|
|
||||||
migrator::migrate(&sqlite_path).await?;
|
migrator::migrate(&sqlite_path).await?;
|
||||||
|
|
||||||
match DiceBot::new(&cfg, &state, &db) {
|
match DiceBot::new(&cfg, &state, &db) {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::commands::{execute_command, ExecutionError, ExecutionResult, ResponseExtractor};
|
use crate::commands::{execute_command, ExecutionError, ExecutionResult, ResponseExtractor};
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::context::{Context, RoomContext};
|
use crate::context::{Context, RoomContext};
|
||||||
use crate::db::Database;
|
use crate::db::sqlite::Database;
|
||||||
|
use crate::db::sqlite::DbState;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use crate::matrix;
|
use crate::matrix;
|
||||||
use crate::state::DiceBotState;
|
use crate::state::DiceBotState;
|
||||||
|
@ -134,7 +135,7 @@ impl DiceBot {
|
||||||
|
|
||||||
// Pull device ID from database, if it exists. Then write it
|
// Pull device ID from database, if it exists. Then write it
|
||||||
// to DB if the library generated one for us.
|
// to DB if the library generated one for us.
|
||||||
let device_id: Option<String> = self.db.state.get_device_id()?;
|
let device_id: Option<String> = self.db.get_device_id().await?;
|
||||||
let device_id: Option<&str> = device_id.as_deref();
|
let device_id: Option<&str> = device_id.as_deref();
|
||||||
|
|
||||||
client
|
client
|
||||||
|
@ -143,7 +144,7 @@ impl DiceBot {
|
||||||
|
|
||||||
if device_id.is_none() {
|
if device_id.is_none() {
|
||||||
let device_id = client.device_id().await.ok_or(BotError::NoDeviceIdFound)?;
|
let device_id = client.device_id().await.ok_or(BotError::NoDeviceIdFound)?;
|
||||||
self.db.state.set_device_id(device_id.as_str())?;
|
self.db.set_device_id(device_id.as_str()).await?;
|
||||||
info!("Recorded new device ID: {}", device_id.as_str());
|
info!("Recorded new device ID: {}", device_id.as_str());
|
||||||
} else {
|
} else {
|
||||||
info!("Using existing device ID: {}", device_id.unwrap());
|
info!("Using existing device ID: {}", device_id.unwrap());
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
* SDK example code.
|
* SDK example code.
|
||||||
*/
|
*/
|
||||||
use super::DiceBot;
|
use super::DiceBot;
|
||||||
use crate::db::Database;
|
use crate::db::sqlite::Database;
|
||||||
|
use crate::db::sqlite::Rooms;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use crate::logic::record_room_information;
|
use crate::logic::record_room_information;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -90,9 +91,9 @@ async fn should_process_message<'a>(
|
||||||
Ok((msg_body, sender_username))
|
Ok((msg_body, sender_username))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_process_event(db: &Database, room_id: &str, event_id: &str) -> bool {
|
async fn should_process_event(db: &Database, room_id: &str, event_id: &str) -> bool {
|
||||||
db.rooms
|
db.should_process(room_id, event_id)
|
||||||
.should_process(room_id, event_id)
|
.await
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
error!(
|
error!(
|
||||||
"Database error when checking if we should process an event: {}",
|
"Database error when checking if we should process an event: {}",
|
||||||
|
@ -116,7 +117,7 @@ impl EventHandler for DiceBot {
|
||||||
let room_id_str = room_id.as_str();
|
let room_id_str = room_id.as_str();
|
||||||
let username = &event.state_key;
|
let username = &event.state_key;
|
||||||
|
|
||||||
if !should_process_event(&self.db, room_id_str, event.event_id.as_str()) {
|
if !should_process_event(&self.db, room_id_str, event.event_id.as_str()).await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +136,7 @@ impl EventHandler for DiceBot {
|
||||||
|
|
||||||
let result = if event_affects_us && !adding_user {
|
let result = if event_affects_us && !adding_user {
|
||||||
info!("Clearing all information for room ID {}", room_id);
|
info!("Clearing all information for room ID {}", room_id);
|
||||||
self.db.rooms.clear_info(room_id_str)
|
self.db.clear_info(room_id_str).await
|
||||||
} else if event_affects_us && adding_user {
|
} else if event_affects_us && adding_user {
|
||||||
info!("Joined room {}; recording room information", room_id);
|
info!("Joined room {}; recording room information", room_id);
|
||||||
record_room_information(
|
record_room_information(
|
||||||
|
@ -148,10 +149,10 @@ impl EventHandler for DiceBot {
|
||||||
.await
|
.await
|
||||||
} else if !event_affects_us && adding_user {
|
} else if !event_affects_us && adding_user {
|
||||||
info!("Adding user {} to room ID {}", username, room_id);
|
info!("Adding user {} to room ID {}", username, room_id);
|
||||||
self.db.rooms.add_user_to_room(username, room_id_str)
|
self.db.add_user_to_room(username, room_id_str).await
|
||||||
} else if !event_affects_us && !adding_user {
|
} else if !event_affects_us && !adding_user {
|
||||||
info!("Removing user {} from room ID {}", username, room_id);
|
info!("Removing user {} from room ID {}", username, room_id);
|
||||||
self.db.rooms.remove_user_from_room(username, room_id_str)
|
self.db.remove_user_from_room(username, room_id_str).await
|
||||||
} else {
|
} else {
|
||||||
debug!("Ignoring a room member event: {:#?}", event);
|
debug!("Ignoring a room member event: {:#?}", event);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -196,7 +197,7 @@ impl EventHandler for DiceBot {
|
||||||
};
|
};
|
||||||
|
|
||||||
let room_id = room.room_id().as_str();
|
let room_id = room.room_id().as_str();
|
||||||
if !should_process_event(&self.db, room_id, event.event_id.as_str()) {
|
if !should_process_event(&self.db, room_id, event.event_id.as_str()).await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -325,7 +325,8 @@ pub async fn roll_pool(pool: &DicePoolWithContext<'_>) -> Result<RolledDicePool,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::Database;
|
use crate::db::sqlite::Database;
|
||||||
|
use crate::db::sqlite::Variables;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
macro_rules! dummy_room {
|
macro_rules! dummy_room {
|
||||||
|
@ -463,8 +464,8 @@ mod tests {
|
||||||
assert_eq!(vec![10], roll);
|
assert_eq!(vec![10], roll);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn number_of_dice_equality_test() {
|
fn number_of_dice_equality_test() {
|
||||||
let num_dice = 5;
|
let num_dice = 5;
|
||||||
let rolls = vec![1, 2, 3, 4, 5];
|
let rolls = vec![1, 2, 3, 4, 5];
|
||||||
let pool = DicePool::easy_pool(5, DicePoolQuality::TenAgain);
|
let pool = DicePool::easy_pool(5, DicePoolQuality::TenAgain);
|
||||||
|
@ -472,10 +473,10 @@ mod tests {
|
||||||
assert_eq!(5, rolled_pool.num_dice);
|
assert_eq!(5, rolled_pool.num_dice);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn rejects_large_expression_test() {
|
async fn rejects_large_expression_test() {
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
let homeserver = Url::parse("http://example.com").unwrap();
|
||||||
let db = Database::new_temp().unwrap();
|
let db = Database::new_temp().await.unwrap();
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
||||||
|
@ -504,9 +505,17 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn converts_to_chance_die_test() {
|
async fn converts_to_chance_die_test() {
|
||||||
let db = Database::new_temp().unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
|
crate::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
let homeserver = Url::parse("http://example.com").unwrap();
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
db: db,
|
db: db,
|
||||||
|
@ -533,11 +542,17 @@ mod tests {
|
||||||
assert_eq!(1, roll.num_dice);
|
assert_eq!(1, roll.num_dice);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn can_resolve_variables_test() {
|
async fn can_resolve_variables_test() {
|
||||||
use crate::db::variables::UserAndRoom;
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
|
crate::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let db = Database::new_temp().unwrap();
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
let homeserver = Url::parse("http://example.com").unwrap();
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
|
@ -547,10 +562,8 @@ mod tests {
|
||||||
message_body: "message",
|
message_body: "message",
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_and_room = UserAndRoom(&ctx.username, &ctx.room.id.as_str());
|
db.set_user_variable(&ctx.username, &ctx.room.id.as_str(), "myvariable", 10)
|
||||||
|
.await
|
||||||
db.variables
|
|
||||||
.set_user_variable(&user_and_room, "myvariable", 10)
|
|
||||||
.expect("could not set myvariable to 10");
|
.expect("could not set myvariable to 10");
|
||||||
|
|
||||||
let amounts = vec![Amount {
|
let amounts = vec![Amount {
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::context::Context;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use BotError::{DataError, SqliteDataError};
|
||||||
|
|
||||||
pub mod basic_rolling;
|
pub mod basic_rolling;
|
||||||
pub mod cofd;
|
pub mod cofd;
|
||||||
|
@ -50,7 +51,13 @@ pub struct ExecutionError(#[from] pub BotError);
|
||||||
|
|
||||||
impl From<crate::db::errors::DataError> for ExecutionError {
|
impl From<crate::db::errors::DataError> for ExecutionError {
|
||||||
fn from(error: crate::db::errors::DataError) -> Self {
|
fn from(error: crate::db::errors::DataError) -> Self {
|
||||||
Self(BotError::DataError(error))
|
Self(DataError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::db::sqlite::errors::DataError> for ExecutionError {
|
||||||
|
fn from(error: crate::db::sqlite::errors::DataError) -> Self {
|
||||||
|
Self(SqliteDataError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,9 +136,9 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn unrecognized_command() {
|
async fn unrecognized_command() {
|
||||||
let db = crate::db::Database::new_temp().unwrap();
|
let db = crate::db::sqlite::Database::new_temp().await.unwrap();
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
let homeserver = Url::parse("http://example.com").unwrap();
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
db: db,
|
db: db,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use super::{Command, Execution, ExecutionResult};
|
use super::{Command, Execution, ExecutionResult};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::db::errors::DataError;
|
use crate::db::sqlite::errors::DataError;
|
||||||
|
use crate::db::sqlite::Variables;
|
||||||
use crate::db::variables::UserAndRoom;
|
use crate::db::variables::UserAndRoom;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
@ -13,8 +14,10 @@ impl Command for GetAllVariablesCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
||||||
let key = UserAndRoom(&ctx.username, &ctx.room_id().as_str());
|
let variables = ctx
|
||||||
let variables = ctx.db.variables.get_user_variables(&key)?;
|
.db
|
||||||
|
.get_user_variables(&ctx.username, ctx.room_id().as_str())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut variable_list: Vec<String> = variables
|
let mut variable_list: Vec<String> = variables
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -43,8 +46,10 @@ impl Command for GetVariableCommand {
|
||||||
|
|
||||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
||||||
let name = &self.0;
|
let name = &self.0;
|
||||||
let key = UserAndRoom(&ctx.username, &ctx.room_id().as_str());
|
let result = ctx
|
||||||
let result = ctx.db.variables.get_user_variable(&key, name);
|
.db
|
||||||
|
.get_user_variable(&ctx.username, ctx.room_id().as_str(), name)
|
||||||
|
.await;
|
||||||
|
|
||||||
let value = match result {
|
let value = match result {
|
||||||
Ok(num) => format!("{} = {}", name, num),
|
Ok(num) => format!("{} = {}", name, num),
|
||||||
|
@ -68,9 +73,10 @@ impl Command for SetVariableCommand {
|
||||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
||||||
let name = &self.0;
|
let name = &self.0;
|
||||||
let value = self.1;
|
let value = self.1;
|
||||||
let key = UserAndRoom(&ctx.username, ctx.room_id().as_str());
|
|
||||||
|
|
||||||
ctx.db.variables.set_user_variable(&key, name, value)?;
|
ctx.db
|
||||||
|
.set_user_variable(&ctx.username, ctx.room_id().as_str(), name, value)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let content = format!("{} = {}", name, value);
|
let content = format!("{} = {}", name, value);
|
||||||
let html = format!("<strong>Set Variable:</strong> {}", content);
|
let html = format!("<strong>Set Variable:</strong> {}", content);
|
||||||
|
@ -88,8 +94,10 @@ impl Command for DeleteVariableCommand {
|
||||||
|
|
||||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
||||||
let name = &self.0;
|
let name = &self.0;
|
||||||
let key = UserAndRoom(&ctx.username, ctx.room_id().as_str());
|
let result = ctx
|
||||||
let result = ctx.db.variables.delete_user_variable(&key, name);
|
.db
|
||||||
|
.delete_user_variable(&ctx.username, ctx.room_id().as_str(), name)
|
||||||
|
.await;
|
||||||
|
|
||||||
let value = match result {
|
let value = match result {
|
||||||
Ok(()) => format!("{} now unset", name),
|
Ok(()) => format!("{} now unset", name),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::db::Database;
|
use crate::db::sqlite::Database;
|
||||||
use matrix_sdk::identifiers::RoomId;
|
use matrix_sdk::identifiers::RoomId;
|
||||||
use matrix_sdk::room::Joined;
|
use matrix_sdk::room::Joined;
|
||||||
use matrix_sdk::Client;
|
use matrix_sdk::Client;
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
use crate::db::sqlite::Variables;
|
||||||
use crate::error::{BotError, DiceRollingError};
|
use crate::error::{BotError, DiceRollingError};
|
||||||
use crate::parser::{Amount, Element};
|
use crate::parser::{Amount, Element};
|
||||||
use crate::{context::Context, db::variables::UserAndRoom};
|
use crate::{context::Context, db::variables::UserAndRoom};
|
||||||
use crate::{dice::calculate_single_die_amount, parser::DiceParsingError};
|
use crate::{dice::calculate_single_die_amount, parser::DiceParsingError};
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
use rand::Rng;
|
||||||
|
use rand::SeedableRng;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
@ -270,10 +274,11 @@ macro_rules! is_variable {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
///A version of DieRoller that uses a rand::Rng to roll numbers.
|
/// A die roller than can have an RNG implementation injected, but
|
||||||
struct RngDieRoller<R: rand::Rng>(R);
|
/// must be thread-safe. Required for the async dice rolling code.
|
||||||
|
struct RngDieRoller<R: Rng + ?Sized + Send>(R);
|
||||||
|
|
||||||
impl<R: rand::Rng> DieRoller for RngDieRoller<R> {
|
impl<R: Rng + ?Sized + Send> DieRoller for RngDieRoller<R> {
|
||||||
fn roll(&mut self) -> u32 {
|
fn roll(&mut self) -> u32 {
|
||||||
self.0.gen_range(0..=9)
|
self.0.gen_range(0..=9)
|
||||||
}
|
}
|
||||||
|
@ -361,7 +366,7 @@ pub async fn regular_roll(
|
||||||
let target = calculate_single_die_amount(&roll_with_ctx.0.amount, roll_with_ctx.1).await?;
|
let target = calculate_single_die_amount(&roll_with_ctx.0.amount, roll_with_ctx.1).await?;
|
||||||
let target = u32::try_from(target).map_err(|_| DiceRollingError::InvalidAmount)?;
|
let target = u32::try_from(target).map_err(|_| DiceRollingError::InvalidAmount)?;
|
||||||
|
|
||||||
let mut roller = RngDieRoller(rand::thread_rng());
|
let mut roller = RngDieRoller::<StdRng>(SeedableRng::from_entropy());
|
||||||
let rolled_dice = roll_regular_dice(&roll_with_ctx.0.modifier, target, &mut roller);
|
let rolled_dice = roll_regular_dice(&roll_with_ctx.0.modifier, target, &mut roller);
|
||||||
|
|
||||||
Ok(ExecutedDiceRoll {
|
Ok(ExecutedDiceRoll {
|
||||||
|
@ -371,11 +376,12 @@ pub async fn regular_roll(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_skill(ctx: &Context, variable: &str, value: u32) -> Result<(), BotError> {
|
async fn update_skill(ctx: &Context<'_>, variable: &str, value: u32) -> Result<(), BotError> {
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
let value: i32 = value.try_into()?;
|
let value: i32 = value.try_into()?;
|
||||||
let key = UserAndRoom(ctx.username, ctx.room_id().as_str());
|
ctx.db
|
||||||
ctx.db.variables.set_user_variable(&key, variable, value)?;
|
.set_user_variable(&ctx.username, &ctx.room_id().as_str(), variable, value)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,12 +403,14 @@ pub async fn advancement_roll(
|
||||||
return Err(DiceRollingError::InvalidAmount.into());
|
return Err(DiceRollingError::InvalidAmount.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut roller = RngDieRoller(rand::thread_rng());
|
let mut roller = RngDieRoller::<StdRng>(SeedableRng::from_entropy());
|
||||||
let roll = roll_advancement_dice(target, &mut roller);
|
let roll = roll_advancement_dice(target, &mut roller);
|
||||||
|
|
||||||
|
drop(roller);
|
||||||
|
|
||||||
if roll.successful && is_variable!(existing_skill) {
|
if roll.successful && is_variable!(existing_skill) {
|
||||||
let variable_name: &str = extract_variable(existing_skill)?;
|
let variable_name: &str = extract_variable(existing_skill)?;
|
||||||
update_skill(roll_with_ctx.1, variable_name, roll.new_skill_amount())?;
|
update_skill(roll_with_ctx.1, variable_name, roll.new_skill_amount()).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ExecutedAdvancementRoll { target, roll })
|
Ok(ExecutedAdvancementRoll { target, roll })
|
||||||
|
@ -411,7 +419,7 @@ pub async fn advancement_roll(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::parser::{Amount, Element, Operator};
|
use crate::parser::{Amount, Element, Operator};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -474,7 +482,7 @@ mod tests {
|
||||||
assert!(matches!(result, Err(DiceParsingError::WrongElementType)));
|
assert!(matches!(result, Err(DiceParsingError::WrongElementType)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn regular_roll_rejects_negative_numbers() {
|
async fn regular_roll_rejects_negative_numbers() {
|
||||||
let roll = DiceRoll {
|
let roll = DiceRoll {
|
||||||
amount: Amount {
|
amount: Amount {
|
||||||
|
@ -484,7 +492,15 @@ mod tests {
|
||||||
modifier: DiceRollModifier::Normal,
|
modifier: DiceRollModifier::Normal,
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = Database::new_temp().unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
|
crate::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
let homeserver = Url::parse("http://example.com").unwrap();
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
db: db,
|
db: db,
|
||||||
|
@ -503,7 +519,7 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn advancement_roll_rejects_negative_numbers() {
|
async fn advancement_roll_rejects_negative_numbers() {
|
||||||
let roll = AdvancementRoll {
|
let roll = AdvancementRoll {
|
||||||
existing_skill: Amount {
|
existing_skill: Amount {
|
||||||
|
@ -512,7 +528,15 @@ mod tests {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = Database::new_temp().unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
|
crate::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
let homeserver = Url::parse("http://example.com").unwrap();
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
db: db,
|
db: db,
|
||||||
|
@ -531,7 +555,7 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn advancement_roll_rejects_big_numbers() {
|
async fn advancement_roll_rejects_big_numbers() {
|
||||||
let roll = AdvancementRoll {
|
let roll = AdvancementRoll {
|
||||||
existing_skill: Amount {
|
existing_skill: Amount {
|
||||||
|
@ -540,7 +564,15 @@ mod tests {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = Database::new_temp().unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
|
crate::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let homeserver = Url::parse("http://example.com").unwrap();
|
let homeserver = Url::parse("http://example.com").unwrap();
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
db: db,
|
db: db,
|
||||||
|
|
|
@ -2,14 +2,43 @@ use async_trait::async_trait;
|
||||||
use errors::DataError;
|
use errors::DataError;
|
||||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||||
use sqlx::ConnectOptions;
|
use sqlx::ConnectOptions;
|
||||||
use sqlx::Connection;
|
use std::clone::Clone;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::Path;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::models::RoomInfo;
|
||||||
|
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
pub mod rooms;
|
||||||
|
pub mod state;
|
||||||
pub mod variables;
|
pub mod variables;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub(crate) trait DbState {
|
||||||
|
async fn get_device_id(&self) -> Result<Option<String>, DataError>;
|
||||||
|
|
||||||
|
async fn set_device_id(&self, device_id: &str) -> Result<(), DataError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub(crate) trait Rooms {
|
||||||
|
async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError>;
|
||||||
|
|
||||||
|
async fn insert_room_info(&self, info: &RoomInfo) -> Result<(), DataError>;
|
||||||
|
|
||||||
|
async fn get_room_info(&self, room_id: &str) -> Result<Option<RoomInfo>, DataError>;
|
||||||
|
|
||||||
|
async fn get_rooms_for_user(&self, user_id: &str) -> Result<HashSet<String>, DataError>;
|
||||||
|
|
||||||
|
async fn get_users_in_room(&self, room_id: &str) -> Result<HashSet<String>, DataError>;
|
||||||
|
|
||||||
|
async fn add_user_to_room(&self, username: &str, room_id: &str) -> Result<(), DataError>;
|
||||||
|
|
||||||
|
async fn remove_user_from_room(&self, username: &str, room_id: &str) -> Result<(), DataError>;
|
||||||
|
|
||||||
|
async fn clear_info(&self, room_id: &str) -> Result<(), DataError>;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO move this up to the top once we delete sled. Traits will be the
|
// TODO move this up to the top once we delete sled. Traits will be the
|
||||||
// main API, then we can have different impls for different DBs.
|
// main API, then we can have different impls for different DBs.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -52,7 +81,6 @@ pub struct Database {
|
||||||
impl Database {
|
impl Database {
|
||||||
fn new_db(conn: SqlitePool) -> Result<Database, DataError> {
|
fn new_db(conn: SqlitePool) -> Result<Database, DataError> {
|
||||||
let database = Database { conn: conn.clone() };
|
let database = Database { conn: conn.clone() };
|
||||||
|
|
||||||
Ok(database)
|
Ok(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,3 +106,11 @@ impl Database {
|
||||||
Self::new("sqlite::memory:").await
|
Self::new("sqlite::memory:").await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for Database {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Database {
|
||||||
|
conn: self.conn.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
use super::errors::DataError;
|
||||||
|
use super::{Database, Rooms};
|
||||||
|
use crate::models::RoomInfo;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Rooms for Database {
|
||||||
|
async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_room_info(&self, info: &RoomInfo) -> Result<(), DataError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_room_info(&self, room_id: &str) -> Result<Option<RoomInfo>, DataError> {
|
||||||
|
Ok(Some(RoomInfo {
|
||||||
|
room_id: "".to_string(),
|
||||||
|
room_name: "".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_rooms_for_user(&self, user_id: &str) -> Result<HashSet<String>, DataError> {
|
||||||
|
Ok(HashSet::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_users_in_room(&self, room_id: &str) -> Result<HashSet<String>, DataError> {
|
||||||
|
Ok(HashSet::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_user_to_room(&self, username: &str, room_id: &str) -> Result<(), DataError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_user_from_room(&self, username: &str, room_id: &str) -> Result<(), DataError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_info(&self, room_id: &str) -> Result<(), DataError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
use super::errors::DataError;
|
||||||
|
use super::{Database, DbState};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl DbState for Database {
|
||||||
|
async fn get_device_id(&self) -> Result<Option<String>, DataError> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_device_id(&self, device_id: &str) -> Result<(), DataError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,7 +58,7 @@ impl Variables for Database {
|
||||||
) -> Result<(), DataError> {
|
) -> Result<(), DataError> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO user_variables
|
"INSERT INTO user_variables
|
||||||
(user_id, room_id, variable_name, value)
|
(user_id, room_id, key, value)
|
||||||
values (?, ?, ?, ?)",
|
values (?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(user)
|
.bind(user)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
|
use crate::db::sqlite::Variables;
|
||||||
use crate::db::variables::UserAndRoom;
|
use crate::db::variables::UserAndRoom;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use crate::error::DiceRollingError;
|
use crate::error::DiceRollingError;
|
||||||
|
@ -22,8 +23,10 @@ pub async fn calculate_single_die_amount(
|
||||||
/// it cannot find a variable defined, or if the database errors.
|
/// it cannot find a variable defined, or if the database errors.
|
||||||
pub async fn calculate_dice_amount(amounts: &[Amount], ctx: &Context<'_>) -> Result<i32, BotError> {
|
pub async fn calculate_dice_amount(amounts: &[Amount], ctx: &Context<'_>) -> Result<i32, BotError> {
|
||||||
let stream = stream::iter(amounts);
|
let stream = stream::iter(amounts);
|
||||||
let key = UserAndRoom(&ctx.username, ctx.room_id().as_str());
|
let variables = &ctx
|
||||||
let variables = &ctx.db.variables.get_user_variables(&key)?;
|
.db
|
||||||
|
.get_user_variables(&ctx.username, ctx.room_id().as_str())
|
||||||
|
.await?;
|
||||||
|
|
||||||
use DiceRollingError::VariableNotFound;
|
use DiceRollingError::VariableNotFound;
|
||||||
let dice_amount: i32 = stream
|
let dice_amount: i32 = stream
|
||||||
|
|
|
@ -21,6 +21,9 @@ pub enum BotError {
|
||||||
#[error("database error: {0}")]
|
#[error("database error: {0}")]
|
||||||
DataError(#[from] DataError),
|
DataError(#[from] DataError),
|
||||||
|
|
||||||
|
#[error("sqlite database error: {0}")]
|
||||||
|
SqliteDataError(#[from] crate::db::sqlite::errors::DataError),
|
||||||
|
|
||||||
#[error("the message should not be processed because it failed validation")]
|
#[error("the message should not be processed because it failed validation")]
|
||||||
ShouldNotProcessError,
|
ShouldNotProcessError,
|
||||||
|
|
||||||
|
|
24
src/logic.rs
24
src/logic.rs
|
@ -1,12 +1,14 @@
|
||||||
use crate::db::errors::DataError;
|
use crate::db::sqlite::errors::DataError;
|
||||||
|
use crate::db::sqlite::Rooms;
|
||||||
use crate::matrix;
|
use crate::matrix;
|
||||||
use crate::models::RoomInfo;
|
use crate::models::RoomInfo;
|
||||||
|
use futures::stream::{self, StreamExt, TryStreamExt};
|
||||||
use matrix_sdk::{self, identifiers::RoomId, Client};
|
use matrix_sdk::{self, identifiers::RoomId, Client};
|
||||||
|
|
||||||
/// Record the information about a room, including users in it.
|
/// Record the information about a room, including users in it.
|
||||||
pub async fn record_room_information(
|
pub async fn record_room_information(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
db: &crate::db::Database,
|
db: &crate::db::sqlite::Database,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
room_display_name: &str,
|
room_display_name: &str,
|
||||||
our_username: &str,
|
our_username: &str,
|
||||||
|
@ -21,11 +23,19 @@ pub async fn record_room_information(
|
||||||
|
|
||||||
// TODO this and the username adding should be one whole
|
// TODO this and the username adding should be one whole
|
||||||
// transaction in the db.
|
// transaction in the db.
|
||||||
db.rooms.insert_room_info(&info)?;
|
db.insert_room_info(&info).await?;
|
||||||
|
|
||||||
usernames
|
let filtered_usernames = usernames
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|username| username != our_username)
|
.filter(|username| username != our_username);
|
||||||
.map(|username| db.rooms.add_user_to_room(&username, room_id_str))
|
|
||||||
.collect() //Make use of collect impl on Result.
|
// Async collect into vec of results, then use into_iter of result
|
||||||
|
// to go to from Result<Vec<()>> to just Result<()>. Easier than
|
||||||
|
// attempting to async-collect our way to a single Result<()>.
|
||||||
|
stream::iter(filtered_usernames)
|
||||||
|
.then(|username| async move { db.add_user_to_room(&username, &room_id_str).await })
|
||||||
|
.collect::<Vec<Result<(), DataError>>>()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue