From 98b88b1778ba12813eef31e81d0c6d45c67e028a Mon Sep 17 00:00:00 2001 From: s0lst1ce Date: Tue, 14 Sep 2021 19:09:50 +0200 Subject: [PATCH 1/2] WIP --- .gitignore | 1 + Cargo.toml | 1 + src/commands/misc.rs | 20 -------------------- src/main.rs | 19 ++++++------------- src/tweak.rs | 24 ++++++++++++++++++++++++ src/utils.rs | 3 +++ 6 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 src/tweak.rs diff --git a/.gitignore b/.gitignore index 0e948e4..056ad16 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ Cargo.lock .env *.log +*.code-workspace diff --git a/Cargo.toml b/Cargo.toml index 0019931..6ce98d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ tracing = "0.1.23" tracing-subscriber = "0.2" chrono = "0.4" async-trait = "0.1" +db-adapter = {git="https://github.com/Botanism/rust-db-adapter", branch="main"} [dependencies.sqlx] version = "0.5" diff --git a/src/commands/misc.rs b/src/commands/misc.rs index 87570cc..ddb1e8f 100644 --- a/src/commands/misc.rs +++ b/src/commands/misc.rs @@ -62,26 +62,6 @@ async fn clear(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { }, }; - //deletion with only an upper limit - /*if number.is_some() && args.remaining() == 0 { - return Ok(bulk_delete(ctx, &msg.channel_id, { - let mut limit = number.unwrap(); - let mut messages = Vec::with_capacity(limit as usize); - while limit > 0 { - let chunk = 100.min(limit); - limit -= chunk; - messages.append( - &mut msg - .channel_id - .messages(ctx, |history| history.limit(chunk)) - .await?, - ) - } - messages - }) - .await?); - }*/ - //duration criteria? let since = if args.remaining() > 0 { match parse_duration(args.current().unwrap()) { diff --git a/src/main.rs b/src/main.rs index a641ee1..df080e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ mod checks; mod commands; -mod db; mod tweak; mod utils; +use db_adapter::establish_connection; use std::borrow::Cow; use std::{collections::HashSet, env, sync::Arc}; -use sqlx::{query, MySqlPool}; +use sqlx::MySqlPool; use crate::utils::*; use serenity::{ @@ -17,14 +17,8 @@ use serenity::{ standard::{macros::hook, DispatchError}, StandardFramework, }, - http::{CacheHttp, Http}, - model::{ - channel::Message, - event::ResumedEvent, - gateway::Ready, - guild::{Guild, Member}, - id::{GuildId, UserId}, - }, + http::Http, + model::{channel::Message, event::ResumedEvent, gateway::Ready, guild::Guild, id::UserId}, prelude::*, }; @@ -32,7 +26,6 @@ use tracing::{error, info}; use tracing_subscriber::{EnvFilter, FmtSubscriber}; use commands::{dev::*, misc::*}; -use db::insert_new_guild; pub struct ShardManagerContainer; impl TypeMapKey for ShardManagerContainer { @@ -58,10 +51,9 @@ impl EventHandler for Handler { //sent when a a guild's data is sent to us (one) async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) { if is_new { - //move data access into its own block to free access sooner let data = ctx.data.read().await; let conn = &data.get::().unwrap().0; - insert_new_guild(&conn, guild.id).await; + insert_new_guild(&conn, guild.id).await.unwrap(); } } async fn ready(&self, _: Context, ready: Ready) { @@ -106,6 +98,7 @@ async fn dispatch_error_hook(ctx: &Context, msg: &Message, error: DispatchError) async fn main() { // This will load the environment variables located at `./.env`, relative to dotenv::dotenv().expect("Failed to load .env file"); + let pool = establish_connection().await; // Initialize the logger to use environment variables. // diff --git a/src/tweak.rs b/src/tweak.rs new file mode 100644 index 0000000..6953791 --- /dev/null +++ b/src/tweak.rs @@ -0,0 +1,24 @@ +use async_trait::async_trait; +use serenity::prelude::*; + +#[async_trait] +///Used for command groups that need to be configured for each server. +///The whole process is done through the `config` function. +pub trait Configurable { + ///This handles the configuration of the extension within discord. + async fn config(ctx: &Context) -> SerenityError; +} + +pub struct IsConfigured { + pub poll: bool, + pub slaps: bool, +} + +impl Default for IsConfigured { + fn default() -> Self { + IsConfigured { + poll: false, + slaps: false, + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 2cef849..a387d05 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -184,6 +184,9 @@ impl Display for DurationParseError { } impl std::error::Error for DurationParseError {} +///Parses a String into a duration using its own convention. +///XdYhZmAs would be parsed a duration of X days Y hours +///Z minutes and A seconds pub fn parse_duration>(string: S) -> Result { const TIME_IDENTIFIERS: [char; 4] = ['d', 'h', 'm', 's']; const TIME_VALUES: [u64; 4] = [86400, 3600, 60, 1]; From a0e5fcfba7ba148d7fd208f6951d33779cf8ddea Mon Sep 17 00:00:00 2001 From: s0lst1ce Date: Wed, 17 Nov 2021 18:51:46 +0100 Subject: [PATCH 2/2] port to poise --- Cargo.toml | 24 ++-- src/commands/dev.rs | 7 +- src/commands/misc.rs | 267 ++++++++----------------------------------- src/commands/mod.rs | 4 +- src/db.rs | 9 -- src/main.rs | 213 ++++++++++++++++++---------------- src/utils.rs | 228 ++++++++++++++++++------------------ 7 files changed, 303 insertions(+), 449 deletions(-) delete mode 100644 src/db.rs diff --git a/Cargo.toml b/Cargo.toml index 6ce98d8..6db7323 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,26 +2,30 @@ name = "botanist" version = "0.1.0" authors = ["s0lst1ce "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] dotenv = "0.15" tracing = "0.1.23" -tracing-subscriber = "0.2" +tracing-subscriber = {version="0.3", features=["env-filter"]} chrono = "0.4" async-trait = "0.1" -db-adapter = {git="https://github.com/Botanism/rust-db-adapter", branch="main"} +db-adapter = {path="../db-adapter"} +poise = { git = "https://github.com/kangalioo/poise", branch = "develop", features = ["collector"] } -[dependencies.sqlx] -version = "0.5" -features = ["macros", "runtime-tokio-rustls", "mysql"] +[dependencies.tokio] +version = "1" +features = ["macros", "rt-multi-thread", "signal"] [dependencies.serenity] version = "0.10" -features = ["framework", "standard_framework", "rustls_backend", "cache"] +default-features = false +features = ["model", "rustls_backend", "cache", "collector", "client"] -[dependencies.tokio] -version = "1.0" -features = ["macros", "rt-multi-thread", "signal"] \ No newline at end of file +#[patch.crates-io] +#serenity = { git = "https://github.com/kangalioo/serenity", branch = "poise-tailored" } + +[patch.crates-io] +serenity = {git="https://github.com/serenity-rs/serenity", branch="next"} \ No newline at end of file diff --git a/src/commands/dev.rs b/src/commands/dev.rs index d5eea9c..2b51d8f 100644 --- a/src/commands/dev.rs +++ b/src/commands/dev.rs @@ -1,16 +1,17 @@ use crate::utils::*; use crate::ShardManagerContainer; -use serenity::model::prelude::*; -use serenity::prelude::*; use serenity::{ client::bridge::gateway::ShardId, framework::standard::{ macros::{command, group}, CommandError, CommandResult, }, + model::prelude::*, + prelude::*, }; use std::env; use tracing::error; + #[group] #[commands(shutdown, latency, log)] struct Development; @@ -103,7 +104,7 @@ async fn log(ctx: &Context, msg: &Message) -> CommandResult { Ok(path) => path, Err(err) => { error!("{:#}", err); - let error = BotError::new( + let error = BotErrorReport::new( "LOG_FILE is missing", Some(BotErrorKind::EnvironmentError), Some(msg), diff --git a/src/commands/misc.rs b/src/commands/misc.rs index ddb1e8f..f0b3b03 100644 --- a/src/commands/misc.rs +++ b/src/commands/misc.rs @@ -1,118 +1,67 @@ -use crate::utils::*; +use crate::{utils::*, Context, Result}; use chrono::offset::Utc; -use serenity::framework::standard::{ - macros::{command, group}, - ArgError, Args, CommandResult, +use poise::{ + command, + serenity::{ + futures::StreamExt, + model::{ + channel::Message, + id::{ChannelId, UserId}, + }, + utils::ArgumentConvert, + }, }; -use serenity::futures::StreamExt; -use serenity::model::prelude::*; -use serenity::model::user::OnlineStatus; -use serenity::prelude::*; -use serenity::utils::Parse; -use std::collections::HashMap; -use std::fmt::Display; use std::time::{SystemTime, UNIX_EPOCH}; use tracing::info; -#[group] -#[commands(ping, provoke_error, clear, status)] -struct Misc; - -#[command] -// Limit command usage to guilds. -#[only_in(guilds)] -async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - msg.channel_id.say(&ctx.http, "Pong!").await?; - Ok(()) +/// TODO: remove +/// this is only supposed to be used to test out error reporting +#[command(slash_command)] +pub async fn provoke_error(_ctx: Context<'_>) -> Result<()> { + Err(BotError::EnvironmentError) } -#[command] -async fn provoke_error(ctx: &Context, msg: &Message) -> CommandResult { - report_error( - ctx, - &msg.channel_id, - &BotError::new( - "the dev is dumb", - Some(BotErrorKind::EnvironmentError), - Some(&msg), - ), - ) - .await; +/// Check if the bot is online and responsive +#[command(slash_command)] +pub async fn ping(ctx: Context<'_>) -> Result<()> { + // Limit command usage to guilds. + in_guild!(ctx)?; + + ctx.say("Pong!").await?; + Ok(()) } -#[command] -#[aliases(clean)] -#[min_args(1)] -async fn clear(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - //upper limit? - let number = match args.parse::() { - Ok(number) => { - args.advance(); - Some(number) - } - Err(err) => match err { - //no upper limit or the number was not correctly formed - ArgError::Parse(_) => None, - ArgError::Eos => { - //since we have `#[min_args(1)]` - unreachable!() - } - _ => unreachable!(), - }, - }; - +#[command(slash_command, aliases("clean"))] +//#[min_args(1)] +/// Delete messages from the current channel +/// filters can be mixed as desired +pub async fn clear( + ctx: Context<'_>, + #[description = "The maximum number of messages to delete"] number: Option, + #[description = "The maximum age of the messages to be deleted. All messages more recent than the date given that also meet the other filters will be deleted"] + since: Option, + // TODO: find a way to support multiple users + #[description = "Only delete messages from these members"] who: Option, +) -> Result<()> { //duration criteria? - let since = if args.remaining() > 0 { - match parse_duration(args.current().unwrap()) { - Ok(duration) => { - args.advance(); - Some(duration) - } - //not having a duration by now doesn't mean the inputed arguments were wrong - //there can still be a combination of amount & members - Err(_err) => None, - } - } else { - None - }; + let since = since.map(|str| parse_duration(str)).transpose()?; //member criteria? - let mut members: Vec = Vec::with_capacity(args.remaining()); - while args.remaining() > 0 { - members.push( - match Parse::parse(ctx, msg, args.current().unwrap()).await { - Ok(m) => m, - Err(parse_err) => { - report_error( - ctx, - &msg.channel_id, - &BotError::new( - format!( - "{:#} cannot be understood as a member of this server.", - args.current().unwrap() - ) - .as_str(), - Some(BotErrorKind::ModelParsingError), - Some(msg), - ), - ) - .await; - info!("Couldn't correclty parse {:?}", args.raw()); - return Err(From::from(BotanistError::SerenityUserIdParseError( - parse_err, - ))); - } - }, - ); - args.advance(); + let mut members: Vec = Vec::new(); + if let Some(member) = who { + members.push(member) } + /*while args.remaining() > 0 { + members.push(ArgumentConvert::convert(ctx, msg, args.current().unwrap()).await?); + args.advance(); + }*/ //argument parsing is done -> we select the messages to be deleted let mut messages: Vec = Vec::new(); - let mut history = msg.channel_id.messages_iter(ctx).boxed(); + let mut history = ctx.channel_id().messages_iter(ctx.discord()).boxed(); let mut limit = if let Some(amount) = number { - if !members.is_empty() && !members.contains(&msg.author.id) { + if !members.is_empty() && !members.contains(&ctx.author().id) { amount } else { amount + 1 @@ -152,24 +101,7 @@ async fn clear(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { >= TWO_WEEKS { dbg!("the message was over two weeks old so we manually delete it"); - if let Err(err) = message.delete(ctx).await { - report_error( - ctx, - &msg.channel_id, - &BotError::new( - "Missing permissions to delete messages in this channel", - Some(BotErrorKind::MissingPermissions), - Some(&msg), - ), - ) - .await; - info!( - "missing permissions to delete messages in {:?}", - &msg.channel_id - ); - info!("{}", err); - return Err(From::from(err)); - }; + message.delete(ctx.discord()).await? } else { messages.push(message); } @@ -181,118 +113,19 @@ async fn clear(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { } info!("trying to delete {:?} messages", messages.len()); - match bulk_delete(ctx, &msg.channel_id, messages).await { - Ok(_) => return Ok(()), - Err(err) => { - report_error( - ctx, - &msg.channel_id, - &BotError::new( - "Missing permissions to delete messages in this channel", - Some(BotErrorKind::MissingPermissions), - Some(&msg), - ), - ) - .await; - info!( - "missing permissions to delete messages in {:?}", - &msg.channel_id - ); - info!("{:?}", err); - return Err(From::from(BotanistError::Serenity(err))); - } - }; + bulk_delete(ctx, &ctx.channel_id(), messages).await } //bulks deletes messages, even if there are more than 100 //because of this the only possible error is missing permissions to delete the msgs -async fn bulk_delete( - ctx: &Context, - chan: &ChannelId, - messages: Vec, -) -> Result<(), SerenityError> { +async fn bulk_delete(ctx: Context<'_>, chan: &ChannelId, messages: Vec) -> Result<()> { if messages.is_empty() { return Ok(()); } else { for chunk in messages.chunks(100) { - chan.delete_messages(ctx, chunk).await?; - } - } - - Ok(()) -} - -struct MembersStatus { - online: usize, - dnd: usize, - idle: usize, - offline: usize, -} - -impl MembersStatus { - fn new(online: usize, dnd: usize, idle: usize, offline: usize) -> MembersStatus { - MembersStatus { - online, - dnd, - idle, - offline, + chan.delete_messages(ctx.discord(), chunk).await?; } } -} -impl Display for MembersStatus { - fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { - let max = self.online.max(self.dnd.max(self.idle.max(self.offline))); - write!(f, "**{:> for MembersStatus { - fn from(map: &HashMap) -> Self { - let mut online = 0; - let mut idle = 0; - let mut dnd = 0; - let mut offline = 0; - dbg!(&map); - for (_, presence) in map.iter() { - match presence.status { - OnlineStatus::Online => online += 1, - OnlineStatus::Idle => idle += 1, - OnlineStatus::DoNotDisturb => dnd += 1, - OnlineStatus::Offline => offline += 1, - OnlineStatus::Invisible => offline += 1, - _ => (), //discord may add new statuses without notice - } - } - MembersStatus::new(online, dnd, idle, offline) - } -} - -#[command] -#[only_in(guilds)] -async fn status(ctx: &Context, msg: &Message) -> CommandResult { - let guild = msg.guild(ctx).await.expect("missing guild in cache"); - let owned_name = guild.owner_id.to_user(ctx).await.unwrap(); - //we deduce the age of the guild through its id (snowflake) - let creation_date = guild.id.created_at(); - let mut roles = String::new(); - for (role_id, _) in &guild.roles { - roles.push_str(role_id.mention().to_string().as_str()) - } - let members = MembersStatus::from(&guild.presences); - msg.channel_id - .send_message(ctx, |m| { - m.embed(|e| { - e.color(7506394).description(format!( - "{:#} is owned by {:#} and was created on {:#}. Since then {:#} members joined.", - guild.name, owned_name, creation_date, guild.member_count - )).field("Roles", roles, true).field("Members", members, true); if let Some(url) = guild.icon_url(){e.thumbnail(url);}; - e - }) - }) - .await?; Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f8fee92..834eaf3 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,3 @@ -pub mod dev; +//pub mod dev; pub mod misc; -pub mod poll; +//pub mod poll; diff --git a/src/db.rs b/src/db.rs deleted file mode 100644 index 1f9be45..0000000 --- a/src/db.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serenity::model::id::GuildId; -use sqlx::{query, MySqlPool, Result}; - -pub async fn insert_new_guild(conn: &MySqlPool, id: GuildId) -> Result<()> { - query!("INSERT INTO guilds (id) values (?)", u64::from(id)) - .execute(conn) - .await?; - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index df080e1..2c17aed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,104 +1,109 @@ -mod checks; mod commands; -mod tweak; mod utils; -use db_adapter::establish_connection; -use std::borrow::Cow; -use std::{collections::HashSet, env, sync::Arc}; +use db_adapter::guild::{GuildConfig, GuildConfigBuilder}; +use db_adapter::{establish_connection, PgPool}; +use poise::PrefixFrameworkOptions; -use sqlx::MySqlPool; +use std::{collections::HashSet, env}; use crate::utils::*; -use serenity::{ - async_trait, - client::bridge::gateway::ShardManager, - framework::{ - standard::{macros::hook, DispatchError}, - StandardFramework, +use poise::{ + serenity::{ + async_trait, + http::Http, + model::{event::ResumedEvent, gateway::Ready, guild::Guild}, + prelude::*, }, - http::Http, - model::{channel::Message, event::ResumedEvent, gateway::Ready, guild::Guild, id::UserId}, - prelude::*, + ErrorContext, }; use tracing::{error, info}; use tracing_subscriber::{EnvFilter, FmtSubscriber}; -use commands::{dev::*, misc::*}; -pub struct ShardManagerContainer; - -impl TypeMapKey for ShardManagerContainer { - type Value = Arc>; +//Shared data contained in the Context +pub struct GlobalData { + pub pool: PgPool, } -pub struct BotId(UserId); - -impl TypeMapKey for BotId { - type Value = BotId; +struct DBPool; +impl TypeMapKey for DBPool { + type Value = PgPool; } -pub struct DBConn(MySqlPool); - -impl TypeMapKey for DBConn { - type Value = DBConn; -} +//Context type passed to every command +pub type Context<'a> = poise::Context<'a, GlobalData, BotError>; +pub type Result = std::result::Result; struct Handler; #[async_trait] impl EventHandler for Handler { //sent when a a guild's data is sent to us (one) - async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) { + async fn guild_create( + &self, + ctx: poise::serenity::prelude::Context, + guild: Guild, + is_new: bool, + ) { if is_new { let data = ctx.data.read().await; - let conn = &data.get::().unwrap().0; - insert_new_guild(&conn, guild.id).await.unwrap(); + let conn = &data.get::().unwrap(); + GuildConfig::new(*conn, GuildConfigBuilder::new(guild.id)) + .await + .unwrap(); } } - async fn ready(&self, _: Context, ready: Ready) { + async fn ready(&self, _: poise::serenity::prelude::Context, ready: Ready) { info!("Connected as {}", ready.user.name); } - async fn resume(&self, _: Context, _: ResumedEvent) { + async fn resume(&self, _: poise::serenity::prelude::Context, _: ResumedEvent) { info!("Connection resumed"); } } -#[hook] -async fn dispatch_error_hook(ctx: &Context, msg: &Message, error: DispatchError) { +async fn on_error<'a>(err_ctx: ErrorContext<'_, GlobalData, BotError>, error: BotError) { error!("{:?}", error); - let (description, kind): (Cow<'static, str>, Option) = match error { - DispatchError::CheckFailed(_, reason) => (Cow::Borrowed("a check failed"), None), - DispatchError::OnlyForOwners => ( - Cow::Borrowed("This commands requires the `runner` privilege, which you are missing."), - None, - ), - DispatchError::NotEnoughArguments { min, given } => ( - Cow::Owned(format!( - "You only provided {:#} arguments when the command expects a minimum of {:#}.", - given, min - )), - Some(BotErrorKind::IncorrectNumberOfArgs), - ), - _ => ( - Cow::Borrowed("An undocumented error occured with the command you just used."), - Some(BotErrorKind::UnexpectedError), - ), + use poise::ErrorContext::*; + let ctx = match err_ctx { + Command(ctx) => ctx.ctx(), + _ => unimplemented!(), }; - report_error( + + report_error(ctx, error).await +} + +/// Show this help menu +#[poise::command(prefix_command, track_edits, slash_command)] +async fn help( + ctx: Context<'_>, + #[description = "Specific command to show help about"] command: Option, +) -> Result<()> { + poise::builtins::help( ctx, - &msg.channel_id, - &BotError::new(&description, kind, Some(msg)), + command.as_deref(), + "Made with love by Toude#6601", + poise::builtins::HelpResponseMode::Ephemeral, ) - .await; + .await?; + Ok(()) +} + +/// Register application commands in this guild or globally +/// +/// Run with no arguments to register in guild, run with argument "global" to register globally. +#[poise::command(prefix_command, hide_in_help)] +async fn register(ctx: Context<'_>, #[flag] global: bool) -> Result<()> { + poise::builtins::register_application_commands(ctx, global).await?; + + Ok(()) } #[tokio::main] async fn main() { // This will load the environment variables located at `./.env`, relative to dotenv::dotenv().expect("Failed to load .env file"); - let pool = establish_connection().await; // Initialize the logger to use environment variables. // @@ -110,11 +115,10 @@ async fn main() { tracing::subscriber::set_global_default(subscriber).expect("Failed to start the logger"); - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - + let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + //TODO: is this the right way to do it? let http = Http::new_with_token(&token); - - // We will fetch your bot's owners and id + // We fetch the bot's owners and id let (owners, bot_id) = match http.get_current_application_info().await { Ok(info) => { let mut owners = HashSet::new(); @@ -129,49 +133,64 @@ async fn main() { Err(why) => panic!("Could not access application info: {:?}", why), }; - let prefix = env::var("DISCORD_PREFIX").expect("Expected a prefix in the environment"); - - // Create the framework - let framework = StandardFramework::new() - .configure(|c| { - c.owners(owners) - .prefix(prefix.as_str()) - .on_mention(Some(bot_id)) - .with_whitespace(true) + let mut framework_builder = poise::Framework::::build() + .token(token) + .user_data_setup(move |_ctx, _ready, _framework| { + Box::pin(async move { + Ok(GlobalData { + pool: establish_connection().await, + }) + }) }) - .on_dispatch_error(dispatch_error_hook) - .group(&DEVELOPMENT_GROUP) - .group(&MISC_GROUP); - - let mut client = Client::builder(&token) - .framework(framework) - .event_handler(Handler) - .await - .expect("Err creating client"); - - //DB setup - let db_url = env::var("DATABASE_URL").expect("`DATABASE_URL` is not set"); - let pool = MySqlPool::connect(&db_url) - .await - .expect("Could not establish DB connection"); - - { - let mut data = client.data.write().await; - data.insert::(client.shard_manager.clone()); - data.insert::(BotId(bot_id)); - data.insert::(DBConn(pool)); - } + .options(poise::FrameworkOptions { + // configure framework here + prefix_options: PrefixFrameworkOptions { + prefix: Some( + env::var("DISCORD_PREFIX").expect("Expected a prefix in the environment"), + ), + + ..Default::default() + }, + owners, + on_error: |error, ctx| Box::pin(on_error(ctx, error)), + ..Default::default() + }); + + framework_builder = { + use commands::misc::{clear, ping, provoke_error}; + add_commands!( + framework_builder, + help, + register, + //clear, + ping, + provoke_error + ) + }; + + let framework = framework_builder.build().await.unwrap(); - let shard_manager = client.shard_manager.clone(); + dbg!(env::var("DISCORD_PREFIX").expect("Expected a prefix in the environment")); + //running the bot until an error occurs + if let Err(why) = framework.clone().start().await { + error!("Client error: {:?}", why); + } + + //halting the bot through INTERRUPT + let shard_manager = framework.shard_manager(); tokio::spawn(async move { tokio::signal::ctrl_c() .await .expect("Could not register ctrl+c handler"); shard_manager.lock().await.shutdown_all().await; }); +} - if let Err(why) = client.start().await { - error!("Client error: {:?}", why); - } +#[macro_export] +macro_rules! add_commands { + ($framework_builder:ident, $($command:path),*) => {{ + $($framework_builder = $framework_builder.command($command(), |f| f);)* + $framework_builder + }}; } diff --git a/src/utils.rs b/src/utils.rs index a387d05..9d2bd21 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,8 @@ -use serenity::{builder::CreateEmbed, model::prelude::*, prelude::*}; +use crate::Context; +use poise::{ + serenity::{builder::CreateEmbed, model::prelude::*, prelude::SerenityError}, + ArgumentParseError, SlashArgError, +}; use std::fmt::Display; use std::time::Duration; use tracing::{debug, error, info}; @@ -7,54 +11,53 @@ use tracing::{debug, error, info}; pub const TWO_WEEKS: i64 = 1209600; //Error handler //Logs the error and sends an embed error report on discord -pub async fn report_error<'a, 'b>( - ctx: &Context, - channel: &ChannelId, - error: &BotError<'a, 'b>, -) -> () { - debug!("The error report {:?} was sent in {:?}", error, channel); +pub async fn report_error(ctx: Context<'_>, error: BotError) -> () { + debug!( + "The error report {:?} was sent in {:?}", + error, + ctx.channel_id() + ); + + // /!\ I CURRENTLY BUILD THE EMBED TWICE BECAUSE OF A LIMITATION OF POISE let mut embed = CreateEmbed::default(); embed .color(16720951) - .description(error.description) - .title(match &error.kind { - None => "UNEXPECTED", - Some(kind) => kind.pretty_name(), - }); + //.description() + .title(&error.pretty_name()); let cause_user: Option<&User> = None; - if let Some(msg) = error.origin { - embed.timestamp(&msg.timestamp); - //embed.url(msg.link()); currently removed because it doesn't link correctly - let cause_user = Some(&msg.author); - embed.footer(|f| { - f.text(format!("caused by {:#}", cause_user.unwrap().name)) - .icon_url( - cause_user - .unwrap() - .avatar_url() - .unwrap_or(cause_user.unwrap().default_avatar_url()), - ) - }); - }; - if let Some(kind) = &error.kind { - embed.field("❓ How to fix this", format!("{:#}", kind), false); - }; + embed.timestamp(ctx.created_at()); + //embed.url(msg.link()); currently removed because it doesn't link correctly + let cause_user = ctx.author(); + embed.footer(|f| { + f.text(format!("caused by {:#}", cause_user.name)).icon_url( + cause_user + .avatar_url() + .unwrap_or(cause_user.default_avatar_url()), + ) + }); + + embed.field("❓ How to fix this", format!("{:#}", error), false); embed.field("🐛 Bug report", String::from("[GitHub](https://github.com/Botanism/Botanist/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BREPORTED+BUG%5D) | [Official Server](https://discord.gg/mpGM5cg)"), false); //we send the created embed in the channel where the error occured, if possible - match channel - .send_message(ctx, |m| m.set_embed(embed.clone())) + match ctx + .send(|r| { + r.embed(|e| { + *e = embed.clone(); + e + }) + }) .await { Ok(_) => (), Err(SerenityError::Http(_)) => { info!( - "couldn't report error in {:?} because of missing permissions", - channel + "couldn't report error in channel {:?} because of missing permissions", + ctx.channel_id() ); - error_report_fallback(ctx, channel, cause_user, embed).await; + error_report_fallback(ctx, cause_user, embed).await; } Err(SerenityError::Model(ModelError::MessageTooLong(excess))) => { error!("CRITICAL: error embed was too long by {:?}", excess) @@ -64,81 +67,61 @@ pub async fn report_error<'a, 'b>( } //used if the bot couldn't report an error through the channel of origin -//since it's a fallback it mustn't fail, however acttuall delivery may for network error reasons +//since it's a fallback it mustn't fail, however actual delivery may fail for network error reasons //or if the user has blocked DMs //may one day be merged with error_report if BotError gains a dm:bool attribute -async fn error_report_fallback( - ctx: &Context, - chan: &ChannelId, - culprit: Option<&User>, - embed: CreateEmbed, -) { - match culprit { - None => info!("Could not determine a user that caused the issue, can't fallback to DM error reporting"), - //since we couldn't send the error in the channel we try to do so in DM - Some(user) => match user.dm(ctx, |m| m.set_embed(embed).content(format!("We couldn't report the error in {:#} so we're doing it here!", chan.mention()))).await { - Err(_) => error!("Fallback error reporting failed because the DM couldn't be sent"), - Ok(_) => (), - } +async fn error_report_fallback(ctx: Context<'_>, culprit: &User, embed: CreateEmbed) { + //since we couldn't send the error in the channel we try to do so in DM + match culprit + .dm(ctx.discord(), |m| { + m.set_embed(embed).content(format!( + "We couldn't report the error in {:#} so we're doing it here!", + ctx.channel_id().mention() + )) + }) + .await + { + Err(_) => error!("Fallback error reporting failed because the DM couldn't be sent"), + Ok(_) => (), } } -//used to describe an error to the user, meant to be supplied to `report_error` +//occurs only with `parse_duration` when the given string cannot be parsed #[derive(Debug)] -pub struct BotError<'a, 'b> { - description: &'a str, - //optionally provide additional information on this kind of error - kind: Option, - //sometimes the error doesn't originate from a specific message or command -> None - origin: Option<&'b Message>, +pub struct DurationParseError { + pub source: String, } -impl<'a, 'b> BotError<'a, 'b> { - pub fn new( - description: &'a str, - kind: Option, - origin: Option<&'b Message>, - ) -> BotError<'a, 'b> { - BotError { - description, - kind, - origin, - } +impl Display for DurationParseError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + write!(f, "Could not parse {:#} as a duration.", self.source) } } +impl std::error::Error for DurationParseError {} -//when applicable, used to provide additional information on the error to the user -#[derive(Debug)] -pub enum BotErrorKind { - EnvironmentError, - IncorrectNumberOfArgs, - UnexpectedError, - DurationSyntaxError, - ModelParsingError, - MissingPermissions, -} - -impl BotErrorKind { +impl BotError { pub fn pretty_name(&self) -> &'static str { match self { - BotErrorKind::EnvironmentError => "Misconfigured", - BotErrorKind::IncorrectNumberOfArgs => "Incorrect number of arguments provided", - BotErrorKind::UnexpectedError => "UNEXPECTED", - BotErrorKind::DurationSyntaxError => "Not a duration", - BotErrorKind::ModelParsingError => "Unrecognized datum type", - BotErrorKind::MissingPermissions => "Botanist misses permissions", + BotError::EnvironmentError => "Misconfigured", + BotError::IncorrectNumberOfArgs => "Incorrect number of arguments provided", + BotError::UnexpectedError => "UNEXPECTED", + BotError::DurationParseError(_) => "Not a duration", + BotError::ModelParsingError => "Unrecognized datum type", + BotError::MissingPermissions => "Botanist lacks permissions", + _ => "Unknown error occured!", } } } -impl Display for BotErrorKind { +impl Display for BotError { fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { write!(f, "{}", match self { - BotErrorKind::EnvironmentError => "The bot was incorrectly configured by its owner. Please contact a bot administrator so that they can fix this ASAP!", - BotErrorKind::IncorrectNumberOfArgs => "You called a command but provided an incorrect number of arguments. Consult the online documentation or type `::help ` to know which arguments are expected. If you think you've provided the right number of arguments make sure they are separated by a valid delimiter. For arguments containing space(s), surround them with quotes: `:: \"arg with spaces\"`", - BotErrorKind::UnexpectedError => "This error is not covered. It is either undocumented or comes from an unforseen series of events. Either way this is a bug. **Please report it!**", - BotErrorKind::DurationSyntaxError => "A duration was expected as an argument but what you provided was invalid. The correct syntax for duations is `00d00h00m00s` where `00` stands for any valid number. As for the letters they're shorts for `day`, `hour`, `minute` and `second` respectively. They can be both lowercase and uppercase.", - BotErrorKind::ModelParsingError => "This error occurs when a command expects a role/member/channel but something that could not be understood as such was provided. The bot uses multiple methods to interpolate the channel/role/member. You can use it's ID, mention directly, or use the name. The latter may fail if the name is ambiguous.", - BotErrorKind::MissingPermissions => "The bot lacks the required permissions for this command in this channel. Please contact a server admin so that they can fix this. ", + BotError::EnvironmentError => "The bot was incorrectly configured by its owner. Please contact a bot administrator so that they can fix this ASAP!", + BotError::IncorrectNumberOfArgs => "You called a command but provided an incorrect number of arguments. Consult the online documentation or type `::help ` to know which arguments are expected. If you think you've provided the right number of arguments make sure they are separated by a valid delimiter. For arguments containing space(s), surround them with quotes: `:: \"arg with spaces\"`", + BotError::UnexpectedError => "This error is not covered. It is either undocumented or comes from an unforseen series of events. Either way this is a bug. **Please report it!**", + BotError::DurationParseError(_) => "A duration was expected as an argument but what you provided was invalid. The correct syntax for duations is `00d00h00m00s` where `00` stands for any valid number. As for the letters they're shorts for `day`, `hour`, `minute` and `second` respectively. They can be both lowercase and uppercase.", + BotError::ModelParsingError => "This error occurs when a command expects a role/member/channel but something that could not be understood as such was provided. The bot uses multiple methods to interpolate the channel/role/member. You can use it's ID, mention directly, or use the name. The latter may fail if the name is ambiguous.", + BotError::MissingPermissions => "The bot lacks the required permissions for this command in this channel. Please contact a server admin so that they can fix this. ", + _ => "Unknown error occured!", }) } } @@ -146,43 +129,49 @@ impl Display for BotErrorKind { //Top-level error enum for the bot (which name is Botanist). //allows us to use custom errors alongside serenity's ones #[derive(Debug)] -pub enum BotanistError { +pub enum BotError { + EnvironmentError, + IncorrectNumberOfArgs, + UnexpectedError, + ModelParsingError, + MissingPermissions, DurationParseError(DurationParseError), + PoiseError(Box), Serenity(SerenityError), - SerenityUserIdParseError(serenity::model::misc::UserIdParseError), + SerenityUserIdParseError(poise::serenity::model::misc::UserIdParseError), } +impl std::error::Error for BotError {} -impl Display for BotanistError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { - write!(f, "BotanistError: {:#}", self) +impl From for BotError { + fn from(err: SlashArgError) -> Self { + BotError::PoiseError(Box::new(err)) } } -impl std::error::Error for BotanistError {} - -impl From for BotanistError { +impl From for BotError { fn from(err: SerenityError) -> Self { - BotanistError::Serenity(err) + BotError::Serenity(err) } } -impl From for BotanistError { - fn from(err: serenity::model::misc::UserIdParseError) -> Self { - BotanistError::SerenityUserIdParseError(err) +//somehow this error is not part of `SerenityError` +impl From for BotError { + fn from(err: poise::serenity::model::misc::UserIdParseError) -> Self { + BotError::SerenityUserIdParseError(err) } } -//occurs only with `parse_duration` when the given string cannot be parsed -#[derive(Debug)] -pub struct DurationParseError { - pub source: String, +impl From for BotError { + fn from(err: DurationParseError) -> Self { + BotError::DurationParseError(err) + } } -impl Display for DurationParseError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { - write!(f, "Could not parse {:#} as a duration.", self.source) + +impl From for BotError { + fn from(err: ArgumentParseError) -> Self { + BotError::PoiseError(Box::new(err)) } } -impl std::error::Error for DurationParseError {} ///Parses a String into a duration using its own convention. ///XdYhZmAs would be parsed a duration of X days Y hours @@ -230,3 +219,20 @@ pub fn parse_duration>(string: S) -> Result { + if $ctx.guild_id().is_none() { + Err(poise::serenity::Error::Other("not in guild")) + } else { + Ok(()) + } + }; +} + +#[allow(unused_imports)] +pub(crate) use in_guild;