From 48be50dd67c070187ec7339cb2f248a71c87f028 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sat, 18 Dec 2021 11:13:18 -0700 Subject: [PATCH] Added link command + Link provides a link code that a user can use to link other accounts + This places the main auth source into MC and the plugin + Refactored register to accept a link code + Clippy + fmt --- Cargo.lock | 1 + geoffrey_api/Cargo.toml | 3 +- geoffrey_api/src/commands/link.rs | 62 +++++++++++++++++ geoffrey_api/src/commands/mod.rs | 3 + geoffrey_api/src/commands/register.rs | 34 ++++++++-- geoffrey_bot/src/bot/commands/mod.rs | 1 + geoffrey_bot/src/bot/commands/register.rs | 66 +++++++++++++++++++ geoffrey_bot/src/bot/mod.rs | 3 + geoffrey_models/src/models/link.rs | 40 +++++++++++ geoffrey_models/src/models/mod.rs | 1 + .../src/models/parameters/link_params.rs | 9 +++ geoffrey_models/src/models/parameters/mod.rs | 1 + .../src/models/parameters/register_params.rs | 9 ++- geoffrey_models/src/models/player.rs | 9 ++- .../src/models/response/api_error.rs | 8 +++ 15 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 geoffrey_api/src/commands/link.rs create mode 100644 geoffrey_bot/src/bot/commands/register.rs create mode 100644 geoffrey_models/src/models/link.rs create mode 100644 geoffrey_models/src/models/parameters/link_params.rs diff --git a/Cargo.lock b/Cargo.lock index 98a3f09..f7a582e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -421,6 +421,7 @@ dependencies = [ name = "geoffrey_api" version = "0.1.0" dependencies = [ + "chrono", "config", "geoffrey_db", "geoffrey_models", diff --git a/geoffrey_api/Cargo.toml b/geoffrey_api/Cargo.toml index 863299e..8e7a2a9 100644 --- a/geoffrey_api/Cargo.toml +++ b/geoffrey_api/Cargo.toml @@ -18,4 +18,5 @@ structopt = "0.3.21" log = "0.4.14" simple_logger = "1.13.0" rand = "0.8.4" -regex = "1.5.4" \ No newline at end of file +regex = "1.5.4" +chrono = { version = "0.4.19", features = ["serde"] } diff --git a/geoffrey_api/src/commands/link.rs b/geoffrey_api/src/commands/link.rs new file mode 100644 index 0000000..0fd28de --- /dev/null +++ b/geoffrey_api/src/commands/link.rs @@ -0,0 +1,62 @@ +use crate::commands::{Command, RequestType}; +use crate::context::Context; +use crate::Result; +use chrono::{Duration, Utc}; +use geoffrey_models::models::link::Link; +use geoffrey_models::models::parameters::link_params::LinkParameters; +use geoffrey_models::models::player::Player; +use geoffrey_models::models::CommandLevel; +use geoffrey_models::GeoffreyDatabaseModel; +use rand::distributions::Alphanumeric; +use rand::Rng; +use std::sync::Arc; + +pub struct LinkCommand {} + +impl Command for LinkCommand { + type Req = LinkParameters; + type Resp = Link; + + fn command_name() -> String { + "link".to_string() + } + + fn request_type() -> RequestType { + RequestType::POST + } + + fn command_level() -> CommandLevel { + CommandLevel::REGISTERED + } + + fn run_command( + ctx: Arc, + _req: &Self::Req, + player: Option, + ) -> Result { + let player = player.unwrap(); + + let links: Vec = ctx + .db + .filter(|_, link: &Link| player.id().unwrap() == link.player_id)? + .collect(); + + for link in links { + ctx.db.remove::(link.id().unwrap())?; + } + + let link_code: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); + + let expire_time = Utc::now() + Duration::days(1); + + let link = Link::new(player.id.unwrap(), link_code, expire_time); + + let link = ctx.db.insert(link)?; + + Ok(link) + } +} diff --git a/geoffrey_api/src/commands/mod.rs b/geoffrey_api/src/commands/mod.rs index 4b279ba..acf8fa3 100644 --- a/geoffrey_api/src/commands/mod.rs +++ b/geoffrey_api/src/commands/mod.rs @@ -2,6 +2,7 @@ use crate::commands::add_item::AddItem; use crate::commands::add_location::AddLocation; use crate::commands::delete::Delete; use crate::commands::find::FindCommand; +use crate::commands::link::LinkCommand; use crate::commands::register::Register; use crate::commands::selling::Selling; use crate::commands::set_portal::SetPortal; @@ -26,6 +27,7 @@ pub mod add_location; pub mod add_token; pub mod delete; pub mod find; +pub mod link; pub mod register; pub mod selling; pub mod set_portal; @@ -131,6 +133,7 @@ pub fn command_filter( .or(create_command_filter::(ctx.clone())) .or(create_command_filter::(ctx.clone())) .or(create_command_filter::(ctx.clone())) + .or(create_command_filter::(ctx.clone())) .or(create_command_filter::(ctx)), ) } diff --git a/geoffrey_api/src/commands/register.rs b/geoffrey_api/src/commands/register.rs index 3bcccfc..8c7b4fd 100644 --- a/geoffrey_api/src/commands/register.rs +++ b/geoffrey_api/src/commands/register.rs @@ -1,9 +1,12 @@ use crate::commands::{Command, RequestType}; use crate::context::Context; +use crate::Result; +use geoffrey_models::models::link::Link; use geoffrey_models::models::parameters::register_params::RegisterParameters; -use geoffrey_models::models::player::Player; +use geoffrey_models::models::player::{Player, UserID}; use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::CommandLevel; +use geoffrey_models::GeoffreyDatabaseModel; use std::sync::Arc; pub struct Register {} @@ -24,11 +27,30 @@ impl Command for Register { CommandLevel::ALL } - fn run_command( - ctx: Arc, - req: &Self::Req, - _: Option, - ) -> crate::Result { + fn run_command(ctx: Arc, req: &Self::Req, _: Option) -> Result { + if let Some(link_code) = &req.link_code { + let link: Option = ctx + .db + .filter(|_, link: &Link| link.is_valid(link_code.clone()))? + .next(); + + return if let Some(link) = link { + ctx.db.remove::(link.id().unwrap())?; + + let mut player = ctx.db.get::(link.player_id)?; + + player.user_ids.insert(req.user_id.clone()); + + ctx.db.insert(player).map_err(GeoffreyAPIError::from) + } else { + Err(GeoffreyAPIError::AccountLinkInvalid) + }; + } + + if !matches!(&req.user_id, UserID::MinecraftUUID { mc_uuid: _ }) { + return Err(GeoffreyAPIError::PlayerRegistrationWithoutMCUUID); + } + let player = Player::new(req.username.as_str(), req.user_id.clone()); ctx.db.insert(player).map_err(GeoffreyAPIError::from) diff --git a/geoffrey_bot/src/bot/commands/mod.rs b/geoffrey_bot/src/bot/commands/mod.rs index 685b4f4..3cdd32e 100644 --- a/geoffrey_bot/src/bot/commands/mod.rs +++ b/geoffrey_bot/src/bot/commands/mod.rs @@ -21,6 +21,7 @@ pub mod add_item; pub mod add_location; pub mod delete; pub mod find; +pub mod register; pub mod selling; pub mod set_portal; diff --git a/geoffrey_bot/src/bot/commands/register.rs b/geoffrey_bot/src/bot/commands/register.rs new file mode 100644 index 0000000..984c907 --- /dev/null +++ b/geoffrey_bot/src/bot/commands/register.rs @@ -0,0 +1,66 @@ +use async_trait::async_trait; +use reqwest::Method; +use serenity::model::interactions::application_command::{ + ApplicationCommandInteraction, ApplicationCommandOptionType, +}; + +use crate::bot::arg_parse::option_to_string; +use crate::bot::commands::{BotCommand, CommandError}; +use geoffrey_models::models::parameters::register_params::RegisterParameters; +use geoffrey_models::models::player::{Player, UserID}; +use serenity::builder::CreateApplicationCommand; + +pub struct RegisterCommand; + +#[async_trait] +impl BotCommand for RegisterCommand { + type ApiParams = RegisterParameters; + type ApiResp = Player; + + fn command_name() -> String { + "register".to_string() + } + + fn request_type() -> Method { + Method::POST + } + + fn create_app_command(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { + command + .name(Self::command_name()) + .description("Link your discord account to a geoffrey account") + .create_option(|option| { + option + .name("link_code") + .description("Link code give to you by Geoffrey in-game") + .kind(ApplicationCommandOptionType::String) + .required(true) + }) + } + + async fn process_arguments( + command_interaction: ApplicationCommandInteraction, + ) -> Result { + let options = command_interaction.data.options; + let link_code = option_to_string(options.get(0), "link_code")?; + + let discord_user_id = UserID::DiscordUUID { + discord_uuid: command_interaction.user.id.0, + }; + + let register = RegisterParameters::new( + command_interaction.user.name, + discord_user_id, + Some(link_code), + ); + + Ok(register) + } + + fn build_response(resp: Self::ApiResp) -> String { + format!( + "**{}**, you have been registered for the Geoffrey bot!", + resp.name + ) + } +} diff --git a/geoffrey_bot/src/bot/mod.rs b/geoffrey_bot/src/bot/mod.rs index d6d20b3..f68c106 100644 --- a/geoffrey_bot/src/bot/mod.rs +++ b/geoffrey_bot/src/bot/mod.rs @@ -2,6 +2,7 @@ use serenity::model::interactions::application_command::ApplicationCommand; use serenity::prelude::*; use crate::bot::commands::delete::DeleteCommand; +use crate::bot::commands::register::RegisterCommand; use crate::bot::commands::GeoffreyCommandFn; use crate::context::GeoffreyContext; use commands::add_item::AddItemCommand; @@ -80,6 +81,8 @@ pub async fn build_commands( .add_command::(ctx) .await? .add_command::(ctx) + .await? + .add_command::(ctx) .await?; Ok(()) diff --git a/geoffrey_models/src/models/link.rs b/geoffrey_models/src/models/link.rs new file mode 100644 index 0000000..ff7fbc6 --- /dev/null +++ b/geoffrey_models/src/models/link.rs @@ -0,0 +1,40 @@ +use crate::GeoffreyDatabaseModel; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Link { + id: Option, + pub player_id: u64, + pub link_code: String, + pub expires: DateTime, +} + +impl Link { + pub fn new(player_id: u64, link_code: String, expires: DateTime) -> Self { + Self { + id: None, + player_id, + link_code, + expires, + } + } + + pub fn is_valid(&self, link_code: String) -> bool { + self.expires > Utc::now() && self.link_code == link_code + } +} + +impl GeoffreyDatabaseModel for Link { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id); + } + + fn tree() -> String { + "link".to_string() + } +} diff --git a/geoffrey_models/src/models/mod.rs b/geoffrey_models/src/models/mod.rs index a6798f1..0745546 100644 --- a/geoffrey_models/src/models/mod.rs +++ b/geoffrey_models/src/models/mod.rs @@ -4,6 +4,7 @@ use std::str::FromStr; pub mod db_metadata; pub mod item; +pub mod link; pub mod locations; pub mod meta; pub mod parameters; diff --git a/geoffrey_models/src/models/parameters/link_params.rs b/geoffrey_models/src/models/parameters/link_params.rs new file mode 100644 index 0000000..683352e --- /dev/null +++ b/geoffrey_models/src/models/parameters/link_params.rs @@ -0,0 +1,9 @@ +use crate::models::parameters::GeoffreyParam; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LinkParameters {} + +impl LinkParameters {} + +impl GeoffreyParam for LinkParameters {} diff --git a/geoffrey_models/src/models/parameters/mod.rs b/geoffrey_models/src/models/parameters/mod.rs index ac66291..e9e5639 100644 --- a/geoffrey_models/src/models/parameters/mod.rs +++ b/geoffrey_models/src/models/parameters/mod.rs @@ -3,6 +3,7 @@ pub mod add_location_params; pub mod add_token_params; pub mod delete_params; pub mod find_params; +pub mod link_params; pub mod register_params; pub mod selling_params; pub mod set_portal_params; diff --git a/geoffrey_models/src/models/parameters/register_params.rs b/geoffrey_models/src/models/parameters/register_params.rs index c21ba7a..c1d57f1 100644 --- a/geoffrey_models/src/models/parameters/register_params.rs +++ b/geoffrey_models/src/models/parameters/register_params.rs @@ -6,11 +6,16 @@ use serde::{Deserialize, Serialize}; pub struct RegisterParameters { pub username: String, pub user_id: UserID, + pub link_code: Option, } impl RegisterParameters { - pub fn new(username: String, user_id: UserID) -> Self { - RegisterParameters { username, user_id } + pub fn new(username: String, user_id: UserID, link_code: Option) -> Self { + RegisterParameters { + username, + user_id, + link_code, + } } } diff --git a/geoffrey_models/src/models/player.rs b/geoffrey_models/src/models/player.rs index f8e3119..5a934c0 100644 --- a/geoffrey_models/src/models/player.rs +++ b/geoffrey_models/src/models/player.rs @@ -1,6 +1,7 @@ use crate::models::CommandLevel; use crate::GeoffreyDatabaseModel; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)] pub enum UserID { @@ -15,20 +16,22 @@ impl Default for UserID { } } -#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct Player { pub id: Option, pub name: String, - pub user_ids: Vec, + pub user_ids: HashSet, pub auth_level: CommandLevel, } impl Player { pub fn new(name: &str, user_id: UserID) -> Self { + let mut user_ids = HashSet::new(); + user_ids.insert(user_id); Self { id: None, name: name.to_string(), - user_ids: vec![user_id], + user_ids, auth_level: CommandLevel::REGISTERED, } } diff --git a/geoffrey_models/src/models/response/api_error.rs b/geoffrey_models/src/models/response/api_error.rs index 55c2121..94e85c0 100644 --- a/geoffrey_models/src/models/response/api_error.rs +++ b/geoffrey_models/src/models/response/api_error.rs @@ -11,6 +11,8 @@ pub enum GeoffreyAPIError { TokenNotAuthorized, MultipleLocationsMatch, ParameterInvalid(String), + PlayerRegistrationWithoutMCUUID, + AccountLinkInvalid, } impl Display for GeoffreyAPIError { @@ -34,6 +36,12 @@ impl Display for GeoffreyAPIError { GeoffreyAPIError::ParameterInvalid(param) => { format!("Parameter \"{}\" is invalid", param) } + GeoffreyAPIError::PlayerRegistrationWithoutMCUUID => { + "Players can only registered with a MC UUID".to_string() + } + GeoffreyAPIError::AccountLinkInvalid => { + "The supplied account link code is invalid".to_string() + } }; write!(f, "{}", string) }