Added link command
continuous-integration/woodpecker the build was successful Details

+ 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
main
Joey Hines 2021-12-18 11:13:18 -07:00
parent 37117ce5a9
commit 48be50dd67
No known key found for this signature in database
GPG Key ID: 80F567B5C968F91B
15 changed files with 238 additions and 12 deletions

1
Cargo.lock generated
View File

@ -421,6 +421,7 @@ dependencies = [
name = "geoffrey_api" name = "geoffrey_api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"config", "config",
"geoffrey_db", "geoffrey_db",
"geoffrey_models", "geoffrey_models",

View File

@ -18,4 +18,5 @@ structopt = "0.3.21"
log = "0.4.14" log = "0.4.14"
simple_logger = "1.13.0" simple_logger = "1.13.0"
rand = "0.8.4" rand = "0.8.4"
regex = "1.5.4" regex = "1.5.4"
chrono = { version = "0.4.19", features = ["serde"] }

View File

@ -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<Context>,
_req: &Self::Req,
player: Option<Player>,
) -> Result<Self::Resp> {
let player = player.unwrap();
let links: Vec<Link> = ctx
.db
.filter(|_, link: &Link| player.id().unwrap() == link.player_id)?
.collect();
for link in links {
ctx.db.remove::<Link>(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)
}
}

View File

@ -2,6 +2,7 @@ use crate::commands::add_item::AddItem;
use crate::commands::add_location::AddLocation; use crate::commands::add_location::AddLocation;
use crate::commands::delete::Delete; use crate::commands::delete::Delete;
use crate::commands::find::FindCommand; use crate::commands::find::FindCommand;
use crate::commands::link::LinkCommand;
use crate::commands::register::Register; use crate::commands::register::Register;
use crate::commands::selling::Selling; use crate::commands::selling::Selling;
use crate::commands::set_portal::SetPortal; use crate::commands::set_portal::SetPortal;
@ -26,6 +27,7 @@ pub mod add_location;
pub mod add_token; pub mod add_token;
pub mod delete; pub mod delete;
pub mod find; pub mod find;
pub mod link;
pub mod register; pub mod register;
pub mod selling; pub mod selling;
pub mod set_portal; pub mod set_portal;
@ -131,6 +133,7 @@ pub fn command_filter(
.or(create_command_filter::<Selling>(ctx.clone())) .or(create_command_filter::<Selling>(ctx.clone()))
.or(create_command_filter::<AddItem>(ctx.clone())) .or(create_command_filter::<AddItem>(ctx.clone()))
.or(create_command_filter::<Delete>(ctx.clone())) .or(create_command_filter::<Delete>(ctx.clone()))
.or(create_command_filter::<LinkCommand>(ctx.clone()))
.or(create_command_filter::<SetPortal>(ctx)), .or(create_command_filter::<SetPortal>(ctx)),
) )
} }

View File

