Rough first pass of a bot

+ Uses application commands from discord
+ Tried to follow a similar structure to how the API handles commands
+ Implements add_location, add_item, selling, and find
+ Needs a lot of work lol
+ clippy + fmt
main
Joey Hines 2021-11-30 21:13:56 -07:00
parent bc4936febd
commit 7fb4054cdb
No known key found for this signature in database
GPG Key ID: 80F567B5C968F91B
23 changed files with 2016 additions and 226 deletions

1090
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,5 +2,6 @@
members = [
"geoffrey_models",
"geoffrey_db",
"geoffrey_api"
"geoffrey_api",
"geoffrey_bot"
]

7
bot_config.toml 100644
View File

@ -0,0 +1,7 @@
[api]
token = "0Bxpl0N4wBYHtgfwdtnaFPblQKa4Em9wIAGp6q0TV5X6M2HzuPsjfcCKxXddDny2"
base_url = "http://localhost:8080/"
[discord]
token = "OTExNDQ1MDY3MTMzMjQ3NDk4.YZhfXQ.idsLVIgipSU1SJUnG_vkhZgFOMg"
app_id = 911445067133247498

View File

@ -0,0 +1,18 @@
[package]
name = "geoffrey_bot"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serenity = { git="https://github.com/serenity-rs/serenity.git", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "unstable_discord_api", "cache"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
reqwest = { version = "0.11.6", features = ["json"]}
geoffrey_models = { path = "../geoffrey_models" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_plain = "1.0.0"
async-trait = "0.1.51"
config = "0.11.0"
structopt = "0.3.21"

View File

@ -0,0 +1,129 @@
use crate::commands::bot_command::{BotCommand, CommandError};
use async_trait::async_trait;
use geoffrey_models::models::locations::Location;
use geoffrey_models::models::parameters::add_item_params::AddItemParams;
use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType,
};
use serenity::model::prelude::application_command::ApplicationCommandInteractionDataOptionValue;
pub struct AddItemCommand;
#[async_trait]
impl BotCommand for AddItemCommand {
type ApiParams = AddItemParams;
type ApiResp = Location;
fn command_name() -> String {
"add_item".to_string()
}
fn request_type() -> Method {
Method::POST
}
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| {
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)
})
})
.await?;
Ok(command)
}
async fn process_arguments(
command_interaction: ApplicationCommandInteraction,
) -> Result<Self::ApiParams, CommandError> {
let options = command_interaction.data.options;
let mut item_name = String::default();
let mut price = 0;
let mut quanity = 0;
let mut shop = String::default();
if let Some(option) = options.get(0) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = option {
item_name = s.clone();
} else {
return Err(CommandError::ArgumentParse("item_name".to_string()));
}
}
if let Some(option) = options.get(1) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::Integer(p)) = option {
price = *p;
} else {
return Err(CommandError::ArgumentParse("price".to_string()));
}
}
if let Some(option) = options.get(2) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::Integer(q)) = option {
quanity = *q;
} else {
return Err(CommandError::ArgumentParse("quantity".to_string()));
}
}
if let Some(option) = options.get(3) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = option {
shop = s.clone();
} else {
return Err(CommandError::ArgumentParse("shop".to_string()));
}
}
Ok(Self::ApiParams::new(
item_name,
price as u32,
quanity as u32,
shop,
))
}
fn build_response(resp: Self::ApiResp) -> String {
format!("{} has been updated", resp.name)
}
}

View File

