diff --git a/src/config.rs b/src/config.rs index b2c679e..31e72a1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,7 +14,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use structopt::StructOpt; use tokio::sync::mpsc::{channel, Receiver, Sender}; -use tokio::sync::{Mutex, MutexGuard}; +use tokio::sync::Mutex; #[derive(Debug, StructOpt)] #[structopt(name = "fren", about = "Friend Bot")] @@ -104,14 +104,6 @@ impl GlobalData { picox: AlbumManager::new(cfg.picox.api_base_url, &cfg.picox.token), }) } - - pub async fn reload(&mut self) -> Result<(), Error> { - let cfg = BotConfig::new(&self.args.cfg_path)?; - - self.cfg = cfg; - - Ok(()) - } } impl TypeMapKey for GlobalData { diff --git a/src/discord/admin.rs b/src/discord/admin.rs index 2ad0fee..2ad0c1d 100644 --- a/src/discord/admin.rs +++ b/src/discord/admin.rs @@ -1,4 +1,6 @@ use crate::config::BotConfig; +use crate::discord::Context; +use crate::error::Error; use crate::inventory::ItemType; use crate::models::api_key::Apikey; use crate::models::lil_fren::{ @@ -7,69 +9,30 @@ use crate::models::lil_fren::{ }; use crate::models::task::Task; use crate::user::User; -use crate::{command, group, GlobalData}; use json::JsonValue; -use serenity::all::{CreateAttachment, CreateMessage, FormattedTimestamp, FormattedTimestampStyle}; -use serenity::client::Context; -use serenity::framework::standard::{Args, CommandResult}; -use serenity::model::channel::Message; -use serenity::model::misc::EmojiIdentifier; -use serenity::model::prelude::UserId; -use serenity::model::Timestamp; -use serenity::utils::MessageBuilder; +use log::info; +use poise::serenity_prelude::{ + Attachment, CreateAttachment, CreateMessage, EmojiIdentifier, FormattedTimestamp, + FormattedTimestampStyle, MessageBuilder, Timestamp, UserId, +}; use std::borrow::Cow; -#[group] -#[commands( - reload, - dump_db, - load_db, - add_key, - debug_buddy, - draw_buddy_states, - op_give, - list_tasks -)] -pub struct ADMIN; - -pub fn is_admin(user_id: &UserId, cfg: &BotConfig) -> bool { - cfg.admins.contains(user_id) +pub async fn is_admin(ctx: Context<'_>) -> Result { + Ok(ctx.data().cfg.admins.contains(&ctx.author().id)) } -#[command] -async fn reload(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); - - if !is_admin(&msg.author.id, &global_data.cfg) { - return Ok(()); - } - - global_data.reload().await?; - msg.reply(&ctx.http, "Reload done ;)").await?; - - Ok(()) -} - -#[command] -async fn dump_db(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); - - if !is_admin(&msg.author.id, &global_data.cfg) { - return Ok(()); - } - - let db_dump = global_data.db.dump_db()?; +#[poise::command(prefix_command, category = "Admin", check = "is_admin")] +pub async fn dump_db(ctx: Context<'_>) -> Result<(), Error> { + let db_dump = ctx.data().db.dump_db()?; let output = db_dump.pretty(4); - msg.author + ctx.author() .id - .create_dm_channel(&ctx.http) + .create_dm_channel(ctx.http()) .await? .send_message( - &ctx.http, + ctx.http(), CreateMessage::new() .content("The current DB state") .add_file(CreateAttachment::bytes( @@ -82,67 +45,56 @@ async fn dump_db(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { Ok(()) } -#[command] -async fn load_db(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); +#[poise::command(prefix_command, category = "Admin", check = "is_admin")] +pub async fn load_db( + ctx: Context<'_>, + #[description = "DB import file"] attachment: Attachment, +) -> Result<(), Error> { + let db_bytes = attachment.download().await?; + let db_string = String::from_utf8(db_bytes).unwrap(); - if !is_admin(&msg.author.id, &global_data.cfg) { - return Ok(()); - } + let json_value: JsonValue = match json::parse(&db_string) { + Ok(v) => v, + Err(e) => { + ctx.reply(format!("Error parsing json: {}", e)).await?; + return Ok(()); + } + }; - if let Some(attachment) = msg.attachments.first() { - let db_bytes = attachment.download().await?; - let db_string = String::from_utf8(db_bytes)?; - - let json_value: JsonValue = match json::parse(&db_string) { - Ok(v) => v, - Err(e) => { - msg.reply(&ctx.http, format!("Error parsing json: {}", e)) - .await?; - return Ok(()); - } - }; - - match global_data.db.import_db(json_value) { - Ok(_) => { - msg.reply(&ctx.http, "Database imported successfully") - .await? - } - Err(err) => { - msg.reply(&ctx.http, format!("Error importing db: {}", err)) - .await?; - return Ok(()); - } - }; - } + info!("Importing DB from user supplied file"); + match ctx.data().db.import_db(json_value) { + Ok(_) => ctx.reply("Database imported successfully").await?, + Err(err) => { + ctx.reply(format!("Error importing db: {}", err)).await?; + return Ok(()); + } + }; Ok(()) } -#[command] -async fn add_key(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); - - if !is_admin(&msg.author.id, &global_data.cfg) { - return Ok(()); - } - - let (api_key, key) = if args.len() == 1 { - let user_id = args.parse::()?; - let user = user_id.to_user(&ctx.http).await?; +#[poise::command(prefix_command, category = "Admin", check = "is_admin")] +pub async fn add_key( + ctx: Context<'_>, + #[description = "User to make a key for"] user_id: Option, +) -> Result<(), Error> { + let (api_key, key) = if let Some(user_id) = user_id { + let user = user_id.to_user(&ctx.http()).await?; Apikey::new(&format!("{}'s Key", user.name), Some(user_id)) } else { - Apikey::new(&format!("{}'s Key", msg.author.name), Some(msg.author.id)) + Apikey::new( + &format!("{}'s Key", ctx.author().name), + Some(ctx.author().id), + ) }; - global_data.db.insert::(api_key.clone())?; + info!("Adding key '{}'", api_key.name); + ctx.data().db.insert::(api_key.clone())?; - let dm = msg.author.create_dm_channel(&ctx.http).await?; + let dm = ctx.author().create_dm_channel(&ctx.http()).await?; dm.say( - &ctx.http, + ctx.http(), format!("Key '{}' added. Api Key: {}", api_key.name, key), ) .await?; @@ -150,108 +102,76 @@ async fn add_key(ctx: &Context, msg: &Message, args: Args) -> CommandResult { Ok(()) } -#[command] -#[description("Check little buddy stats")] -async fn debug_buddy(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); - - if !is_admin(&msg.author.id, &global_data.cfg) { - return Ok(()); - } - - let lil_fren = LilFren::get_lil_fren(&global_data.db)?; +#[poise::command(prefix_command, category = "Admin", check = "is_admin")] +pub async fn debug_buddy(ctx: Context<'_>) -> Result<(), Error> { + let lil_fren = LilFren::get_lil_fren(&ctx.data().db)?; if let Some(lil_fren) = lil_fren { if lil_fren.is_alive() == AliveState::Alive { - msg.reply( - &ctx.http, - format!( - "Hunger: {} Thirst: {} Entertainment: {} Smarts: {} Metabolism: {} State: {:?}", - lil_fren.hunger, - lil_fren.thirst, - lil_fren.entertainment, - lil_fren.smarts, - lil_fren.metabolism, - lil_fren.state - ), - ) + ctx.reply(format!( + "Hunger: {} Thirst: {} Entertainment: {} Smarts: {} Metabolism: {} State: {:?}", + lil_fren.hunger, + lil_fren.thirst, + lil_fren.entertainment, + lil_fren.smarts, + lil_fren.metabolism, + lil_fren.state + )) .await?; } else { - msg.reply(&ctx.http, "Lil buddy is dead!").await?; + ctx.reply("Lil buddy is dead!").await?; } } else { - msg.reply(&ctx.http, "Sorry no little buddy found!").await?; + ctx.reply("Sorry no little buddy found!").await?; } Ok(()) } -#[command] -#[description("Check little buddy stats")] -async fn draw_buddy_states(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let emoji = args.parse::()?; +#[poise::command(prefix_command, category = "Admin", check = "is_admin")] +pub async fn draw_buddy_states( + ctx: Context<'_>, + #[description = "Emoji to use for debugging"] emoji: EmojiIdentifier, +) -> Result<(), Error> { + let emoji = ctx.guild_id().unwrap().emoji(ctx.http(), emoji.id).await?; - let emoji = msg.guild_id.unwrap().emoji(&ctx.http, emoji.id).await?; - - msg.reply(&ctx.http, draw_standing(&emoji)).await?; - msg.reply(&ctx.http, draw_dancing(&emoji)).await?; - msg.reply(&ctx.http, draw_frankenstein(&emoji)).await?; - msg.reply(&ctx.http, draw_sick(&emoji)).await?; - msg.reply(&ctx.http, draw_magic(&emoji)).await?; - msg.reply(&ctx.http, draw_resonance_cascade(&emoji)).await?; - msg.reply(&ctx.http, draw_tax_fraud(&emoji)).await?; - msg.reply(&ctx.http, draw_sleep(&emoji)).await?; - msg.reply(&ctx.http, draw_mining(&emoji)).await?; - msg.reply(&ctx.http, draw_gone()).await?; + ctx.reply(draw_standing(&emoji)).await?; + ctx.reply(draw_dancing(&emoji)).await?; + ctx.reply(draw_frankenstein(&emoji)).await?; + ctx.reply(draw_sick(&emoji)).await?; + ctx.reply(draw_magic(&emoji)).await?; + ctx.reply(draw_resonance_cascade(&emoji)).await?; + ctx.reply(draw_tax_fraud(&emoji)).await?; + ctx.reply(draw_sleep(&emoji)).await?; + ctx.reply(draw_mining(&emoji)).await?; + ctx.reply(draw_gone()).await?; Ok(()) } -#[command] -#[description("Hey can I get op? I'm from planet minecraft")] -async fn op_give(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); +#[poise::command(prefix_command, category = "Admin", check = "is_admin")] +pub async fn op_give( + ctx: Context<'_>, + #[description = "Item to give"] + #[rest] + item: ItemType, +) -> Result<(), Error> { + info!("'{}' has been given a {:?}", ctx.author().name, item); + User::give_item(&ctx.data().db, ctx.author().id, item, 1, None)?; - if args.is_empty() { - msg.reply(&ctx.http, "You need to select an item to give") - .await?; - return Ok(()); - } - - if !is_admin(&msg.author.id, &global_data.cfg) { - return Ok(()); - } - - let msg_content = args.rest().to_lowercase(); - let item = match msg_content.parse::() { - Ok(i) => i, - Err(_) => { - msg.reply(&ctx.http, "I don't know what the heck that is tbh.") - .await?; - return Ok(()); - } - }; - - User::give_item(&global_data.db, msg.author.id, item, 1, None)?; - - msg.reply( - &ctx, - format!("[Console] Op has given {} 1 {}", msg.author.name, item), - ) + ctx.reply(format!( + "[Console] Op has given {} 1 {}", + ctx.author().name, + item + )) .await?; Ok(()) } -#[command] -#[description("List scheduled tasks")] -async fn list_tasks(ctx: &Context, msg: &Message) -> CommandResult { - let data = ctx.data.read().await; - let global_data = data.get::().unwrap(); - - let tasks: Vec = global_data.db.filter(|_, _task: &Task| true)?.collect(); +#[poise::command(prefix_command, category = "Admin")] +pub async fn list_tasks(ctx: Context<'_>) -> Result<(), Error> { + let tasks: Vec = ctx.data().db.filter(|_, _task: &Task| true)?.collect(); let mut resp = MessageBuilder::new(); @@ -265,7 +185,7 @@ async fn list_tasks(ctx: &Context, msg: &Message) -> CommandResult { )); } - msg.reply(&ctx.http, resp.build()).await?; + ctx.reply(resp.build()).await?; Ok(()) } diff --git a/src/discord/mod.rs b/src/discord/mod.rs index bde2d71..4fcaceb 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -1,3 +1,4 @@ +mod admin; mod celeryman; use crate::config::GlobalData; @@ -5,7 +6,7 @@ use crate::error::Error; use poise::{serenity_prelude as serenity, FrameworkOptions}; -type Context<'a> = poise::Context<'a, GlobalData, Error>; +pub type Context<'a> = poise::Context<'a, GlobalData, Error>; pub async fn run_bot(global_data: GlobalData) { let framework_options: FrameworkOptions = poise::FrameworkOptions { @@ -20,6 +21,14 @@ pub async fn run_bot(global_data: GlobalData) { celeryman::nudetayne(), celeryman::celeryman(), celeryman::tayne(), + admin::dump_db(), + admin::load_db(), + admin::add_key(), + admin::debug_buddy(), + admin::draw_buddy_states(), + admin::debug_buddy(), + admin::op_give(), + admin::list_tasks(), ], ..Default::default() }; diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index 4ba2507..31620f4 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -20,22 +20,35 @@ impl Display for InventoryError { } } +impl std::error::Error for InventoryError {} + #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] #[serde(tag = "type", content = "data")] pub enum ItemData { Nft(String), } -#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Copy)] +#[derive( + Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Copy, poise::ChoiceParameter, +)] pub enum ItemType { + #[name = "Cancel Insurance"] CancelInsurance, + #[name = "The Concept of Love"] TheConceptOfLove, + #[name = "Good Fortune"] GoodFortune, + #[name = "NFT"] Nft, + #[name = "License to be Horny"] LicenseToBeHorny, + #[name = "Kill Gun"] KillGun, + #[name = "Cancel Ray"] CancelRay, + #[name = "Helmet"] Helmet, + #[name = "Tactical Nuke"] TacticalNuke, } diff --git a/src/models/mod.rs b/src/models/mod.rs index 959a1a0..034a1ff 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -4,4 +4,4 @@ pub mod insult_compliment; pub mod lil_fren; pub mod motivation; pub mod random; -//pub mod task; +pub mod task; diff --git a/src/models/task.rs b/src/models/task.rs index fbcb906..5d55e1d 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -1,14 +1,15 @@ use crate::config::GlobalData; //use crate::discord::shop::restock_shop; +use crate::discord::Context; use crate::error::Error; use crate::models::birthday::BirthdayEntry; use crate::models::insult_compliment::{RandomResponseTemplate, ResponseType}; use chrono::{Days, Duration, TimeZone, Timelike, Utc}; use j_db::database::Database; use j_db::model::JdbModel; -use serde::{Deserialize, Serialize}; +use log::{error, info}; use poise::serenity_prelude::all::Mentionable; -use poise:: +use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] pub enum TaskType { @@ -79,14 +80,11 @@ impl Task { Ok(()) } - pub async fn create_reoccurring_tasks(ctx: &Context) -> Result<(), Error> { - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); - - Task::add_task(&global_data.db, TaskType::CheckBirthdays, Utc::now())?; - Task::add_task(&global_data.db, TaskType::RestockShop, Utc::now())?; + pub async fn create_reoccurring_tasks(ctx: Context<'_>) -> Result<(), Error> { + Task::add_task(&ctx.data().db, TaskType::CheckBirthdays, Utc::now())?; + Task::add_task(&ctx.data().db, TaskType::RestockShop, Utc::now())?; Task::add_task( - &global_data.db, + &ctx.data().db, TaskType::HandleReload, Utc::now() + Duration::hours(1), )?; @@ -94,63 +92,54 @@ impl Task { Ok(()) } - pub async fn run_tasks(ctx: &Context) -> Result<(), Error> { - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); - - let active_tasks: Vec = global_data + pub async fn run_tasks(ctx: Context<'_>) -> Result<(), Error> { + let active_tasks: Vec = ctx + .data() .db - .filter(|_, task: &Task| task.time < chrono::Utc::now())? + .filter(|_, task: &Task| task.time < Utc::now())? .collect(); for task in active_tasks { match task.task_type { TaskType::RemoveRole { user_id, role_id } => { - let user = global_data - .cfg - .guild_id - .member(&ctx.http, user_id) - .await - .unwrap(); + let user = ctx.data().cfg.guild_id.member(ctx.http(), user_id).await?; - println!("Removing role {} from {}", role_id, user.display_name()); - user.remove_role(&ctx.http, role_id).await.unwrap(); + info!("Removing role {} from {}", role_id, user.display_name()); + user.remove_role(ctx.http(), role_id).await?; } TaskType::CheckBirthdays => { - println!("Checking Birthdays"); + info!("Checking Birthdays"); let todays_birthdays = BirthdayEntry::todays_birthdays( - &global_data.db, + &ctx.data().db, chrono::Utc::now().date_naive(), - ) - .unwrap(); + )?; for birth in todays_birthdays { - if let Ok(user) = global_data + if let Ok(user) = ctx + .data() .cfg .guild_id - .member(&ctx.http, birth.discord_id) + .member(ctx.http(), birth.discord_id) .await { - global_data + ctx.data() .cfg .announcement_channel - .say(&ctx.http, format!("Happy birthday {}!", user.mention())) - .await - .unwrap(); + .say(ctx.http(), format!("Happy birthday {}!", user.mention())) + .await?; let compliment = RandomResponseTemplate::get_random_response( - &global_data.db, + &ctx.data().db, ResponseType::Compliment, user.display_name(), - ) - .unwrap() - .unwrap(); - global_data + )? + .unwrap_or("I couldn't think of anything funny tbh...".to_string()); + + ctx.data() .cfg .announcement_channel - .say(&ctx.http, compliment) - .await - .unwrap(); + .say(ctx.http(), compliment) + .await?; } } @@ -166,44 +155,32 @@ impl Task { .unwrap(); Task::add_task( - &global_data.db, + &ctx.data().db, TaskType::CheckBirthdays, next_check.with_timezone(&Utc), )?; } TaskType::HandleReload => { - println!("Reloading config..."); - let res = global_data.reload().await; - - match res { - Ok(_) => println!("Finished reloading config!"), - Err(err) => println!("Error reloading config: {:?}", err), - } - - Task::add_task( - &global_data.db, - TaskType::HandleReload, - Utc::now() + Duration::hours(1), - )?; + // Do nothing for now } TaskType::RestockShop => { - println!("Restocking Shop..."); - let res = restock_shop(ctx, global_data).await; + info!("Restocking Shop..."); + //let res = restock_shop(ctx, global_data).await; - match res { - Ok(_) => println!("Finished restocking shop!"), - Err(err) => println!("Error restocking shop: {:?}", err), - } + //match res { + // Ok(_) => info!("Finished restocking shop!"), + // Err(err) => error!("Error restocking shop: {:?}", err), + // } Task::add_task( - &global_data.db, + &ctx.data().db, TaskType::RestockShop, Utc::now() + Duration::hours(1), )?; } } - let _ = global_data.db.remove::(task.id().unwrap()).is_ok(); + let _ = ctx.data().db.remove::(task.id().unwrap()).is_ok(); } Ok(())