@ -1,9 +1,12 @@
use crate::commands::{Command, RequestType}; use crate::commands::{Command, RequestType};
use crate::context::Context; 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::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::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::CommandLevel; use geoffrey_models::models::CommandLevel;
use geoffrey_models::GeoffreyDatabaseModel;
use std::sync::Arc; use std::sync::Arc;
pub struct Register {} pub struct Register {}
@ -24,11 +27,30 @@ impl Command for Register {
CommandLevel::ALL CommandLevel::ALL
} }
fn run_command( fn run_command(ctx: Arc<Context>, req: &Self::Req, _: Option<Player>) -> Result<Self::Resp> {
ctx: Arc<Context>, if let Some(link_code) = &req.link_code {
req: &Self::Req, let link: Option<Link> = ctx
_: Option<Player>, .db
) -> crate::Result<Self::Resp> { .filter(|_, link: &Link| link.is_valid(link_code.clone()))?
.next();
return if let Some(link) = link {
ctx.db.remove::<Link>(link.id().unwrap())?;
let mut player = ctx.db.get::<Player>(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()); let player = Player::new(req.username.as_str(), req.user_id.clone());
ctx.db.insert(player).map_err(GeoffreyAPIError::from) ctx.db.insert(player).map_err(GeoffreyAPIError::from)

View File

@ -21,6 +21,7 @@ pub mod add_item;
pub mod add_location; pub mod add_location;
pub mod delete; pub mod delete;
pub mod find; pub mod find;
pub mod register;
pub mod selling; pub mod selling;
pub mod set_portal; pub mod set_portal;

View File

@ -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<Self::ApiParams, CommandError> {
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
)
}
}

View File

@ -2,6 +2,7 @@ use serenity::model::interactions::application_command::ApplicationCommand;
use serenity::prelude::*; use serenity::prelude::*;
use crate::bot::commands::delete::DeleteCommand; use crate::bot::commands::delete::DeleteCommand;
use crate::bot::commands::register::RegisterCommand;
use crate::bot::commands::GeoffreyCommandFn; use crate::bot::commands::GeoffreyCommandFn;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use commands::add_item::AddItemCommand; use commands::add_item::AddItemCommand;
@ -80,6 +81,8 @@ pub async fn build_commands(
.add_command::<SetPortalCommand>(ctx) .add_command::<SetPortalCommand>(ctx)
.await? .await?
.add_command::<DeleteCommand>(ctx) .add_command::<DeleteCommand>(ctx)
.await?
.add_command::<RegisterCommand>(ctx)
.await?; .await?;
Ok(()) Ok(())

View File

@ -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<u64>,
pub player_id: u64,
pub link_code: String,
pub expires: DateTime<Utc>,
}
impl Link {
pub fn new(player_id: u64, link_code: String, expires: DateTime<Utc>) -> 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<u64> {
self.id
}
fn set_id(&mut self, id: u64) {
self.id = Some(id);
}
fn tree() -> String {
"link".to_string()
}
}

View File

@ -4,6 +4,7 @@ use std::str::FromStr;
pub mod db_metadata; pub mod db_metadata;
pub mod item; pub mod item;
pub mod link;
pub mod locations; pub mod locations;
pub mod meta; pub mod meta;
pub mod parameters; pub mod parameters;

View File

@ -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 {}

View File

@ -3,6 +3,7 @@ pub mod add_location_params;
pub mod add_token_params; pub mod add_token_params;
pub mod delete_params; pub mod delete_params;
pub mod find_params; pub mod find_params;
pub mod link_params;
pub mod register_params; pub mod register_params;
pub mod selling_params; pub mod selling_params;
pub mod set_portal_params; pub mod set_portal_params;

View File

@ -6,11 +6,16 @@ use serde::{Deserialize, Serialize};
pub struct RegisterParameters { pub struct RegisterParameters {
pub username: String, pub username: String,
pub user_id: UserID, pub user_id: UserID,
pub link_code: Option<String>,
} }
impl RegisterParameters { impl RegisterParameters {
pub fn new(username: String, user_id: UserID) -> Self { pub fn new(username: String, user_id: UserID, link_code: Option<String>) -> Self {
RegisterParameters { username, user_id } RegisterParameters {
username,
user_id,
link_code,
}
} }
} }

View File

@ -1,6 +1,7 @@
use crate::models::CommandLevel; use crate::models::CommandLevel;
use crate::GeoffreyDatabaseModel; use crate::GeoffreyDatabaseModel;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
pub enum UserID { 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 struct Player {
pub id: Option<u64>, pub id: Option<u64>,
pub name: String, pub name: String,
pub user_ids: Vec<UserID>, pub user_ids: HashSet<UserID>,
pub auth_level: CommandLevel, pub auth_level: CommandLevel,
} }
impl Player { impl Player {
pub fn new(name: &str, user_id: UserID) -> Self { pub fn new(name: &str, user_id: UserID) -> Self {
let mut user_ids = HashSet::new();
user_ids.insert(user_id);
Self { Self {
id: None, id: None,
name: name.to_string(), name: name.to_string(),
user_ids: vec![user_id], user_ids,
auth_level: CommandLevel::REGISTERED, auth_level: CommandLevel::REGISTERED,
} }
} }

View File

@ -11,6 +11,8 @@ pub enum GeoffreyAPIError {
TokenNotAuthorized, TokenNotAuthorized,
MultipleLocationsMatch, MultipleLocationsMatch,
ParameterInvalid(String), ParameterInvalid(String),
PlayerRegistrationWithoutMCUUID,
AccountLinkInvalid,
} }
impl Display for GeoffreyAPIError { impl Display for GeoffreyAPIError {
@ -34,6 +36,12 @@ impl Display for GeoffreyAPIError {
GeoffreyAPIError::ParameterInvalid(param) => { GeoffreyAPIError::ParameterInvalid(param) => {
format!("Parameter \"{}\" is invalid", 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) write!(f, "{}", string)
} }