@ -0,0 +1,174 @@
use crate::commands::bot_command::{BotCommand, CommandError};
use async_trait::async_trait;
use geoffrey_models::models::locations::{Location, LocationType};
use geoffrey_models::models::parameters::add_location_params::AddLocationParams;
use geoffrey_models::models::{Dimension, Position};
use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandInteractionDataOption,
ApplicationCommandOptionType,
};
use serenity::model::prelude::application_command::ApplicationCommandInteractionDataOptionValue;
use std::str::FromStr;
pub struct AddLocationCommand;
fn option_to_i64(option: &ApplicationCommandInteractionDataOption) -> Option<i64> {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::Integer(s)) = option {
Some(*s)
} else {
None
}
}
#[async_trait]
impl BotCommand for AddLocationCommand {
type ApiParams = AddLocationParams;
type ApiResp = Location;
fn command_name() -> String {
"add_location".to_string()
}
fn request_type() -> Method {
Method::POST
}
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| {
command
.name(Self::command_name())
.description("Add a location to Geoffrey.")
.create_option(|option| {
option
.name("type")
.description("Location type")
.kind(ApplicationCommandOptionType::String)
.required(true)
.add_string_choice(LocationType::Base, LocationType::Base)
.add_string_choice(LocationType::Shop, LocationType::Shop)
.add_string_choice(LocationType::Attraction, LocationType::Attraction)
.add_string_choice(LocationType::Town, LocationType::Town)
.add_string_choice(LocationType::Farm, LocationType::Farm)
.add_string_choice(LocationType::Market, LocationType::Market)
})
.create_option(|option| {
option
.name("name")
.description("Name of the location")
.kind(ApplicationCommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("x")
.description("X coordinate of the shop")
.kind(ApplicationCommandOptionType::Integer)
.required(true)
})
.create_option(|option| {
option
.name("y")
.description("Y coordinate of the shop")
.kind(ApplicationCommandOptionType::Integer)
.required(true)
})
.create_option(|option| {
option
.name("z")
.description("Z coordinate of the shop")
.kind(ApplicationCommandOptionType::Integer)
.required(true)
})
.create_option(|option| {
option
.name("dimension")
.description("Dimension of the shop, default is Overworld")
.kind(ApplicationCommandOptionType::String)
.add_string_choice(Dimension::Overworld, Dimension::Overworld)
.add_string_choice(Dimension::Nether, Dimension::Nether)
.add_string_choice(Dimension::TheEnd, Dimension::TheEnd)
.required(false)
})
.create_option(|option| {
option
.name("portal_x")
.description("X Coordinate of the portal in the nether")
.kind(ApplicationCommandOptionType::Integer)
.required(false)
})
.create_option(|option| {
option
.name("portal_z")
.description("Z Coordinate of the portal in the nether")
.kind(ApplicationCommandOptionType::Integer)
.required(false)
})
})
.await?;
Ok(command)
}
async fn process_arguments(
command_interaction: ApplicationCommandInteraction,
) -> Result<Self::ApiParams, CommandError> {
let options = command_interaction.data.options;
let mut name = String::new();
let mut loc_type = LocationType::Base;
let x;
let _y;
let z;
let dimension;
if let Some(option) = options.get(0) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = option {
loc_type = LocationType::from_str(s).unwrap();
} else {
return Err(CommandError::ArgumentParse("loc_type".to_string()));
}
}
if let Some(option) = options.get(1) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = option {
name = s.clone();
} else {
return Err(CommandError::ArgumentParse("name".to_string()));
}
}
x = option_to_i64(&options[2]).unwrap() as i32;
_y = option_to_i64(&options[3]).unwrap() as i32;
z = option_to_i64(&options[4]).unwrap() as i32;
if let Some(option) = options.get(5) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = option {
dimension = Dimension::from_str(s.as_str()).unwrap();
} else {
return Err(CommandError::ArgumentParse("dimension".to_string()));
}
} else {
dimension = Dimension::default();
}
Ok(Self::ApiParams::new(
name,
Position::new(x, z, dimension),
loc_type,
None,
))
}
fn build_response(resp: Self::ApiResp) -> String {
format!("{} has been added to Geoffrey.", resp.name)
}
}

View File

