diff --git a/Cargo.toml b/Cargo.toml index 7d533cf..9be9c47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,9 @@ authors = ["hodasemi "] [dependencies] typemap = "~0.3" serde_json = "*" -utilities = { git = "http://gavania.de/hodasemi/context" } rusqlite = { version = "*", features = ["bundled"] } serenity = { version = "0.8", default-features = false, features = [ "builder", "cache", "client", "framework", "gateway", "model", "standard_framework", "utils", "voice", "rustls_backend"]} parking_lot = "*" -failure = "*" +anyhow = "*" hey_listen = "*" -white_rabbit = "*" +rand = "*" diff --git a/src/config_handler.rs b/src/config_handler.rs new file mode 100644 index 0000000..0350a5c --- /dev/null +++ b/src/config_handler.rs @@ -0,0 +1,304 @@ +#![allow(unused)] + +//! Config file handler +//! Cares about formatting and type conversion +use anyhow::{Context, Result}; +use std::{ + collections::HashMap, + fmt::Display, + fs::File, + io::{BufRead, BufReader, Write}, + path::Path, + str::FromStr, +}; +/// Value abstraction to convert to and from values +#[derive(Clone, Debug)] +pub enum Value { + Value(String), + Array(Vec), +} +struct ConfigSection { + header: String, + body: HashMap, +} +impl Value { + /// Creates an empty value + pub fn empty() -> Value { + Value::Value("".to_string()) + } + /// Creates an empty array value + pub fn empty_array() -> Value { + Value::Array(Vec::new()) + } + /// Create a value `Value::Array(Vec)`, internal conversion to string + /// + /// # Arguments + /// + /// `array` array of type, type has to implement `Display` trait + #[deprecated] + pub fn from_array(array: &[T]) -> Self { + Value::Array(array.iter().map(|v| format!("{}", v)).collect()) + } + /// Creates a value `Value::Value(String)`, internal conversion to string + /// + /// # Arguments + /// + /// `value` type has to implement `Display` trait + #[deprecated] + pub fn from_value(value: &T) -> Self { + Value::Value(format!("{}", value)) + } + pub fn to_array(&self) -> Result> { + match self { + Value::Array(value_array) => { + let mut target_array = Vec::with_capacity(value_array.len()); + for value_string in value_array { + match value_string.parse::() { + Ok(val) => target_array.push(val), + Err(_) => { + return Err(anyhow::Error::msg(format!( + "ConfigHandler: Error while parsing Value Array: {}", + value_string + ))); + } + } + } + Ok(target_array) + } + _ => Err(anyhow::Error::msg( + "ConfigHandler: Error when requesting the wrong value type", + )), + } + } + pub fn to_value(&self) -> Result { + match self { + Value::Value(value_string) => match value_string.parse::() { + Ok(val) => Ok(val), + Err(_) => Err(anyhow::Error::msg(format!( + "ConfigHandler: Error while parsing Value Array: {}", + value_string + ))), + }, + _ => Err(anyhow::Error::msg( + "ConfigHandler: Error when requesting the wrong value type", + )), + } + } +} +impl<'a, T: Display> From<&'a T> for Value { + fn from(v: &'a T) -> Self { + Value::Value(format!("{}", v)) + } +} +impl<'a, T: Display> From<&'a [T]> for Value { + fn from(v: &'a [T]) -> Self { + Value::Array(v.iter().map(|v| format!("{}", v)).collect()) + } +} +/// Handler struct +pub struct ConfigHandler {} +impl ConfigHandler { + /// Reads the given config file + /// + /// # Arguments + /// + /// `file_name` file that is going to be read + pub fn read_config( + file_name: impl AsRef, + ) -> Result>> { + let file = File::open(&file_name).with_context({ + let file_name = file_name.as_ref().to_str().unwrap().to_string(); + || file_name + })?; + let mut infos = HashMap::new(); + let mut current_section: Option = None; + for line_res in BufReader::new(file).lines() { + if let Ok(line) = line_res { + let mut trimmed = line.trim().to_string(); + if trimmed.starts_with('#') || trimmed.is_empty() { + continue; + } else if trimmed.starts_with('[') && trimmed.ends_with(']') { + trimmed.remove(0); + trimmed.pop(); + if let Some(ref section) = current_section { + infos.insert(section.header.clone(), section.body.clone()); + } + current_section = Some(ConfigSection { + header: trimmed, + body: HashMap::new(), + }); + } else { + let mut split = trimmed.split('='); + let key = match split.nth(0) { + Some(key) => key.trim().to_string(), + None => { + println!("cannot get key from line: {}", trimmed); + continue; + } + }; + let value = match split.last() { + Some(value) => value.trim().to_string(), + None => { + println!("cannot get value from line: {}", trimmed); + continue; + } + }; + if value.starts_with('[') && value.ends_with(']') { + let mut trimmed_value = value; + trimmed_value.remove(0); + trimmed_value.pop(); + let value_split = trimmed_value.split(','); + let mut value_array = Vec::new(); + for v in value_split { + let trimmed = v.trim(); + if !trimmed.is_empty() { + value_array.push(trimmed.to_string()); + } + } + if let Some(ref mut section) = current_section { + section.body.insert(key, Value::Array(value_array)); + } + } else if let Some(ref mut section) = current_section { + section.body.insert(key, Value::Value(value)); + } + } + } + } + // also push the last section + if let Some(section) = current_section { + infos.insert(section.header, section.body); + } + Ok(infos) + } + /// writes a formatted config file + /// + /// # Arguments + /// + /// `file_name` the file to which the config gets written + /// `sections` the sections and keys that are going to be written + pub fn write_config( + file_name: impl AsRef, + sections: &[(&str, Vec<(&str, Value)>)], + ) -> Result<()> { + let mut file = File::create(file_name)?; + for (header, body) in sections { + let fmt_header = format!("[{}]\n", header); + file.write_all(fmt_header.as_bytes())?; + for (key, value) in body.iter() { + let fmt_key_value = format!( + "{} = {}\n", + key, + match value { + Value::Value(val) => val.clone(), + Value::Array(array) => { + let mut array_value = "[".to_string(); + for (i, val) in array.iter().enumerate() { + // if element is not the last one + if i != array.len() - 1 { + array_value = format!("{}{}, ", array_value, val); + } else { + array_value = format!("{}{}", array_value, val); + } + } + format!("{}]", array_value) + } + } + ); + file.write_all(fmt_key_value.as_bytes())?; + } + file.write_all("\n".as_bytes())?; + } + Ok(()) + } +} +#[macro_export] +macro_rules! create_settings_section { + ($struct_name:ident, $section_key:expr, {$($var:ident: $var_type:ty,)* $([$array:ident: $array_type:ty],)*} $(,$($derive:ident,)*)?) => { + #[derive(Default, Debug, Clone, PartialEq $(, $($derive,)* )? )] + pub struct $struct_name { + $( + pub $var: $var_type, + )* + $( + pub $array: Vec<$array_type>, + )* + } + impl $struct_name { + const SECTION_KEY: &'static str = $section_key; + pub fn load( + parsed_config: &std::collections::HashMap> + ) -> anyhow::Result { + let mut me = Self::default(); + if let Some(section) = parsed_config.get(Self::SECTION_KEY) { + $( + if let Some(value) = section.get(stringify!($var)) { + me.$var = value.to_value()?; + } + )* + $( + if let Some(array) = section.get(stringify!($array)) { + me.$array = array.to_array()?; + } + )* + } + Ok(me) + } + pub fn store(&self) -> (&str, Vec<(&str, Value)>) { + ( + Self::SECTION_KEY, + vec![ + $( + (stringify!($var), Value::from(&self.$var)), + )* + $( + (stringify!($array), Value::from(self.$array.as_slice())), + )* + ] + ) + } + } + }; +} +#[macro_export] +macro_rules! create_settings_container { + ($struct_name:ident, {$($var:ident: $var_type:ty$(,)?)*} $(,$($derive:ident,)*)? ) => { + #[derive(Default, Debug, Clone, PartialEq $(, $($derive,)* )? )] + pub struct $struct_name { + pub file_name: assetpath::AssetPath, + $( + pub $var: $var_type, + )* + } + impl $struct_name { + pub fn load(file: impl Into) -> anyhow::Result { + let file = file.into(); + let parsed_stats_settings = ConfigHandler::read_config(&file.full_path())?; + Ok($struct_name { + file_name: file, + $( + $var: <$var_type>::load(&parsed_stats_settings)?, + )* + }) + } + pub fn load_with_default(&mut self, file: impl Into) -> anyhow::Result<()> { + let file = file.into(); + let parsed_stats_settings = ConfigHandler::read_config(&file.full_path())?; + self.file_name = file; + $( + self.$var = <$var_type>::load(&parsed_stats_settings)?; + )* + Ok(()) + } + pub fn store(&self) -> anyhow::Result<()> { + ConfigHandler::write_config( + &self.file_name.full_path(), + &[ + $( + self.$var.store(), + )* + ] + ) + } + } + }; +} diff --git a/src/main.rs b/src/main.rs index f591673..ed5585e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ extern crate parking_lot; extern crate serde_json; extern crate typemap; +mod config_handler; mod player; use serenity::{ @@ -18,15 +19,15 @@ use serenity::{ }; // This imports `typemap`'s `Key` as `TypeMapKey`. +use anyhow::Result; use serenity::prelude::*; use std::sync::{Arc, Mutex}; +use config_handler::{ConfigHandler, Value}; use player::prelude::*; use std::collections::HashSet; -use utilities::prelude::*; - create_settings_section!( Config, "Meta", @@ -53,7 +54,7 @@ fn my_help( help_commands::with_embeds(context, msg, args, &help_options, groups, owners) } -fn main() -> VerboseResult<()> { +fn main() -> Result<()> { // read config file let config_file = ConfigHandler::read_config("bot.conf")?; diff --git a/src/player/commands/play.rs b/src/player/commands/play.rs index 46217e3..35ad588 100644 --- a/src/player/commands/play.rs +++ b/src/player/commands/play.rs @@ -11,7 +11,7 @@ use serenity::{ model::channel::Message, }; -use utilities::prelude::*; +use anyhow::{anyhow, Result}; use std::sync::Once; use std::thread; @@ -127,7 +127,7 @@ fn append_songs( ctx: &Context, msg: &Message, mut source: Vec, -) -> VerboseResult<()> { +) -> Result<()> { media.playlist_mut().append(&mut source); println!("start playing"); @@ -141,7 +141,7 @@ fn handle_http_request( ctx: &serenity::client::Context, msg: &serenity::model::channel::Message, url: &str, -) -> VerboseResult<()> { +) -> Result<()> { let mut names = Vec::new(); { @@ -149,18 +149,18 @@ fn handle_http_request( let mut stmt = match sql.prepare("SELECT name FROM Vulva3 WHERE link = ?") { Ok(statement) => statement, - Err(_) => create_error!("failed preparing data base access"), + Err(_) => return Err(anyhow!("failed preparing data base access")), }; let rows = match stmt.query_map(&[url], |row| row.get(0) as rusqlite::Result) { Ok(rows) => rows, - Err(_) => create_error!("failed querying rows"), + Err(_) => return Err(anyhow!("failed querying rows")), }; for name_result in rows { let name = match name_result { Ok(name) => name, - Err(_) => create_error!("failed getting name from row"), + Err(_) => return Err(anyhow!("failed getting name from row")), }; names.push(name); @@ -170,7 +170,7 @@ fn handle_http_request( if names.len() > 0 { msg.channel_id .say(&ctx.http, "song already loaded!") - .map_err(|err| format!("{}", err))?; + .map_err(|err| anyhow!("{}", err))?; append_songs( media, @@ -186,7 +186,7 @@ fn handle_http_request( msg.channel_id .say(&ctx.http, format!("Error using youtube-dl: {}", why)) - .map_err(|err| format!("{}", err))?; + .map_err(|err| anyhow!("{}", err))?; return Ok(()); } @@ -212,14 +212,14 @@ fn handle_http_request( ) .is_err() { - create_error!("failed inserting songs into db"); + return Err(anyhow!("failed inserting songs into db")); } } } msg.channel_id .say(&ctx.http, info) - .map_err(|err| format!("{}", err))?; + .map_err(|err| anyhow!("{}", err))?; append_songs(media, ctx, msg, source)?; } @@ -231,7 +231,7 @@ fn handle_local_request( media: &mut MediaData, ctx: &serenity::client::Context, msg: &serenity::model::channel::Message, -) -> VerboseResult<()> { +) -> Result<()> { let mut songs = Vec::new(); { @@ -239,18 +239,18 @@ fn handle_local_request( let mut stmt = match sql.prepare("SELECT name FROM Vulva3") { Ok(statement) => statement, - Err(_) => create_error!("failed preparing data base access"), + Err(_) => return Err(anyhow!("failed preparing data base access")), }; let rows = match stmt.query_map(params![], |row| row.get(0) as rusqlite::Result) { Ok(rows) => rows, - Err(_) => create_error!("failed querying rows"), + Err(_) => return Err(anyhow!("failed querying rows")), }; for name_result in rows { let name = match name_result { Ok(name) => name, - Err(_) => create_error!("failed getting name from row"), + Err(_) => return Err(anyhow!("failed getting name from row")), }; songs.push(Song { name }); @@ -270,7 +270,7 @@ fn handle_song_request( ctx: &serenity::client::Context, msg: &serenity::model::channel::Message, pattern: &str, -) -> VerboseResult<()> { +) -> Result<()> { println!("song request ({})", pattern); let mut songs = Vec::new(); @@ -283,18 +283,18 @@ fn handle_song_request( pattern )) { Ok(statement) => statement, - Err(_) => create_error!("failed preparing data base access"), + Err(_) => return Err(anyhow!("failed preparing data base access")), }; let rows = match stmt.query_map(params![], |row| row.get(0) as rusqlite::Result) { Ok(rows) => rows, - Err(_) => create_error!("failed querying rows"), + Err(_) => return Err(anyhow!("failed querying rows")), }; for name_result in rows { let name = match name_result { Ok(name) => name, - Err(_) => create_error!("failed getting name from row"), + Err(_) => return Err(anyhow!("failed getting name from row")), }; songs.push(Song { name }); @@ -313,7 +313,7 @@ fn handle_song_request( } else { msg.channel_id .say(&ctx.http, format!("no song found with pattern {}", pattern)) - .map_err(|err| format!("{}", err))?; + .map_err(|err| anyhow!("{}", err))?; } Ok(()) @@ -324,7 +324,7 @@ fn handle_tag_request( ctx: &serenity::client::Context, msg: &serenity::model::channel::Message, pattern: &str, -) -> VerboseResult<()> { +) -> Result<()> { let mut songs = Vec::new(); { @@ -333,37 +333,37 @@ fn handle_tag_request( pattern )) { Ok(statement) => statement, - Err(_) => create_error!("failed preparing data base access"), + Err(_) => return Err(anyhow!("failed preparing data base access")), }; let mut rows = match stmt.query_map(params![], |row| row.get(0) as rusqlite::Result) { Ok(rows) => rows, - Err(_) => create_error!("failed querying rows"), + Err(_) => return Err(anyhow!("failed querying rows")), }; if let None = rows.next() { msg.channel_id .say(&ctx.http, format!("tag ({}) not found", pattern)) - .map_err(|err| format!("{}", err))?; + .map_err(|err| anyhow!("{}", err))?; return Ok(()); } let mut stmt = match media.db().prepare(&format!("SELECT name FROM {}", pattern)) { Ok(statement) => statement, - Err(_) => create_error!("failed preparing data base access"), + Err(_) => return Err(anyhow!("failed preparing data base access")), }; let rows = match stmt.query_map(params![], |row| row.get(0) as rusqlite::Result) { Ok(rows) => rows, - Err(_) => create_error!("failed querying rows"), + Err(_) => return Err(anyhow!("failed querying rows")), }; for name_result in rows { let name = match name_result { Ok(name) => name, - Err(_) => create_error!("failed getting name from row"), + Err(_) => return Err(anyhow!("failed getting name from row")), }; songs.push(Song { name }); diff --git a/src/player/mediadata.rs b/src/player/mediadata.rs index 8df1396..c076cae 100644 --- a/src/player/mediadata.rs +++ b/src/player/mediadata.rs @@ -13,10 +13,9 @@ use super::prelude::*; use std::fs; +use anyhow::{anyhow, Result}; use rusqlite::{params, Connection}; -use utilities::prelude::*; - #[derive(Debug)] pub struct Song { pub name: String, @@ -74,7 +73,7 @@ impl MediaData { &mut self, ctx: &serenity::client::Context, msg: &serenity::model::channel::Message, - ) -> Result<(), String> { + ) -> Result<()> { self.playlist.clear(); self.current_song = None; self.song_name = String::new(); @@ -124,11 +123,7 @@ impl MediaData { &mut self.song_name } - pub fn start_playing( - ctx: &Context, - mediadata: &mut MediaData, - msg: &Message, - ) -> VerboseResult<()> { + pub fn start_playing(ctx: &Context, mediadata: &mut MediaData, msg: &Message) -> Result<()> { // check if there is already playing let already_started = mediadata.song().is_some(); @@ -141,7 +136,7 @@ impl MediaData { Ok(()) } - fn check_for_next(&mut self, ctx: &Context, msg: &Message) -> VerboseResult { + fn check_for_next(&mut self, ctx: &Context, msg: &Message) -> Result { println!("check for next"); while !self.playlist().is_empty() { @@ -158,7 +153,7 @@ impl MediaData { &first ), ) - .map_err(|err| format!("{}", err))?; + .map_err(|err| anyhow!("{}", err))?; let sql = self.db(); @@ -166,19 +161,19 @@ impl MediaData { let mut stmt = match sql.prepare("SELECT name FROM sqlite_master WHERE type='table'") { Ok(statement) => statement, - Err(_) => create_error!("failed preparing data base access"), + Err(_) => return Err(anyhow!("failed preparing data base access")), }; let rows = match stmt.query_map(params![], |row| row.get(0) as rusqlite::Result) { Ok(rows) => rows, - Err(_) => create_error!("failed querying rows"), + Err(_) => return Err(anyhow!("failed querying rows")), }; for row in rows { let table_name = match row { Ok(name) => name, - Err(_) => create_error!("failed getting name from row"), + Err(_) => return Err(anyhow!("failed getting name from row")), }; if let Err(_) = sql.execute( @@ -194,14 +189,14 @@ impl MediaData { } println!("no song found!"); - create_error!("no suitable song found!") + Err(anyhow!("no suitable song found!")) } fn check_for_channel( manager: &mut MutexGuard, ctx: &Context, msg: &Message, - ) -> VerboseResult<()> { + ) -> Result<()> { println!("check for channel!"); let guild = guild(ctx, msg)?; @@ -216,7 +211,7 @@ impl MediaData { .and_then(|voice_state| voice_state.channel_id) { Some(channel) => channel, - None => create_error!("author is not in a voice channel!"), + None => return Err(anyhow!("author is not in a voice channel!")), }; println!("got author channel"); @@ -238,7 +233,7 @@ impl MediaData { Ok(()) } - pub fn next_song(ctx: &Context, mediadata: &mut MediaData, msg: &Message) -> VerboseResult<()> { + pub fn next_song(ctx: &Context, mediadata: &mut MediaData, msg: &Message) -> Result<()> { println!("start next song"); let voice_manager = mediadata.voice_manager.clone(); let mut manager = voice_manager.lock(); @@ -269,7 +264,7 @@ impl MediaData { Some(handler) => handler, None => { println!("failed getting handler"); - create_error!("error getting handler"); + return Err(anyhow!("error getting handler")); } } }; @@ -278,7 +273,7 @@ impl MediaData { let source = match ffmpeg(first.clone()) { Ok(mpeg) => mpeg, - Err(_) => create_error!(format!("failed loading: {}", &first)), + Err(_) => return Err(anyhow!(format!("failed loading: {}", &first))), }; handler.stop(); @@ -295,7 +290,7 @@ impl MediaData { msg.channel_id .say(&ctx.http, format!("Playing song: {}", first)) - .map_err(|err| format!("{}", err))?; + .map_err(|err| anyhow!("{}", err))?; } if need_to_leave { diff --git a/src/player/player.rs b/src/player/player.rs index 5ca0e72..85c0a9a 100644 --- a/src/player/player.rs +++ b/src/player/player.rs @@ -8,14 +8,14 @@ use serenity::voice::Handler; use std::sync::Arc; -use utilities::prelude::*; +use anyhow::{anyhow, Result}; // This imports `typemap`'s `Key` as `TypeMapKey`. use serenity::prelude::*; use super::prelude::*; -pub fn guild(ctx: &Context, msg: &Message) -> Result>, String> { +pub fn guild(ctx: &Context, msg: &Message) -> Result>> { match msg.guild(&ctx.cache) { Some(guild) => Ok(guild), None => { @@ -25,12 +25,12 @@ pub fn guild(ctx: &Context, msg: &Message) -> Result>, Stri .is_err() {} - Err("failed getting Guild".to_string()) + Err(anyhow!("failed getting Guild")) } } } -pub fn guild_id(ctx: &Context, msg: &Message) -> Result { +pub fn guild_id(ctx: &Context, msg: &Message) -> Result { let guild = guild(ctx, msg)?; let guild_read = guild.read(); @@ -47,7 +47,7 @@ pub fn handler<'a, T: RawMutex>( pub fn channel_contains_author( ctx: &mut serenity::client::Context, msg: &serenity::model::channel::Message, -) -> VerboseResult<()> { +) -> Result<()> { let guild = guild(ctx, msg)?; let guild_id = guild.read().id; @@ -58,7 +58,7 @@ pub fn channel_contains_author( .and_then(|voice_state| voice_state.channel_id) { Some(channel) => channel, - None => create_error!("author is not in a voice channel!"), + None => return Err(anyhow!("author is not in a voice channel!")), }; if let Some(media) = ctx.data.read().get::() { @@ -69,7 +69,9 @@ pub fn channel_contains_author( // check if the bot is in a channel if let Some(bot_channel_id) = handler.channel_id { if bot_channel_id != author_channel_id { - create_error!("author is not in the same voice channel as the bot!"); + return Err(anyhow!( + "author is not in the same voice channel as the bot!" + )); } } }; diff --git a/src/player/youtube.rs b/src/player/youtube.rs index ec773c8..87796db 100644 --- a/src/player/youtube.rs +++ b/src/player/youtube.rs @@ -23,7 +23,7 @@ fn convert_output(out: &Output) -> Result, String> { let file_name = line.split_off(FIRST_LOAD_PREFIX.len()); files.push(file_name.trim().to_string()); } else if line.ends_with(RELOAD_SUFFIX) { - line.split_off(line_len - RELOAD_SUFFIX.len()); + line.truncate(line_len - RELOAD_SUFFIX.len()); let file_name = line.split_off(PREFIX.len()); files.push(file_name.trim().to_string()); }