Start system to accept required terms to continue.
This commit is contained in:
parent
4055a856f4
commit
16bd49f160
@ -8,8 +8,8 @@ use uuid::Uuid;
|
||||
use tokio_postgres::NoTls;
|
||||
use crate::message_handler::ListenerSession;
|
||||
use crate::DResult;
|
||||
use crate::models::session::Session;
|
||||
use crate::models::user::User;
|
||||
use crate::models::{session::Session, user::User, item::Item};
|
||||
|
||||
use serde_json;
|
||||
use futures::FutureExt;
|
||||
|
||||
@ -193,9 +193,43 @@ impl DBTrans {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_username(self: &Self, username: &str) -> DResult<Option<User>> {
|
||||
if let Some(details_json) = self.pg_trans()?
|
||||
.query_opt("SELECT details FROM users WHERE username=$1",
|
||||
&[&username.to_lowercase()]).await? {
|
||||
return Ok(Some(serde_json::from_value(details_json.get("details"))?))
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn create_item(self: &Self, item: &Item) -> DResult<i64> {
|
||||
Ok(self.pg_trans()?.query_one("INSERT INTO items (details) VALUES ($1) RETURNING item_id",
|
||||
&[&serde_json::to_value(item)?]).await?
|
||||
.get("item_id"))
|
||||
}
|
||||
|
||||
pub async fn create_user(self: &Self, session: &ListenerSession, user_dat: &User) -> DResult<()> {
|
||||
self.pg_trans()?.execute("INSERT INTO users (\
|
||||
username, current_session, current_listener, details\
|
||||
) VALUES ($1, $2, $3, $4)", &[&user_dat.username.to_lowercase(),
|
||||
&session.session,
|
||||
&session.listener,
|
||||
&serde_json::to_value(user_dat)?]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_user_model(self: &Self, details: &User)
|
||||
-> DResult<()> {
|
||||
self.pg_trans()?
|
||||
.execute("UPDATE users SET details = $1 WHERE username = $2",
|
||||
&[&serde_json::to_value(details)?,
|
||||
&details.username.to_lowercase()]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn commit(mut self: Self) -> DResult<()> {
|
||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
||||
for trans in trans_opt {
|
||||
if let Some(trans) = trans_opt {
|
||||
trans.commit().await?;
|
||||
}
|
||||
Ok(())
|
||||
|
@ -13,6 +13,7 @@ mod help;
|
||||
mod quit;
|
||||
mod less_explicit_mode;
|
||||
mod register;
|
||||
mod agree;
|
||||
|
||||
pub struct VerbContext<'l> {
|
||||
session: &'l ListenerSession,
|
||||
@ -34,6 +35,13 @@ pub trait UserVerb {
|
||||
|
||||
pub type UResult<A> = Result<A, CommandHandlingError>;
|
||||
|
||||
|
||||
impl From<&str> for CommandHandlingError {
|
||||
fn from(input: &str) -> CommandHandlingError {
|
||||
SystemError(Box::from(input))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn std::error::Error + Send + Sync>> for CommandHandlingError {
|
||||
fn from(input: Box<dyn std::error::Error + Send + Sync>) -> CommandHandlingError {
|
||||
SystemError(input)
|
||||
@ -58,14 +66,26 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
"less_explicit_mode" => less_explicit_mode::VERB,
|
||||
"register" => register::VERB,
|
||||
"agree" => agree::VERB
|
||||
};
|
||||
|
||||
static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
};
|
||||
|
||||
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
|
||||
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
|
||||
|
||||
match ctx.user_dat {
|
||||
None => { result = result.or_else(|| UNREGISTERED_COMMANDS.get(cmd)); }
|
||||
Some(_) => {}
|
||||
match &ctx.user_dat {
|
||||
None => {
|
||||
result = result.or_else(|| UNREGISTERED_COMMANDS.get(cmd));
|
||||
}
|
||||
Some(user_dat) => {
|
||||
if user_dat.terms.terms_complete {
|
||||
result = result.or_else(|| REGISTERED_COMMANDS.get(cmd));
|
||||
} else if cmd == "agree" {
|
||||
result = Some(&agree::VERB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
@ -95,17 +115,19 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes
|
||||
"That's not a command I know. Try <bold>help<reset>\r\n"
|
||||
))
|
||||
).await?;
|
||||
trans.commit().await?;
|
||||
}
|
||||
Some(handler) => {
|
||||
match handler.handle(&mut ctx, cmd, params).await {
|
||||
Ok(()) => {}
|
||||
Ok(()) => {
|
||||
trans.commit().await?;
|
||||
}
|
||||
Err(UserError(err_msg)) => {
|
||||
trans.queue_for_session(session, Some(&(err_msg + "\r\n"))).await?;
|
||||
pool.queue_for_session(session, Some(&(err_msg + "\r\n"))).await?;
|
||||
}
|
||||
Err(SystemError(e)) => Err(e)?
|
||||
}
|
||||
}
|
||||
}
|
||||
trans.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
96
blastmud_game/src/message_handler/user_commands/agree.rs
Normal file
96
blastmud_game/src/message_handler/user_commands/agree.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
|
||||
use crate::models::user::{User, UserTermData};
|
||||
use async_trait::async_trait;
|
||||
use ansi_macro::ansi;
|
||||
|
||||
pub struct Verb;
|
||||
|
||||
static REQUIRED_AGREEMENTS: [&str;4] = [
|
||||
"I acknowledge that BlastMud is for adults only, and certify that I am over 18 years of age \
|
||||
(or any higher relevant age of majority in my country) and want to view this content.",
|
||||
"THIS GAME IS PROVIDED BY THE CREATORS, STAFF, VOLUNTEERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR \
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND \
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE CREATORS, STAFF, VOLUNTEERS OR \
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL \
|
||||
DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR \
|
||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS GAME, EVEN IF \
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. FOR THE AVOIDANCE OF DOUBT, THIS DISCLAIMER EXTENDS TO ANY \
|
||||
USER-SUPPLIED CONTENT THAT THE GAME MAY EXPOSE.",
|
||||
"I acknowledge that this game allows user-supplied content, and that while staff endeavour to \
|
||||
moderate it, I may encounter content that is distressing and/or outside community standards. \
|
||||
I agree that I will not use the game or any services provided in connection with it to transmit content \
|
||||
which is illegal (including by virtue of infringing copyright), infringes on the rights of others, \
|
||||
is personally identifying information, or is objectionable or abhorrent (including, without \
|
||||
limitation, any content related to sexual violence, real or fictional children under 18, bestiality, \
|
||||
the promotion or glorification of proscribed drug use, or fetishes that involve degrading or \
|
||||
inflicting pain in someone for the enjoyment of others). I agree to defend, indemnify, and hold \
|
||||
harmless the creators, staff, volunteers and contributors in any matter relating to content sent \
|
||||
(or re-sent) by me, in any matter arising from the game sending content to me, and in any matter \
|
||||
consequential to sharing my password, using an insecure password, or otherwise allowing or taking \
|
||||
inadequate measures to prevent another player from logging in as one or more of my characters.",
|
||||
"I certify that I am not, to my knowledge, currently banned from the game. I agree not to sustain any \
|
||||
contact with another player that is unwelcome, or to take any action for the purpose of harassment or \
|
||||
limiting the game for other players without all affected players' consent. I agree not to allow any \
|
||||
other person to play as my character, not to have more than 5 characters active (available to log \
|
||||
in as) at any one time, not to be logged in as more than one character at any instant in time, and \
|
||||
not to use any of my characters to help another character of mine in the game.",
|
||||
];
|
||||
|
||||
fn user_mut<'a>(ctx: &'a mut VerbContext) -> UResult<&'a mut User> {
|
||||
match ctx.user_dat.as_mut() {
|
||||
None => Err("Checked agreements before user logged in, which is a logic error")?,
|
||||
Some(user_dat) => Ok(user_dat)
|
||||
}
|
||||
}
|
||||
|
||||
fn terms<'a>(ctx: &'a VerbContext<'a>) -> UResult<&'a UserTermData> {
|
||||
match ctx.user_dat.as_ref() {
|
||||
None => Err("Checked agreements before user logged in, which is a logic error")?,
|
||||
Some(user_dat) => Ok(&user_dat.terms)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn first_outstanding_agreement(ctx: &VerbContext) -> UResult<Option<(String, String)>> {
|
||||
let existing_terms = &terms(ctx)?.accepted_terms;
|
||||
for agreement in REQUIRED_AGREEMENTS {
|
||||
let shortcode =
|
||||
base64::encode(ring::digest::digest(&ring::digest::SHA256,
|
||||
agreement.as_bytes()))[0..20].to_owned();
|
||||
match existing_terms.get(&shortcode) {
|
||||
None => { return Ok(Some((agreement.to_owned(), shortcode))); }
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> UResult<bool> where 'b: 'a {
|
||||
match first_outstanding_agreement(ctx)? {
|
||||
None => {
|
||||
user_mut(ctx)?.terms.terms_complete = true;
|
||||
Ok(true)
|
||||
}
|
||||
Some((text, hash)) => {
|
||||
let user = user_mut(ctx)?;
|
||||
user.terms.terms_complete = false;
|
||||
user.terms.last_presented_term = Some(hash);
|
||||
ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!(
|
||||
"Please review the following:\r\n\
|
||||
{}\r\n\
|
||||
Type <bold>agree<reset> to accept. If you can't or don't agree, you \
|
||||
unfortunately can't play, so type <bold>quit<reset> to log off.\r\n"),
|
||||
text))).await?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserVerb for Verb {
|
||||
async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
static VERB_INT: Verb = Verb;
|
||||
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
|
@ -6,7 +6,23 @@ use async_trait::async_trait;
|
||||
use ansi_macro::ansi;
|
||||
use phf::phf_map;
|
||||
|
||||
static HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
|
||||
static ALWAYS_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
|
||||
"<topicname>" =>
|
||||
ansi!("You are supposed to replace <lt>topicname> with the topic you want \
|
||||
to learn about. Example:\r\n\
|
||||
\t<bold>help register<reset> will tell you about the register command.")
|
||||
};
|
||||
|
||||
static UNREGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
|
||||
"" =>
|
||||
ansi!("Type <bold>help <lt>topicname><reset> to learn about a topic. Most \
|
||||
commands can be used as a topicname.\r\n\
|
||||
Topics of interest to unregistered users:\r\n\
|
||||
\t<bold>register<reset>\tLearn about the <bold>register<reset> command.\r\n\
|
||||
\t<bold>login<reset>\tLearn how to log in as an existing user.\r\n"),
|
||||
};
|
||||
|
||||
static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
|
||||
"" =>
|
||||
ansi!("Type <bold>help <lt>topicname><reset> to learn about a topic. Most \
|
||||
commands can be used as a topicname.\r\n\
|
||||
@ -30,10 +46,19 @@ pub struct Verb;
|
||||
impl UserVerb for Verb {
|
||||
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
||||
let mut help = None;
|
||||
if !ctx.session_dat.less_explicit_mode {
|
||||
help = help.or_else(|| EXPLICIT_HELP_PAGES.get(remaining))
|
||||
let is_unregistered = match ctx.user_dat {
|
||||
None => true,
|
||||
Some(user_dat) => !user_dat.terms.terms_complete
|
||||
};
|
||||
if is_unregistered {
|
||||
help = help.or_else(|| UNREGISTERED_HELP_PAGES.get(remaining));
|
||||
} else {
|
||||
help = help.or_else(|| REGISTERED_HELP_PAGES.get(remaining));
|
||||
if !ctx.session_dat.less_explicit_mode {
|
||||
help = help.or_else(|| EXPLICIT_HELP_PAGES.get(remaining))
|
||||
}
|
||||
}
|
||||
help = help.or_else(|| HELP_PAGES.get(remaining));
|
||||
help = help.or_else(|| ALWAYS_HELP_PAGES.get(remaining));
|
||||
let help_final = help.ok_or(
|
||||
UserError("No help available on that".to_string()))?;
|
||||
ctx.trans.queue_for_session(ctx.session,
|
||||
|
@ -1,16 +1,53 @@
|
||||
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
|
||||
use async_trait::async_trait;
|
||||
use super::{user_error, parsing::parse_username};
|
||||
use crate::models::{user::User, item::Item};
|
||||
use chrono::Utc;
|
||||
use ansi_macro::ansi;
|
||||
|
||||
pub struct Verb;
|
||||
#[async_trait]
|
||||
impl UserVerb for Verb {
|
||||
async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
||||
let (username, password) = match parse_username(remaining) {
|
||||
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
||||
let (username, mut password) = match parse_username(remaining) {
|
||||
Err(e) => user_error("Invalid username: ".to_owned() + e)?,
|
||||
Ok(r) => r
|
||||
};
|
||||
let _pwhash = bcrypt::hash(password, 10);
|
||||
password = password.trim();
|
||||
if ctx.trans.find_by_username(username).await?.is_some() {
|
||||
user_error("Username already exists".to_owned())?;
|
||||
}
|
||||
if password.contains(" ") || password.contains("\t") {
|
||||
user_error("To avoid future confusion, password can't contain spaces / tabs".to_owned())?;
|
||||
} else if password.len() < 6 {
|
||||
user_error("Password must be 6 characters long or longer".to_owned())?;
|
||||
}
|
||||
|
||||
let player_item_id = ctx.trans.create_item(&Item {
|
||||
item_type: "player".to_owned(),
|
||||
item_code: username.to_lowercase(),
|
||||
display: username.to_owned(),
|
||||
location: "room/chargen_room".to_owned(),
|
||||
..Item::default()
|
||||
}).await?;
|
||||
let password_hash = bcrypt::hash(password, 10).expect("hash not to fail");
|
||||
let user_dat = User {
|
||||
username: username.to_owned(),
|
||||
password_hash: password_hash.to_owned(),
|
||||
player_item_id,
|
||||
registered_at: Some(Utc::now()),
|
||||
..User::default()
|
||||
};
|
||||
*ctx.user_dat = Some(user_dat);
|
||||
|
||||
ctx.trans.queue_for_session(
|
||||
ctx.session,
|
||||
Some(&format!(ansi!("Welcome <bold>{}<reset>, you are now officially registered.\r\n"),
|
||||
&username))
|
||||
).await?;
|
||||
super::agree::check_and_notify_accepts(ctx).await?;
|
||||
ctx.trans.create_user(ctx.session, ctx.user_dat.as_ref().unwrap()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ pub enum LocationActionType {
|
||||
pub struct Item {
|
||||
pub item_code: String,
|
||||
pub item_type: String,
|
||||
pub display: String,
|
||||
pub display_less_explicit: Option<String>,
|
||||
pub location: String, // Item reference as item_type/item_code.
|
||||
pub action_type: LocationActionType,
|
||||
pub presence_target: Option<String>, // e.g. what are they sitting on.
|
||||
@ -54,3 +56,22 @@ pub struct Item {
|
||||
pub total_skills: BTreeMap<SkillType, u64>,
|
||||
pub temporary_buffs: Vec<Buff>,
|
||||
}
|
||||
|
||||
impl Default for Item {
|
||||
fn default() -> Self {
|
||||
Item {
|
||||
item_code: "unset".to_owned(),
|
||||
item_type: "unset".to_owned(),
|
||||
display: "Item".to_owned(),
|
||||
display_less_explicit: None,
|
||||
location: "room/storage".to_owned(),
|
||||
action_type: LocationActionType::Normal,
|
||||
presence_target: None,
|
||||
is_static: false,
|
||||
total_xp: 0,
|
||||
total_stats: BTreeMap::new(),
|
||||
total_skills: BTreeMap::new(),
|
||||
temporary_buffs: Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use std::collections::BTreeMap;
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserTermData {
|
||||
pub accepted_terms: BTreeMap<String, DateTime<Utc>>,
|
||||
pub terms_complete: bool, // Recalculated on accept and login.
|
||||
pub last_presented_term: Option<String>,
|
||||
}
|
||||
|
||||
@ -65,7 +66,7 @@ pub enum StatType {
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub password_hash: String, // bcrypted.
|
||||
pub player_item_id: u64,
|
||||
pub player_item_id: i64,
|
||||
pub registered_at: Option<DateTime<Utc>>,
|
||||
pub banned_until: Option<DateTime<Utc>>,
|
||||
pub abandoned_at: Option<DateTime<Utc>>,
|
||||
@ -83,6 +84,7 @@ impl Default for UserTermData {
|
||||
fn default() -> Self {
|
||||
UserTermData {
|
||||
accepted_terms: BTreeMap::new(),
|
||||
terms_complete: false,
|
||||
last_presented_term: None
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ CREATE INDEX item_by_loc ON items ((details->>'location'));
|
||||
CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean)));
|
||||
|
||||
CREATE TABLE users (
|
||||
-- Username here is all lower case, but details has correct case version.
|
||||
username TEXT NOT NULL PRIMARY KEY,
|
||||
current_session UUID REFERENCES sessions(session),
|
||||
current_listener UUID REFERENCES listeners(listener),
|
||||
|
Loading…
Reference in New Issue
Block a user