@ -0,0 +1,166 @@
use crate::context::GeoffreyContext;
use async_trait::async_trait;
use geoffrey_models::models::parameters::CommandRequest;
use geoffrey_models::models::player::UserID;
use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::response::APIResponse;
use reqwest::Error;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serenity::model::prelude::application_command::{
ApplicationCommand, ApplicationCommandInteraction,
};
use serenity::prelude::{Context, SerenityError};
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum CommandError {
ArgumentParse(String),
GeoffreyApi(GeoffreyAPIError),
Serenity(serenity::Error),
Reqwest(reqwest::Error),
}
impl Display for CommandError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
CommandError::ArgumentParse(s) => format!("Unable to parse argument '{}'", s),
CommandError::GeoffreyApi(err) => format!("Got error from GeoffreyAPI: {}", err),
CommandError::Serenity(err) => format!("Serenity Error: {}", err),
CommandError::Reqwest(err) => format!("Reqwest Error: {}", err),
};
write!(f, "{}", s)
}
}
impl From<GeoffreyAPIError> for CommandError {
fn from(err: GeoffreyAPIError) -> Self {
Self::GeoffreyApi(err)
}
}
impl From<SerenityError> for CommandError {
fn from(err: SerenityError) -> Self {
Self::Serenity(err)
}
}
impl From<reqwest::Error> for CommandError {
fn from(err: Error) -> Self {
Self::Reqwest(err)
}
}
#[async_trait]
pub trait BotCommand {
type ApiParams: CommandRequest;
type ApiResp: Serialize + DeserializeOwned + Send;
fn command_name() -> String;
fn request_type() -> reqwest::Method;
fn command_url(base_string: &str) -> String {
let slash = if !base_string.ends_with('/') { "/" } else { "" };
format!("{}{}command/{}/", base_string, slash, Self::command_name())
}
async fn run_api_query(
ctx: &GeoffreyContext,
params: Self::ApiParams,
) -> Result<APIResponse<Self::ApiResp>, CommandError> {
let command_url = Self::command_url(&ctx.cfg.api.base_url);
let resp: APIResponse<Self::ApiResp> = ctx
.http_client
.request(Self::request_type(), command_url)
.json(&params)
.send()
.await?
.json()
.await?;
Ok(resp)
}
fn get_err_resp(err: CommandError) -> String {
if let Some(resp) = Self::custom_err_resp(&err) {
resp
} else {
match err {
CommandError::GeoffreyApi(err) => match err {
GeoffreyAPIError::PlayerNotRegistered => {
"You need to register before using this command!".to_string()
}
GeoffreyAPIError::EntryNotFound => {
"Couldn't find that, maybe look for something that exists?".to_string()
}
GeoffreyAPIError::PermissionInsufficient => {
"Looks like you don't have permission for that.".to_string()
}
GeoffreyAPIError::EntryNotUnique => {
"Slow down, I already know that thing. Try a new name.".to_string()
}
GeoffreyAPIError::DatabaseError(_) => "How the heck u mess that up".to_string(),
GeoffreyAPIError::TokenNotAuthorized => "WHO ARE YOU????".to_string(),
GeoffreyAPIError::MultipleLocationsMatch => {
"I couldn't match a single location, narrow down your search".to_string()
}
GeoffreyAPIError::ParameterInvalid(err) => {
format!(
"Welp, you some how messed up the {} parameter, great job",
err
)
}
},
_ => {
println!("GeoffreyBot an unhandled error: {}", err);
format!("OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The admins at our \
headquarters are working VEWY HAWD to fix this! (Error in command {})", Self::command_name())
}
}
}
}
fn custom_err_resp(_: &CommandError) -> Option<String> {
None
}
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError>;
async fn process_arguments(
command_interaction: ApplicationCommandInteraction,
) -> Result<Self::ApiParams, CommandError>;
async fn run_command(
ctx: &GeoffreyContext,
user_id: UserID,
command_interact: ApplicationCommandInteraction,
) -> Result<String, CommandError> {
let mut args = Self::process_arguments(command_interact).await?;
args.set_token(ctx.cfg.api.token.clone());
args.set_user_id(user_id);
let resp = Self::run_api_query(ctx, args).await?;
match resp {
APIResponse::Response(resp) => Ok(Self::build_response(resp)),
APIResponse::Error { error: err, .. } => Err(CommandError::GeoffreyApi(err)),
}
}
async fn command(
ctx: &GeoffreyContext,
user_id: UserID,
command_interact: ApplicationCommandInteraction,
) -> String {
match Self::run_command(ctx, user_id, command_interact).await {
Ok(msg) => msg,
Err(e) => Self::get_err_resp(e),
}
}
fn build_response(resp: Self::ApiResp) -> String;
}

View File

