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 = [
|
||||
"geoffrey_models",
|
||||
"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::{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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in New Issue