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 + fmtmain
parent
bc4936febd
commit
7fb4054cdb
File diff suppressed because it is too large
Load Diff
|
@ -2,5 +2,6 @@
|
||||||
members = [
|
members = [
|
||||||
"geoffrey_models",
|
"geoffrey_models",
|
||||||
"geoffrey_db",
|
"geoffrey_db",
|
||||||
"geoffrey_api"
|
"geoffrey_api",
|
||||||
|
"geoffrey_bot"
|
||||||
]
|
]
|
|
@ -0,0 +1,7 @@
|
||||||
|
[api]
|
||||||
|
token = "0Bxpl0N4wBYHtgfwdtnaFPblQKa4Em9wIAGp6q0TV5X6M2HzuPsjfcCKxXddDny2"
|
||||||
|
base_url = "http://localhost:8080/"
|
||||||
|
|
||||||
|
[discord]
|
||||||
|
token = "OTExNDQ1MDY3MTMzMjQ3NDk4.YZhfXQ.idsLVIgipSU1SJUnG_vkhZgFOMg"
|
||||||
|
app_id = 911445067133247498
|
|
@ -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"
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(¶ms)
|
||||||
|
.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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ use crate::models::locations::town::{Town, TownDb};
|
||||||
use crate::models::player::Player;
|
use crate::models::player::Player;
|
||||||
use crate::models::{Position, Tunnel};
|
use crate::models::{Position, Tunnel};
|
||||||
use crate::GeoffreyDatabaseModel;
|
use crate::GeoffreyDatabaseModel;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
pub mod farm;
|
pub mod farm;
|
||||||
pub mod market;
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum LocationDataDb {
|
pub enum LocationDataDb {
|
||||||
Base,
|
Base,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
pub mod item;
|
pub mod item;
|
||||||
pub mod locations;
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
North,
|
North,
|
||||||
|
@ -29,6 +56,19 @@ pub enum Direction {
|
||||||
West,
|
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)]
|
#[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)]
|
||||||
pub struct Position {
|
pub struct Position {
|
||||||
pub x: i32,
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Tunnel {
|
pub struct Tunnel {
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
|
|
@ -12,6 +12,19 @@ pub struct AddItemParams {
|
||||||
pub shop: String,
|
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 {
|
impl CommandRequest for AddItemParams {
|
||||||
fn token(&self) -> String {
|
fn token(&self) -> String {
|
||||||
self.token.clone()
|
self.token.clone()
|
||||||
|
@ -20,4 +33,12 @@ impl CommandRequest for AddItemParams {
|
||||||
fn user_id(&self) -> Option<UserID> {
|
fn user_id(&self) -> Option<UserID> {
|
||||||
Some(self.user_id.clone())
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,24 @@ pub struct AddLocationParams {
|
||||||
pub tunnel: Option<Tunnel>,
|
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 {
|
impl CommandRequest for AddLocationParams {
|
||||||
fn token(&self) -> String {
|
fn token(&self) -> String {
|
||||||
self.token.clone()
|
self.token.clone()
|
||||||
|
@ -22,4 +40,12 @@ impl CommandRequest for AddLocationParams {
|
||||||
fn user_id(&self) -> Option<UserID> {
|
fn user_id(&self) -> Option<UserID> {
|
||||||
Some(self.user_id.clone())
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::models::parameters::CommandRequest;
|
use crate::models::parameters::CommandRequest;
|
||||||
use crate::models::player::UserID;
|
|
||||||
use crate::models::token::Permissions;
|
use crate::models::token::Permissions;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -9,12 +8,21 @@ pub struct AddTokenParams {
|
||||||
pub permissions: Vec<Permissions>,
|
pub permissions: Vec<Permissions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AddTokenParams {
|
||||||
|
fn new(permissions: Vec<Permissions>) -> Self {
|
||||||
|
Self {
|
||||||
|
token: Default::default(),
|
||||||
|
permissions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl CommandRequest for AddTokenParams {
|
impl CommandRequest for AddTokenParams {
|
||||||
fn token(&self) -> String {
|
fn token(&self) -> String {
|
||||||
self.token.clone()
|
self.token.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_id(&self) -> Option<UserID> {
|
fn set_token(&mut self, token: String) {
|
||||||
None
|
self.token = token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,21 @@ pub struct FindParams {
|
||||||
pub query: String,
|
pub query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FindParams {
|
||||||
|
pub fn new(query: String) -> Self {
|
||||||
|
Self {
|
||||||
|
token: Default::default(),
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl CommandRequest for FindParams {
|
impl CommandRequest for FindParams {
|
||||||
fn token(&self) -> String {
|
fn token(&self) -> String {
|
||||||
self.token.clone()
|
self.token.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_token(&mut self, token: String) {
|
||||||
|
self.token = token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,17 @@ use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fmt::Debug;
|
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 token(&self) -> String;
|
||||||
fn user_id(&self) -> Option<UserID> {
|
fn user_id(&self) -> Option<UserID> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_token(&mut self, token: String);
|
||||||
|
fn set_user_id(&mut self, _: UserID) {}
|
||||||
|
|
||||||
fn check_permission(
|
fn check_permission(
|
||||||
&self,
|
&self,
|
||||||
player: &Player,
|
player: &Player,
|
||||||
|
|
|
@ -9,8 +9,26 @@ pub struct RegisterParameters {
|
||||||
pub username: String,
|
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 {
|
impl CommandRequest for RegisterParameters {
|
||||||
fn token(&self) -> String {
|
fn token(&self) -> String {
|
||||||
self.token.clone()
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::models::parameters::CommandRequest;
|
use crate::models::parameters::CommandRequest;
|
||||||
use crate::models::player::UserID;
|
use crate::models::player::UserID;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub enum ItemSort {
|
pub enum ItemSort {
|
||||||
|
@ -8,12 +10,62 @@ pub enum ItemSort {
|
||||||
Restock,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub enum Order {
|
pub enum Order {
|
||||||
High,
|
High,
|
||||||
Low,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct SellingParams {
|
pub struct SellingParams {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
|
@ -22,6 +74,17 @@ pub struct SellingParams {
|
||||||
pub order: Option<Order>,
|
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 {
|
impl CommandRequest for SellingParams {
|
||||||
fn token(&self) -> String {
|
fn token(&self) -> String {
|
||||||
self.token.clone()
|
self.token.clone()
|
||||||
|
@ -30,4 +93,8 @@ impl CommandRequest for SellingParams {
|
||||||
fn user_id(&self) -> Option<UserID> {
|
fn user_id(&self) -> Option<UserID> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_token(&mut self, token: String) {
|
||||||
|
self.token = token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,13 @@ use serde::{Deserialize, Serialize};
|
||||||
pub enum UserID {
|
pub enum UserID {
|
||||||
DiscordUUID { discord_uuid: u64 },
|
DiscordUUID { discord_uuid: u64 },
|
||||||
MinecraftUUID { mc_uuid: String },
|
MinecraftUUID { mc_uuid: String },
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UserID {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
|
|
Loading…
Reference in New Issue