@ -0,0 +1,74 @@
use crate::commands::bot_command::{BotCommand, CommandError};
use async_trait::async_trait;
use geoffrey_models::models::locations::Location;
use geoffrey_models::models::parameters::find_params::FindParams;
use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType,
};
use serenity::model::prelude::application_command::ApplicationCommandInteractionDataOptionValue;
use std::fmt::Write;
pub struct FindCommand;
#[async_trait]
impl BotCommand for FindCommand {
type ApiParams = FindParams;
type ApiResp = Vec<Location>;
fn command_name() -> String {
"find".to_string()
}
fn request_type() -> Method {
Method::GET
}
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| {
command
.name(Self::command_name())
.description("Find a location in Geoffrey.")
.create_option(|option| {
option
.name("query")
.description("The location name or player to lookup")
.kind(ApplicationCommandOptionType::String)
.required(true)
})
})
.await?;
Ok(command)
}
async fn process_arguments(
command_interaction: ApplicationCommandInteraction,
) -> Result<Self::ApiParams, CommandError> {
let options = command_interaction.data.options;
if let Some(option) = options.get(0) {
let query = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = query {
return Ok(FindParams::new(s.clone()));
}
}
Err(CommandError::ArgumentParse("query".to_string()))
}
fn build_response(resp: Self::ApiResp) -> String {
if resp.is_empty() {
"No locations match that query, try better next time ding dong".to_string()
} else {
let mut resp_str = String::new();
writeln!(resp_str, "The following locations match:").unwrap();
for loc in resp {
writeln!(resp_str, "**{}**, {}", loc.name, loc.position).unwrap();
}
resp_str
}
}
}

View File

@ -0,0 +1,28 @@
pub mod add_item;
pub mod add_location;
pub mod bot_command;
pub mod find;
pub mod selling;
use crate::commands::add_item::AddItemCommand;
use crate::commands::add_location::AddLocationCommand;
use crate::commands::bot_command::{BotCommand, CommandError};
use crate::commands::find::FindCommand;
use crate::commands::selling::SellingCommand;
use serenity::model::interactions::application_command::ApplicationCommand;
use serenity::prelude::*;
pub async fn create_commands(ctx: &Context) -> Result<Vec<ApplicationCommand>, CommandError> {
let mut commands: Vec<ApplicationCommand> = Vec::new();
for command in ApplicationCommand::get_global_application_commands(&ctx.http).await? {
ApplicationCommand::delete_global_application_command(&ctx.http, command.id).await?;
}
commands.push(FindCommand::create_app_command(ctx).await?);
commands.push(SellingCommand::create_app_command(ctx).await?);
commands.push(AddLocationCommand::create_app_command(ctx).await?);
commands.push(AddItemCommand::create_app_command(ctx).await?);
Ok(commands)
}

View File

@ -0,0 +1,127 @@
use crate::commands::bot_command::{BotCommand, CommandError};
use async_trait::async_trait;
use geoffrey_models::models::parameters::selling_params::{ItemSort, Order, SellingParams};
use geoffrey_models::models::response::selling_listing::SellingListing;
use reqwest::Method;
use serenity::client::Context;
use serenity::model::interactions::application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType,
};
use serenity::model::prelude::application_command::ApplicationCommandInteractionDataOptionValue;
use std::fmt::Write;
use std::str::FromStr;
pub struct SellingCommand;
#[async_trait]
impl BotCommand for SellingCommand {
type ApiParams = SellingParams;
type ApiResp = Vec<SellingListing>;
fn command_name() -> String {
"selling".to_string()
}
fn request_type() -> Method {
Method::GET
}
async fn create_app_command(ctx: &Context) -> Result<ApplicationCommand, CommandError> {
let command = ApplicationCommand::create_global_application_command(&ctx.http, |command| {
command
.name(Self::command_name())
.description("Find items for sale.")
.create_option(|option| {
option
.name("query")
.description("Item to find")
.kind(ApplicationCommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("sort")
.description("How to sort items")
.kind(ApplicationCommandOptionType::String)
.add_string_choice(ItemSort::Price, ItemSort::Price)
.add_string_choice(ItemSort::Restock, ItemSort::Restock)
.required(false)
})
.create_option(|option| {
option
.name("order")
.description("Order of the item Search")
.kind(ApplicationCommandOptionType::String)
.add_string_choice(Order::Low, Order::Low)
.add_string_choice(Order::High, Order::High)
.required(false)
})
})
.await?;
Ok(command)
}
async fn process_arguments(
command_interaction: ApplicationCommandInteraction,
) -> Result<Self::ApiParams, CommandError> {
let options = command_interaction.data.options;
let mut query = String::new();
let mut sort = None;
let mut order = None;
if let Some(option) = options.get(0) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = option {
query = s.clone();
} else {
return Err(CommandError::ArgumentParse("query".to_string()));
}
}
if let Some(option) = options.get(1) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = option {
sort = Some(ItemSort::from_str(s).unwrap());
} else {
return Err(CommandError::ArgumentParse("sort".to_string()));
}
}
if let Some(option) = options.get(2) {
let option = option.resolved.as_ref();
if let Some(ApplicationCommandInteractionDataOptionValue::String(s)) = option {
order = Some(Order::from_str(s).unwrap());
} else {
return Err(CommandError::ArgumentParse("order".to_string()));
}
}
Ok(SellingParams::new(query, sort, order))
}
fn build_response(resp: Self::ApiResp) -> String {
if resp.is_empty() {
"No shops were found selling that, maybe I should start selling it...".to_string()
} else {
let mut resp_str = String::new();
writeln!(resp_str, "The items match:").unwrap();
for item in resp {
writeln!(
resp_str,
"**{}**, {}D for {}: {} {}",
item.listing.item.name,
item.listing.count_per_price,
item.listing.count_per_price,
item.shop_name,
item.shop_loc
)
.unwrap();
}
resp_str
}
}
}

