diff --git a/src/discord/joke.rs b/src/discord/joke.rs index 99a7450..d7a3d1c 100644 --- a/src/discord/joke.rs +++ b/src/discord/joke.rs @@ -1,7 +1,7 @@ use crate::album_manager::{Album, AlbumQuery, ImageQuery, ImageSort}; use crate::error::Error; use crate::models::insult_compliment::{RandomResponseTemplate, ResponseType}; -use crate::models::random::RandomConfig; +use crate::models::random::{Random, RandomConfig}; use crate::{command, group, GlobalData, BAD_APPLE}; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -106,7 +106,7 @@ pub async fn random(ctx: &Context, msg: &Message, random_name: &str) -> CommandR Some(r) => r, }; - let response_template_str = match random.get_response()? { + let response_template_str = match random.get_response(&global_data.db)? { None => { msg.reply(&ctx, format!("I'm all out of material for {}", random_name)) .await?; @@ -125,7 +125,7 @@ pub async fn random(ctx: &Context, msg: &Message, random_name: &str) -> CommandR let reply = render_random( guild_member.display_name(), global_data, - response_template_str, + &response_template_str, ) .await?; @@ -189,10 +189,16 @@ pub async fn list_random(ctx: &Context, msg: &Message, args: Args) -> CommandRes let dm_channel = msg.author.id.create_dm_channel(&ctx.http).await?; if let Some(random) = random { let mut msg_builder = MessageBuilder::new(); - msg_builder.push_line(format!("All possible responses for {}:", random_name)); + msg_builder.push_line(format!( + "All possible responses for {} all {} of them..:", + random_name, + random.responses.len() + )); for resp in random.responses { - let line_msg = format!("* {}", resp); + let random = global_data.db.get::(resp)?; + + let line_msg = format!("* ({}) ({}) {}", resp, random.score, random.response); if (msg_builder.0.len() + line_msg.len()) > MESSAGE_CODE_LIMIT { dm_channel.say(&ctx.http, msg_builder.build()).await?; diff --git a/src/migrations/migration3_remove_img.rs b/src/migrations/migration3_remove_img.rs index 2abd78b..a5bcccd 100644 --- a/src/migrations/migration3_remove_img.rs +++ b/src/migrations/migration3_remove_img.rs @@ -17,4 +17,4 @@ impl Migration for Migration3RemoveImage { fn version(&self) -> u64 { 3 } -} \ No newline at end of file +} diff --git a/src/migrations/migration_4_update_random.rs b/src/migrations/migration_4_update_random.rs new file mode 100644 index 0000000..7d0eca8 --- /dev/null +++ b/src/migrations/migration_4_update_random.rs @@ -0,0 +1,57 @@ +use crate::models::random::{Random, RandomConfig}; +use j_db::database::Database; +use j_db::error::JDbError; +use j_db::migration::Migration; +use j_db::model::JdbModel; +use json::number::Number; +use json::JsonValue; + +pub struct Migration4UpdateRandoms {} + +impl Migration for Migration4UpdateRandoms { + fn up(&self, db: &Database) -> j_db::error::Result<()> { + let random_config_tree = db.db.open_tree(RandomConfig::tree())?; + + for config in random_config_tree.iter() { + let (id, config) = config?; + let mut config = json::parse(std::str::from_utf8(&config).unwrap()).unwrap(); + + let mut responses: Vec = Vec::new(); + for random in config["responses"].members() { + let random = Random::new(&random.to_string()); + + let random = db.insert(random); + + let random = match random { + Ok(r) => r, + Err(err) => match err { + JDbError::NotUnique => continue, + _ => return Err(err), + }, + }; + + responses.push(random.id().unwrap()); + } + + config.remove("responses"); + + let responses: Vec = responses + .iter() + .map(|id| JsonValue::Number(Number::from(*id))) + .collect(); + config["responses"] = JsonValue::Array(responses); + + random_config_tree.insert(id, config.to_string().as_bytes())?; + } + + Ok(()) + } + + fn down(&self, _db: &Database) -> j_db::error::Result<()> { + Ok(()) + } + + fn version(&self) -> u64 { + 4 + } +} diff --git a/src/migrations/mod.rs b/src/migrations/mod.rs index 8919d90..1288253 100644 --- a/src/migrations/mod.rs +++ b/src/migrations/mod.rs @@ -1,13 +1,15 @@ use crate::migrations::migration2_remove_imgur::Migration2RemoveImgur; +use crate::migrations::migration3_remove_img::Migration3RemoveImage; +use crate::migrations::migration_4_update_random::Migration4UpdateRandoms; use j_db::database::Database; use j_db::migration; use j_db::migration::Direction; -use crate::migrations::migration3_remove_img::Migration3RemoveImage; mod migration2_remove_imgur; mod migration3_remove_img; +mod migration_4_update_random; -const CURRENT_DB_VERSION: u64 = 3; +const CURRENT_DB_VERSION: u64 = 4; #[allow(clippy::single_match)] pub fn do_migration(db: &Database) { @@ -28,7 +30,15 @@ pub fn do_migration(db: &Database) { Migration3RemoveImage {}, Direction::Up, ) - .unwrap(); + .unwrap(); + } + 4 => { + migration::do_migration::( + db, + Migration4UpdateRandoms {}, + Direction::Up, + ) + .unwrap(); } _ => {} } diff --git a/src/models/random.rs b/src/models/random.rs index ce5b6e3..4b59f19 100644 --- a/src/models/random.rs +++ b/src/models/random.rs @@ -1,15 +1,53 @@ use crate::error::Error; use j_db::database::Database; use j_db::model::JdbModel; -use rand::prelude::SliceRandom; +use rand::distributions::WeightedIndex; +use rand::prelude::Distribution; use rand::thread_rng; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +const MAX_SCORE: u16 = 10; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Random { + id: Option, + pub response: String, + pub score: u16, +} + +impl Random { + pub fn new(response: &str) -> Self { + Self { + id: None, + response: response.to_string(), + score: MAX_SCORE, + } + } +} + +impl JdbModel for Random { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + fn tree() -> String { + "random_opt".to_string() + } + + fn check_unique(&self, other: &Self) -> bool { + !self.response.eq_ignore_ascii_case(&other.response) + } +} #[derive(Debug, Deserialize, Serialize, Clone)] pub struct RandomConfig { id: Option, pub name: String, - pub responses: Vec, + pub responses: HashSet, } impl JdbModel for RandomConfig { @@ -32,18 +70,22 @@ impl JdbModel for RandomConfig { impl RandomConfig { pub fn add_random(db: &Database, name: &str, response: &str) -> Result<(), Error> { - let mut random = match Self::get_random(db, name)? { + let mut randoms = match Self::get_random(db, name)? { None => db.insert::(Self { id: None, name: name.to_string(), - responses: vec![], + responses: HashSet::new(), })?, Some(random) => random, }; - random.responses.push(response.to_string()); + let random = Random::new(response); - db.insert::(random)?; + let random = db.insert(random)?; + + randoms.responses.insert(random.id().unwrap()); + + db.insert::(randoms)?; Ok(()) } @@ -54,7 +96,31 @@ impl RandomConfig { .next()) } - pub fn get_response(&self) -> Result, Error> { - Ok(self.responses.choose(&mut thread_rng())) + pub fn get_response(&self, db: &Database) -> Result, Error> { + let mut responses: Vec = db + .filter(|_, random: &Random| self.responses.contains(&random.id().unwrap()))? + .collect(); + + if !responses.iter().any(|r| r.score > 0) { + for response in &mut responses { + response.score = MAX_SCORE; + + db.insert(response.clone())?; + } + } + + if responses.is_empty() { + return Ok(None); + } + + let dist = WeightedIndex::new(responses.iter().map(|r| r.score)).unwrap(); + + let mut resp = responses[dist.sample(&mut thread_rng())].clone(); + + resp.score = resp.score.saturating_sub(1); + + let resp = db.insert(resp)?; + + Ok(Some(resp.response)) } }