Improved bot command handling

+ Created CommandRunner struct to house all the bot commands
+ Streamlines registering app commands and dispatching commands
+ Bit of hecky rust that may need to be cleaned up
+ Clippy + Fmt
main
Joey Hines 2021-12-12 19:30:38 -07:00
parent 2ff4d14e3f
commit 3aaaf39913
No known key found for this signature in database
GPG Key ID: 80F567B5C968F91B
14 changed files with 330 additions and 267 deletions

View File

@ -11,8 +11,8 @@ use geoffrey_models::models::parameters::add_token_params::AddTokenParams;
use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::token::Permissions; use geoffrey_models::models::token::Permissions;
use crate::commands::{Command, command_filter};
use crate::commands::add_token::AddToken; use crate::commands::add_token::AddToken;
use crate::commands::{command_filter, Command};
use crate::config::GeoffreyAPIConfig; use crate::config::GeoffreyAPIConfig;
use crate::context::Context; use crate::context::Context;
use crate::logging::init_logging; use crate::logging::init_logging;

View File

@ -1,8 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::Method; use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{ use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType, ApplicationCommandInteraction, ApplicationCommandOptionType,
}; };
use geoffrey_models::models::locations::Location; use geoffrey_models::models::locations::Location;
@ -11,6 +10,7 @@ use geoffrey_models::models::parameters::add_item_params::AddItemParams;
use crate::bot::arg_parse::{option_to_i64, option_to_string}; use crate::bot::arg_parse::{option_to_i64, option_to_string};
use crate::bot::commands::{BotCommand, CommandError}; use crate::bot::commands::{BotCommand, CommandError};
use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use serenity::builder::CreateApplicationCommand;
pub struct AddItemCommand; pub struct AddItemCommand;
@ -27,45 +27,50 @@ impl BotCommand for AddItemCommand {
Method::POST Method::POST
} }
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> { fn custom_err_resp(e: &CommandError) -> Option<String> {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| { if let CommandError::GeoffreyApi(err) = e {
command if matches!(err, GeoffreyAPIError::EntryNotFound) {
.name(Self::command_name()) return Some("You don't have a shop by that name ding dong!".to_string());
.description("Add a item to a shop.") }
.create_option(|option| { }
option
.name("item_name")
.description("Name of the item to sell.")
.kind(ApplicationCommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("price")
.description("Price to list them item at.")
.kind(ApplicationCommandOptionType::Integer)
.required(true)
.min_int_value(0)
})
.create_option(|option| {
option
.name("quantity")
.description("Number of items to sell for price")
.kind(ApplicationCommandOptionType::Integer)
.required(true)
.min_int_value(1)
})
.create_option(|option| {
option
.name("shop")
.description("Shop to list the item at")
.kind(ApplicationCommandOptionType::String)
.required(true)
})
})
.await?;
Ok(command) None
}
fn create_app_command(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name(Self::command_name())
.description("Add a item to a shop.")
.create_option(|option| {
option
.name("item_name")
.description("Name of the item to sell.")
.kind(ApplicationCommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("price")
.description("Price to list them item at.")
.kind(ApplicationCommandOptionType::Integer)
.required(true)
.min_int_value(0)
})
.create_option(|option| {
option
.name("quantity")
.description("Number of items to sell for price")
.kind(ApplicationCommandOptionType::Integer)
.required(true)
.min_int_value(1)
})
.create_option(|option| {
option
.name("shop")
.description("Shop to list the item at")
.kind(ApplicationCommandOptionType::String)
.required(true)
})
} }
async fn process_arguments( async fn process_arguments(
@ -84,14 +89,4 @@ impl BotCommand for AddItemCommand {
fn build_response(resp: Self::ApiResp) -> String { fn build_response(resp: Self::ApiResp) -> String {
format!("{} has been updated", resp.name) format!("{} has been updated", resp.name)
} }
fn custom_err_resp(e: &CommandError) -> Option<String> {
if let CommandError::GeoffreyApi(err) = e {
if matches!(err, GeoffreyAPIError::EntryNotFound) {
return Some("You don't have a shop by that name ding dong!".to_string());
}
}
None
}
} }

View File

@ -3,13 +3,13 @@ use geoffrey_models::models::locations::{Location, LocationType};
use geoffrey_models::models::parameters::add_location_params::AddLocationParams; use geoffrey_models::models::parameters::add_location_params::AddLocationParams;
use geoffrey_models::models::{Dimension, Position}; use geoffrey_models::models::{Dimension, Position};
use reqwest::Method; use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{ use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType, ApplicationCommandInteraction, ApplicationCommandOptionType,
}; };
use crate::bot::arg_parse::{option_to_dim, option_to_i64, option_to_loc_type, option_to_string}; use crate::bot::arg_parse::{option_to_dim, option_to_i64, option_to_loc_type, option_to_string};
use crate::bot::commands::{BotCommand, CommandError}; use crate::bot::commands::{BotCommand, CommandError};
use serenity::builder::CreateApplicationCommand;
pub struct AddLocationCommand; pub struct AddLocationCommand;
@ -26,72 +26,67 @@ impl BotCommand for AddLocationCommand {
Method::POST Method::POST
} }
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> { fn create_app_command(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| { command
command .name(Self::command_name())
.name(Self::command_name()) .description("Add a location to Geoffrey.")
.description("Add a location to Geoffrey.") .create_option(|option| {
.create_option(|option| { option
option .name("type")
.name("type") .description("Location type")
.description("Location type") .kind(ApplicationCommandOptionType::String)
.kind(ApplicationCommandOptionType::String) .required(true)
.required(true) .add_string_choice(LocationType::Base, LocationType::Base)
.add_string_choice(LocationType::Base, LocationType::Base) .add_string_choice(LocationType::Shop, LocationType::Shop)
.add_string_choice(LocationType::Shop, LocationType::Shop) .add_string_choice(LocationType::Attraction, LocationType::Attraction)
.add_string_choice(LocationType::Attraction, LocationType::Attraction) .add_string_choice(LocationType::Town, LocationType::Town)
.add_string_choice(LocationType::Town, LocationType::Town) .add_string_choice(LocationType::Farm, LocationType::Farm)
.add_string_choice(LocationType::Farm, LocationType::Farm) .add_string_choice(LocationType::Market, LocationType::Market)
.add_string_choice(LocationType::Market, LocationType::Market) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("name")
.name("name") .description("Name of the location")
.description("Name of the location") .kind(ApplicationCommandOptionType::String)
.kind(ApplicationCommandOptionType::String) .required(true)
.required(true) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("x")
.name("x") .description("X coordinate of the location")
.description("X coordinate of the location") .kind(ApplicationCommandOptionType::Integer)
.kind(ApplicationCommandOptionType::Integer) .max_int_value(i32::MAX)
.max_int_value(i32::MAX) .min_int_value(i32::MIN)
.min_int_value(i32::MIN) .required(true)
.required(true) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("y")
.name("y") .description("Y coordinate of the location")
.description("Y coordinate of the location") .kind(ApplicationCommandOptionType::Integer)
.kind(ApplicationCommandOptionType::Integer) .max_int_value(i32::MAX)
.max_int_value(i32::MAX) .min_int_value(i32::MIN)
.min_int_value(i32::MIN) .required(true)
.required(true) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("z")
.name("z") .description("Z coordinate of the location")
.description("Z coordinate of the location") .kind(ApplicationCommandOptionType::Integer)
.kind(ApplicationCommandOptionType::Integer) .max_int_value(i32::MAX)
.max_int_value(i32::MAX) .min_int_value(i32::MIN)
.min_int_value(i32::MIN) .required(true)
.required(true) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("dimension")
.name("dimension") .description("Dimension of the shop, default is Overworld")
.description("Dimension of the shop, default is Overworld") .kind(ApplicationCommandOptionType::String)
.kind(ApplicationCommandOptionType::String) .add_string_choice(Dimension::Overworld, Dimension::Overworld)
.add_string_choice(Dimension::Overworld, Dimension::Overworld) .add_string_choice(Dimension::Nether, Dimension::Nether)
.add_string_choice(Dimension::Nether, Dimension::Nether) .add_string_choice(Dimension::TheEnd, Dimension::TheEnd)
.add_string_choice(Dimension::TheEnd, Dimension::TheEnd) .required(false)
.required(false) })
})
})
.await?;
Ok(command)
} }
async fn process_arguments( async fn process_arguments(

View File

@ -1,8 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::Method; use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{ use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType, ApplicationCommandInteraction, ApplicationCommandOptionType,
}; };
use std::fmt::Write; use std::fmt::Write;
@ -12,6 +11,7 @@ use geoffrey_models::models::parameters::find_params::FindParams;
use crate::bot::arg_parse::option_to_string; use crate::bot::arg_parse::option_to_string;
use crate::bot::commands::{BotCommand, CommandError}; use crate::bot::commands::{BotCommand, CommandError};
use crate::bot::formatters::display_loc; use crate::bot::formatters::display_loc;
use serenity::builder::CreateApplicationCommand;
pub struct FindCommand; pub struct FindCommand;
@ -28,22 +28,17 @@ impl BotCommand for FindCommand {
Method::GET Method::GET
} }
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> { fn create_app_command(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| { command
command .name(Self::command_name())
.name(Self::command_name()) .description("Find a location in Geoffrey.")
.description("Find a location in Geoffrey.") .create_option(|option| {
.create_option(|option| { option
option .name("query")
.name("query") .description("The location name or player to lookup")
.description("The location name or player to lookup") .kind(ApplicationCommandOptionType::String)
.kind(ApplicationCommandOptionType::String) .required(true)
.required(true) })
})
})
.await?;
Ok(command)
} }
async fn process_arguments( async fn process_arguments(

View File

@ -4,10 +4,7 @@ use async_trait::async_trait;
use reqwest::Error; use reqwest::Error;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use serenity::client::Context; use serenity::model::interactions::application_command::ApplicationCommandInteraction;
use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction,
};
use serenity::Error as SerenityError; use serenity::Error as SerenityError;
use geoffrey_models::models::parameters::CommandRequest; use geoffrey_models::models::parameters::CommandRequest;
@ -16,6 +13,9 @@ use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::response::APIResponse; use geoffrey_models::models::response::APIResponse;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use serenity::builder::CreateApplicationCommand;
use std::future::Future;
use std::pin::Pin;
pub mod add_item; pub mod add_item;
pub mod add_location; pub mod add_location;
@ -23,12 +23,21 @@ pub mod find;
pub mod selling; pub mod selling;
pub mod set_portal; pub mod set_portal;
pub type GeoffreyCommandFn = Box<
fn(
GeoffreyContext,
UserID,
ApplicationCommandInteraction,
) -> Pin<Box<dyn Future<Output = String> + Send>>,
>;
#[derive(Debug)] #[derive(Debug)]
pub enum CommandError { pub enum CommandError {
ArgumentParse(String), ArgumentParse(String),
GeoffreyApi(GeoffreyAPIError), GeoffreyApi(GeoffreyAPIError),
Serenity(serenity::Error), Serenity(serenity::Error),
Reqwest(reqwest::Error), Reqwest(reqwest::Error),
CommandNotFound(String),
} }
impl Display for CommandError { impl Display for CommandError {
@ -38,6 +47,7 @@ impl Display for CommandError {
CommandError::GeoffreyApi(err) => format!("Got error from GeoffreyAPI: {}", err), CommandError::GeoffreyApi(err) => format!("Got error from GeoffreyAPI: {}", err),
CommandError::Serenity(err) => format!("Serenity Error: {}", err), CommandError::Serenity(err) => format!("Serenity Error: {}", err),
CommandError::Reqwest(err) => format!("Reqwest Error: {}", err), CommandError::Reqwest(err) => format!("Reqwest Error: {}", err),
CommandError::CommandNotFound(err) => format!("'{}' not found!", err),
}; };
write!(f, "{}", s) write!(f, "{}", s)
@ -63,7 +73,7 @@ impl From<reqwest::Error> for CommandError {
} }
#[async_trait] #[async_trait]
pub trait BotCommand { pub trait BotCommand: Send + 'static {
type ApiParams: CommandRequest; type ApiParams: CommandRequest;
type ApiResp: Serialize + DeserializeOwned + Send; type ApiResp: Serialize + DeserializeOwned + Send;
@ -78,7 +88,7 @@ pub trait BotCommand {
} }
async fn run_api_query( async fn run_api_query(
ctx: &GeoffreyContext, ctx: GeoffreyContext,
params: Self::ApiParams, params: Self::ApiParams,
) -> Result<APIResponse<Self::ApiResp>, CommandError> { ) -> Result<APIResponse<Self::ApiResp>, CommandError> {
let command_url = Self::command_url(&ctx.cfg.api.base_url); let command_url = Self::command_url(&ctx.cfg.api.base_url);
@ -103,7 +113,7 @@ pub trait BotCommand {
"You need to register before using this command!".to_string() "You need to register before using this command!".to_string()
} }
CommandError::GeoffreyApi(GeoffreyAPIError::EntryNotFound) => { CommandError::GeoffreyApi(GeoffreyAPIError::EntryNotFound) => {
"Couldn't find that, maybe look for something that exists?".to_string() "Couldn't find that, maybe look for something that exists?".to_string()
} }
CommandError::GeoffreyApi(GeoffreyAPIError::PermissionInsufficient) => { CommandError::GeoffreyApi(GeoffreyAPIError::PermissionInsufficient) => {
"Looks like you don't have permission for that.".to_string() "Looks like you don't have permission for that.".to_string()
@ -133,20 +143,24 @@ pub trait BotCommand {
None None
} }
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError>; fn create_app_command(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand;
async fn process_arguments( async fn process_arguments(
command_interaction: ApplicationCommandInteraction, command_interaction: ApplicationCommandInteraction,
) -> Result<Self::ApiParams, CommandError>; ) -> Result<Self::ApiParams, CommandError>;
async fn run_command( async fn run_command(
ctx: &GeoffreyContext, ctx: GeoffreyContext,
user_id: UserID, user_id: UserID,
command_interact: ApplicationCommandInteraction, command_interact: ApplicationCommandInteraction,
) -> Result<String, CommandError> { ) -> Result<String, CommandError> {
let mut args = Self::process_arguments(command_interact).await?; let mut args = Self::process_arguments(command_interact).await?;
log::info!("Running command {}, with args {:?}", Self::command_name(), args); log::info!(
"Running command {}, with args {:?}",
Self::command_name(),
args
);
args.set_token(ctx.cfg.api.token.clone()); args.set_token(ctx.cfg.api.token.clone());
args.set_user_id(user_id); args.set_user_id(user_id);
@ -160,7 +174,7 @@ pub trait BotCommand {
} }
async fn command( async fn command(
ctx: &GeoffreyContext, ctx: GeoffreyContext,
user_id: UserID, user_id: UserID,
command_interact: ApplicationCommandInteraction, command_interact: ApplicationCommandInteraction,
) -> String { ) -> String {

View File

@ -2,9 +2,8 @@ use std::fmt::Write;
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::Method; use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{ use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType, ApplicationCommandInteraction, ApplicationCommandOptionType,
}; };
use geoffrey_models::models::parameters::selling_params::{ItemSort, Order, SellingParams}; use geoffrey_models::models::parameters::selling_params::{ItemSort, Order, SellingParams};
@ -12,6 +11,7 @@ use geoffrey_models::models::response::selling_listing::SellingListing;
use crate::bot::arg_parse::{option_to_order, option_to_sort, option_to_string}; use crate::bot::arg_parse::{option_to_order, option_to_sort, option_to_string};
use crate::bot::commands::{BotCommand, CommandError}; use crate::bot::commands::{BotCommand, CommandError};
use serenity::builder::CreateApplicationCommand;
pub struct SellingCommand; pub struct SellingCommand;
@ -28,40 +28,35 @@ impl BotCommand for SellingCommand {
Method::GET Method::GET
} }
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> { fn create_app_command(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| { command
command .name(Self::command_name())
.name(Self::command_name()) .description("Find items for sale.")
.description("Find items for sale.") .create_option(|option| {
.create_option(|option| { option
option .name("query")
.name("query") .description("Item to find")
.description("Item to find") .kind(ApplicationCommandOptionType::String)
.kind(ApplicationCommandOptionType::String) .required(true)
.required(true) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("sort")
.name("sort") .description("How to sort items")
.description("How to sort items") .kind(ApplicationCommandOptionType::String)
.kind(ApplicationCommandOptionType::String) .add_string_choice(ItemSort::Price, ItemSort::Price)
.add_string_choice(ItemSort::Price, ItemSort::Price) .add_string_choice(ItemSort::Restock, ItemSort::Restock)
.add_string_choice(ItemSort::Restock, ItemSort::Restock) .required(false)
.required(false) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("order")
.name("order") .description("Order of the item Search")
.description("Order of the item Search") .kind(ApplicationCommandOptionType::String)
.kind(ApplicationCommandOptionType::String) .add_string_choice(Order::Low, Order::Low)
.add_string_choice(Order::Low, Order::Low) .add_string_choice(Order::High, Order::High)
.add_string_choice(Order::High, Order::High) .required(false)
.required(false) })
})
})
.await?;
Ok(command)
} }
async fn process_arguments( async fn process_arguments(

View File

@ -2,14 +2,14 @@ use async_trait::async_trait;
use geoffrey_models::models::locations::Location; use geoffrey_models::models::locations::Location;
use geoffrey_models::models::Portal; use geoffrey_models::models::Portal;
use reqwest::Method; use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{ use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType, ApplicationCommandInteraction, ApplicationCommandOptionType,
}; };
use crate::bot::arg_parse::{option_to_i64, option_to_string}; use crate::bot::arg_parse::{option_to_i64, option_to_string};
use crate::bot::commands::{BotCommand, CommandError}; use crate::bot::commands::{BotCommand, CommandError};
use geoffrey_models::models::parameters::set_portal_params::SetPortalParams; use geoffrey_models::models::parameters::set_portal_params::SetPortalParams;
use serenity::builder::CreateApplicationCommand;
pub struct SetPortalCommand; pub struct SetPortalCommand;
@ -26,40 +26,35 @@ impl BotCommand for SetPortalCommand {
Method::POST Method::POST
} }
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> { fn create_app_command(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| { command
command .name(Self::command_name())
.name(Self::command_name()) .description("Set a portal for a location")
.description("Set a portal for a location") .create_option(|option| {
.create_option(|option| { option
option .name("loc_name")
.name("loc_name") .description("Name of the location")
.description("Name of the location") .kind(ApplicationCommandOptionType::String)
.kind(ApplicationCommandOptionType::String) .required(true)
.required(true) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("x_portal")
.name("x_portal") .description("X coordinate of the portal in the nether")
.description("X coordinate of the portal in the nether") .kind(ApplicationCommandOptionType::Integer)
.kind(ApplicationCommandOptionType::Integer) .max_int_value(i32::MAX)
.max_int_value(i32::MAX) .min_int_value(i32::MIN)
.min_int_value(i32::MIN) .required(true)
.required(true) })
}) .create_option(|option| {
.create_option(|option| { option
option .name("z_portal")
.name("z_portal") .description("Z coordinate of the portal in the nether")
.description("Z coordinate of the portal in the nether") .kind(ApplicationCommandOptionType::Integer)
.kind(ApplicationCommandOptionType::Integer) .max_int_value(i32::MAX)
.max_int_value(i32::MAX) .min_int_value(i32::MIN)
.min_int_value(i32::MIN) .required(true)
.required(true) })
})
})
.await?;
Ok(command)
} }
async fn process_arguments( async fn process_arguments(
@ -78,4 +73,4 @@ impl BotCommand for SetPortalCommand {
fn build_response(resp: Self::ApiResp) -> String { fn build_response(resp: Self::ApiResp) -> String {
format!("{} has been been updated.", resp.name) format!("{} has been been updated.", resp.name)
} }
} }

View File

@ -30,13 +30,19 @@ pub fn display_owners(owners: Vec<Player>, limit: usize) -> String {
} }
pub fn display_portal(portal: Portal) -> String { pub fn display_portal(portal: Portal) -> String {
format!("Portal: {} {} (x={}, z={})", portal.direction(), portal.tunnel_addr(), portal.x, portal.z) format!(
"Portal: {} {} (x={}, z={})",
portal.direction(),
portal.tunnel_addr(),
portal.x,
portal.z
)
} }
pub fn display_loc(loc: Location) -> String { pub fn display_loc(loc: Location) -> String {
let portal_str = match loc.portal { let portal_str = match loc.portal {
None => "".to_string(), None => "".to_string(),
Some(p) => format!("{}, ", display_portal(p)) Some(p) => format!("{}, ", display_portal(p)),
}; };
format!( format!(

View File

@ -1,25 +1,86 @@
use serenity::model::interactions::application_command::ApplicationCommand; use serenity::model::interactions::application_command::ApplicationCommand;
use serenity::prelude::*; use serenity::prelude::*;
use crate::bot::commands::GeoffreyCommandFn;
use crate::context::GeoffreyContext;
use commands::add_item::AddItemCommand; use commands::add_item::AddItemCommand;
use commands::add_location::AddLocationCommand; use commands::add_location::AddLocationCommand;
use commands::find::FindCommand; use commands::find::FindCommand;
use commands::selling::SellingCommand; use commands::selling::SellingCommand;
use commands::set_portal::SetPortalCommand; use commands::set_portal::SetPortalCommand;
use commands::{BotCommand, CommandError}; use commands::{BotCommand, CommandError};
use geoffrey_models::models::player::UserID;
use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use std::collections::HashMap;
pub mod arg_parse; pub mod arg_parse;
pub mod commands; pub mod commands;
pub mod formatters; pub mod formatters;
pub async fn create_commands(ctx: &Context) -> Result<Vec<ApplicationCommand>, CommandError> { #[derive(Default)]
let mut commands: Vec<ApplicationCommand> = Vec::new(); pub struct CommandRunner {
commands: HashMap<String, GeoffreyCommandFn>,
commands.push(FindCommand::create_app_command(ctx).await?); }
commands.push(SellingCommand::create_app_command(ctx).await?);
commands.push(AddLocationCommand::create_app_command(ctx).await?); impl CommandRunner {
commands.push(AddItemCommand::create_app_command(ctx).await?); async fn register_app_command<T: BotCommand>(ctx: &Context) -> Result<(), CommandError> {
commands.push( SetPortalCommand::create_app_command(ctx).await?); ApplicationCommand::create_global_application_command(&ctx.http, |command| {
T::create_app_command(command)
Ok(commands) })
.await?;
Ok(())
}
fn add_command_to_lookup<T: BotCommand>(&mut self) {
self.commands
.insert(T::command_name(), Box::new(T::command));
}
pub async fn add_command<T: BotCommand>(
&mut self,
ctx: &Context,
) -> Result<&mut Self, CommandError> {
self.add_command_to_lookup::<T>();
Self::register_app_command::<T>(ctx).await?;
Ok(self)
}
pub async fn run_command<'r>(
&self,
command_name: &str,
geoffrey_ctx: GeoffreyContext,
user_id: UserID,
interaction: ApplicationCommandInteraction,
) -> Result<String, CommandError> {
let command_fn = self
.commands
.get(command_name)
.ok_or_else(|| CommandError::CommandNotFound(command_name.to_string()))?;
Ok(command_fn(geoffrey_ctx, user_id, interaction).await)
}
}
pub async fn build_commands(
ctx: &Context,
command_runner: &mut CommandRunner,
) -> Result<(), CommandError> {
command_runner
.add_command::<AddItemCommand>(ctx)
.await?
.add_command::<AddLocationCommand>(ctx)
.await?
.add_command::<FindCommand>(ctx)
.await?
.add_command::<SellingCommand>(ctx)
.await?
.add_command::<SetPortalCommand>(ctx)
.await?;
Ok(())
}
impl TypeMapKey for CommandRunner {
type Value = CommandRunner;
} }

View File

@ -8,4 +8,4 @@ pub fn init_logging(log_level: LogLevel) -> Result<(), SetLoggerError> {
.with_level(LevelFilter::Warn) .with_level(LevelFilter::Warn)
.with_module_level("geoffrey_bot", log_level.into()) .with_module_level("geoffrey_bot", log_level.into())
.init() .init()
} }

View File

@ -3,14 +3,11 @@ mod configs;
mod context; mod context;
mod logging; mod logging;
use crate::bot::create_commands; use crate::bot::{build_commands, CommandRunner};
use crate::configs::GeoffreyBotConfig; use crate::configs::GeoffreyBotConfig;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use bot::commands::add_item::AddItemCommand; use crate::logging::init_logging;
use bot::commands::add_location::AddLocationCommand; use geoffrey_models::logging::LogLevel;
use bot::commands::find::FindCommand;
use bot::commands::selling::SellingCommand;
use bot::commands::BotCommand;
use geoffrey_models::models::player::UserID; use geoffrey_models::models::player::UserID;
use serenity::utils::{content_safe, ContentSafeOptions}; use serenity::utils::{content_safe, ContentSafeOptions};
use serenity::{ use serenity::{
@ -23,9 +20,6 @@ use serenity::{
}; };
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt; use structopt::StructOpt;
use geoffrey_models::logging::LogLevel;
use crate::logging::init_logging;
use crate::bot::commands::set_portal::SetPortalCommand;
#[derive(Debug, StructOpt, Clone)] #[derive(Debug, StructOpt, Clone)]
#[structopt(name = "GeoffreyBot", about = "Geoffrey Discord Bot")] #[structopt(name = "GeoffreyBot", about = "Geoffrey Discord Bot")]
@ -33,11 +27,11 @@ struct Args {
#[structopt(env = "GEOFFREY_BOT_CONFIG", parse(from_os_str))] #[structopt(env = "GEOFFREY_BOT_CONFIG", parse(from_os_str))]
config: PathBuf, config: PathBuf,
#[structopt( #[structopt(
short, short,
long, long,
env = "GEOFFREY_LOG_LEVEL", env = "GEOFFREY_LOG_LEVEL",
parse(from_str), parse(from_str),
default_value = "Info" default_value = "Info"
)] )]
log_level: LogLevel, log_level: LogLevel,
} }
@ -54,36 +48,47 @@ struct Handler;
impl EventHandler for Handler { impl EventHandler for Handler {
async fn ready(&self, ctx: Context, ready: Ready) { async fn ready(&self, ctx: Context, ready: Ready) {
log::info!("{} is connected!", ready.user.name); log::info!("{} is connected!", ready.user.name);
let commands = create_commands(&ctx).await.unwrap();
log::debug!("The following bot have been registered:"); let mut data = ctx.data.write().await;
for command in commands { let command_runner = data
log::debug!( .get_mut::<CommandRunner>()
"{}: {} - {:?}", .expect("Unable to open command runner!");
command.name, command.description, command.options
); match build_commands(&ctx, command_runner).await {
Ok(_) => {}
Err(e) => {
log::warn!("Error registering commands: {:?}", e)
}
} }
} }
async fn interaction_create(&self, ctx: Context, interaction: Interaction) { async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
let data = ctx.data.read().await; let data = ctx.data.read().await;
let geoffrey_ctx = data.get::<GeoffreyContext>().unwrap(); let geoffrey_ctx = data.get::<GeoffreyContext>().unwrap();
let command_runner = data.get::<CommandRunner>().unwrap();
if let Interaction::ApplicationCommand(command) = interaction { if let Interaction::ApplicationCommand(command) = interaction {
let user_id = UserID::DiscordUUID { let user_id = UserID::DiscordUUID {
discord_uuid: command.user.id.0, discord_uuid: command.user.id.0,
}; };
let msg = match command.data.name.as_str() { let command_name = command.data.name.clone();
"find" => FindCommand::command(geoffrey_ctx, user_id, command.clone()).await,
"selling" => SellingCommand::command(geoffrey_ctx, user_id, command.clone()).await, let msg = match command_runner
"add_location" => { .run_command(
AddLocationCommand::command(geoffrey_ctx, user_id, command.clone()).await &command_name,
geoffrey_ctx.clone(),
user_id,
command.clone(),
)
.await
{
Ok(msg) => msg,
Err(e) => {
log::warn!("Error running command '{}': {:?}", command_name, e);
return;
} }
"add_item" => AddItemCommand::command(geoffrey_ctx, user_id, command.clone()).await,
"set_portal" => SetPortalCommand::command(geoffrey_ctx, user_id, command.clone()).await,
_ => "not implemented :(".to_string(),
}; };
let msg = content_safe(&ctx.cache, &msg, &ContentSafeOptions::default()).await; let msg = content_safe(&ctx.cache, &msg, &ContentSafeOptions::default()).await;
@ -143,7 +148,9 @@ async fn main() {
data.insert::<GeoffreyContext>(GeoffreyContext { data.insert::<GeoffreyContext>(GeoffreyContext {
http_client: reqwest::Client::new(), http_client: reqwest::Client::new(),
cfg, cfg,
}) });
data.insert::<CommandRunner>(CommandRunner::default());
} }
if let Err(e) = client.start().await { if let Err(e) = client.start().await {

View File

@ -3,8 +3,8 @@ use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug; use std::fmt::Debug;
pub mod models;
pub mod logging; pub mod logging;
pub mod models;
pub trait GeoffreyDatabaseModel: Serialize + DeserializeOwned + Debug { pub trait GeoffreyDatabaseModel: Serialize + DeserializeOwned + Debug {
fn id(&self) -> Option<u64>; fn id(&self) -> Option<u64>;

View File

@ -1,5 +1,5 @@
use log::LevelFilter; use log::LevelFilter;
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub enum LogLevel { pub enum LogLevel {

View File

@ -122,7 +122,7 @@ impl Portal {
pub fn tunnel_addr(&self) -> i32 { pub fn tunnel_addr(&self) -> i32 {
match self.direction() { match self.direction() {
Direction::North | Direction::South => self.z.abs(), Direction::North | Direction::South => self.z.abs(),
Direction::East | Direction::West => self.x.abs() Direction::East | Direction::West => self.x.abs(),
} }
} }
} }