View File

@ -0,0 +1,30 @@
use config::{Config, ConfigError, File};
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize, Clone)]
pub struct DiscordConfig {
pub token: String,
pub app_id: u64,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ApiConfig {
pub token: String,
pub base_url: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct GeoffreyBotConfig {
pub api: ApiConfig,
pub discord: DiscordConfig,
}
impl GeoffreyBotConfig {
pub fn new(config_path: &Path) -> Result<Self, ConfigError> {
let mut cfg = Config::new();
cfg.merge(File::from(config_path.to_path_buf()))?;
cfg.try_into()
}
}

View File

@ -0,0 +1,12 @@
use crate::configs::GeoffreyBotConfig;
use serenity::prelude::TypeMapKey;
#[derive(Debug, Clone)]
pub struct GeoffreyContext {
pub http_client: reqwest::Client,
pub cfg: GeoffreyBotConfig,
}
impl TypeMapKey for GeoffreyContext {
type Value = GeoffreyContext;
}

View File

@ -0,0 +1,130 @@
mod commands;
mod configs;
mod context;
use crate::commands::add_item::AddItemCommand;
use crate::commands::add_location::AddLocationCommand;
use crate::commands::bot_command::BotCommand;
use crate::commands::create_commands;
use crate::commands::find::FindCommand;
use crate::commands::selling::SellingCommand;
use crate::configs::GeoffreyBotConfig;
use crate::context::GeoffreyContext;
use geoffrey_models::models::player::UserID;
use serenity::utils::{content_safe, ContentSafeOptions};
use serenity::{
async_trait,
model::{
gateway::Ready,
interactions::{Interaction, InteractionResponseType},
},
prelude::*,
};
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, StructOpt, Clone)]
#[structopt(name = "GeoffreyBot", about = "Geoffrey Discord Bot")]
struct Args {
#[structopt(env = "GEOFFREY_BOT_CONFIG", parse(from_os_str))]
config: PathBuf,
}
struct HttpClient;
impl TypeMapKey for HttpClient {
type Value = serenity::client::Client;
}
struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn ready(&self, ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
let commands = create_commands(&ctx).await.unwrap();
println!("The following commands have been registered:");
for command in commands {
println!(
"{}: {} - {:?}",
command.name, command.description, command.options
);
}
}
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
let data = ctx.data.read().await;
let geoffrey_ctx = data.get::<GeoffreyContext>().unwrap();
if let Interaction::ApplicationCommand(command) = interaction {
let user_id = UserID::DiscordUUID {
discord_uuid: command.user.id.0,
};
let msg = match command.data.name.as_str() {
"find" => FindCommand::command(geoffrey_ctx, user_id, command.clone()).await,
"selling" => SellingCommand::command(geoffrey_ctx, user_id, command.clone()).await,
"add_location" => {
AddLocationCommand::command(geoffrey_ctx, user_id, command.clone()).await
}
"add_item" => AddItemCommand::command(geoffrey_ctx, user_id, command.clone()).await,
_ => "not implemented :(".to_string(),
};
let msg = content_safe(&ctx.cache, &msg, &ContentSafeOptions::default()).await;
command
.create_interaction_response(&ctx.http, |resp| {
resp.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|message| message.content(msg))
})
.await
.unwrap();
} else if let Interaction::Autocomplete(auto_complete) = interaction {
auto_complete
.create_autocomplete_response(&ctx.http, |resp| resp)
.await
.unwrap()
} else if let Interaction::MessageComponent(msg) = interaction {
msg.create_interaction_response(&ctx.http, |resp| resp)
.await
.unwrap()
} else if let Interaction::Ping(_) = interaction {
println!("Ping recv'ed");
}
}
}
#[tokio::main]
async fn main() {
let args: Args = Args::from_args();
let cfg = match GeoffreyBotConfig::new(args.config.as_path()) {
Ok(cfg) => cfg,
Err(e) => {
println!("Error opening config: {}", e);
return;
}
};
let mut client = Client::builder(cfg.discord.token.clone())
.event_handler(Handler)
.application_id(cfg.discord.app_id)
.await
.expect("Error creating Geoffrey client");
{
let mut data = client.data.write().await;
data.insert::<GeoffreyContext>(GeoffreyContext {
http_client: reqwest::Client::new(),
cfg,
})
}
if let Err(why) = client.start().await {
println!("Client error: {:?}", why);
}
}

View File

@ -8,6 +8,8 @@ use crate::models::locations::town::{Town, TownDb};
use crate::models::player::Player;
use crate::models::{Position, Tunnel};
use crate::GeoffreyDatabaseModel;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
pub mod farm;
pub mod market;
@ -37,6 +39,39 @@ impl From<LocationDataDb> for LocationType {
}
}
impl Display for LocationType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let name = match self {
LocationType::Base => "Base",
LocationType::Shop => "Shop",
LocationType::Attraction => "Attraction",
LocationType::Town => "Town",
LocationType::Farm => "Farm",
LocationType::Market => "Market",
};
write!(f, "{}", name)
}
}
impl FromStr for LocationType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let t = match s.to_lowercase().as_str() {
"base" => LocationType::Base,
"shop" => LocationType::Shop,
"attraction" => LocationType::Attraction,
"town" => LocationType::Town,
"farm" => LocationType::Farm,
"market" => LocationType::Market,
&_ => return Err(format!("Location type invalid: '{}'", s)),
};
Ok(t)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum LocationDataDb {
Base,

View File

@ -1,4 +1,6 @@
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
pub mod item;
pub mod locations;
@ -21,6 +23,31 @@ impl Default for Dimension {
}
}
impl Display for Dimension {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let display_name = match self {
Dimension::Overworld => "Overworld",
Dimension::Nether => "Nether",
Dimension::TheEnd => "The End",
};
write!(f, "{}", display_name)
}
}
impl FromStr for Dimension {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
Ok(match s.as_str() {
"o" | "overworld" => Dimension::Overworld,
"n" | "nether" => Dimension::Nether,
"e" | "end" | "the end" => Dimension::TheEnd,
_ => return Err(format!("Unable to parse {} as dimension", s)),
})
}
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
pub enum Direction {
North,
@ -29,6 +56,19 @@ pub enum Direction {
West,
}
impl Display for Direction {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let display_name = match self {
Direction::North => "North",
Direction::East => "East",
Direction::South => "South",
Direction::West => "West",
};
write!(f, "{}", display_name)
}
}
#[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)]
pub struct Position {
pub x: i32,
@ -42,6 +82,12 @@ impl Position {
}
}
impl Display for Position {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} @ (x={}, z={}) ", self.dimension, self.x, self.y)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Tunnel {
direction: Direction,

View File

@ -12,6 +12,19 @@ pub struct AddItemParams {
pub shop: String,
}
impl AddItemParams {
pub fn new(item_name: String, price: u32, quantity: u32, shop: String) -> Self {
Self {
token: Default::default(),
user_id: Default::default(),
item_name,
price,
quantity,
shop,
}
}
}
impl CommandRequest for AddItemParams {
fn token(&self) -> String {
self.token.clone()
@ -20,4 +33,12 @@ impl CommandRequest for AddItemParams {
fn user_id(&self) -> Option<UserID> {
Some(self.user_id.clone())
}
fn set_token(&mut self, token: String) {
self.token = token;
}
fn set_user_id(&mut self, user_id: UserID) {
self.user_id = user_id;
}
}

View File

@ -14,6 +14,24 @@ pub struct AddLocationParams {
pub tunnel: Option<Tunnel>,
}
impl AddLocationParams {
pub fn new(
name: String,
position: Position,
loc_type: LocationType,
tunnel: Option<Tunnel>,
) -> Self {
Self {
token: Default::default(),
user_id: Default::default(),
name,
position,
loc_type,
tunnel,
}
}
}
impl CommandRequest for AddLocationParams {
fn token(&self) -> String {
self.token.clone()
@ -22,4 +40,12 @@ impl CommandRequest for AddLocationParams {
fn user_id(&self) -> Option<UserID> {
Some(self.user_id.clone())
}
fn set_user_id(&mut self, user_id: UserID) {
self.user_id = user_id;
}
fn set_token(&mut self, token: String) {
self.token = token;
}
}

View File

@ -1,5 +1,4 @@
use crate::models::parameters::CommandRequest;
use crate::models::player::UserID;
use crate::models::token::Permissions;
use serde::{Deserialize, Serialize};
@ -9,12 +8,21 @@ pub struct AddTokenParams {
pub permissions: Vec<Permissions>,
}
impl AddTokenParams {
fn new(permissions: Vec<Permissions>) -> Self {
Self {
token: Default::default(),
permissions,
}
}
}
impl CommandRequest for AddTokenParams {
fn token(&self) -> String {
self.token.clone()
}
fn user_id(&self) -> Option<UserID> {
None
fn set_token(&mut self, token: String) {
self.token = token;
}
}

View File

@ -7,8 +7,21 @@ pub struct FindParams {
pub query: String,
}
impl FindParams {
pub fn new(query: String) -> Self {
Self {
token: Default::default(),
query,
}
}
}
impl CommandRequest for FindParams {
fn token(&self) -> String {
self.token.clone()
}
fn set_token(&mut self, token: String) {
self.token = token;
}
}

View File

@ -12,12 +12,17 @@ use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt::Debug;
pub trait CommandRequest: Serialize + DeserializeOwned + Debug + Clone + Send + 'static {
pub trait CommandRequest:
Serialize + DeserializeOwned + Debug + Clone + Send + 'static + Sync
{
fn token(&self) -> String;
fn user_id(&self) -> Option<UserID> {
None
}
fn set_token(&mut self, token: String);
fn set_user_id(&mut self, _: UserID) {}
fn check_permission(
&self,
player: &Player,

View File

@ -9,8 +9,26 @@ pub struct RegisterParameters {
pub username: String,
}
impl RegisterParameters {
pub fn new(username: String) -> Self {
RegisterParameters {
token: Default::default(),
new_user_id: Default::default(),
username,
}
}
}
impl CommandRequest for RegisterParameters {
fn token(&self) -> String {
self.token.clone()
}
fn set_user_id(&mut self, user_id: UserID) {
self.new_user_id = user_id;
}
fn set_token(&mut self, token: String) {
self.token = token;
}
}

View File

@ -1,6 +1,8 @@
use crate::models::parameters::CommandRequest;
use crate::models::player::UserID;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum ItemSort {
@ -8,12 +10,62 @@ pub enum ItemSort {
Restock,
}
impl Display for ItemSort {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
ItemSort::Price => "Price",
ItemSort::Restock => "Restock Time",
};
write!(f, "{}", s)
}
}
impl FromStr for ItemSort {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let sort = match s.to_lowercase().as_str() {
"price" => ItemSort::Price,
"restock" => ItemSort::Restock,
&_ => return Err(format!("Unknown sort '{}'", s)),
};
Ok(sort)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum Order {
High,
Low,
}
impl Display for Order {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
Order::High => "High",
Order::Low => "Low",
};
write!(f, "{}", s)
}
}
impl FromStr for Order {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let order = match s.to_lowercase().as_str() {
"high" => Order::High,
"low" => Order::Low,
&_ => return Err(format!("Unknown sorting '{}'", s)),
};
Ok(order)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SellingParams {
pub token: String,
@ -22,6 +74,17 @@ pub struct SellingParams {
pub order: Option<Order>,
}
impl SellingParams {
pub fn new(query: String, sort: Option<ItemSort>, order: Option<Order>) -> Self {
Self {
token: Default::default(),
query,
sort,
order,
}
}
}
impl CommandRequest for SellingParams {
fn token(&self) -> String {
self.token.clone()
@ -30,4 +93,8 @@ impl CommandRequest for SellingParams {
fn user_id(&self) -> Option<UserID> {
None
}
fn set_token(&mut self, token: String) {
self.token = token;
}
}

View File

@ -6,6 +6,13 @@ use serde::{Deserialize, Serialize};
pub enum UserID {
DiscordUUID { discord_uuid: u64 },
MinecraftUUID { mc_uuid: String },
None,
}
impl Default for UserID {
fn default() -> Self {
Self::None
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]