forked from blasthavers/blastmud
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 tokio_postgres::NoTls;
|
||||||
use crate::message_handler::ListenerSession;
|
use crate::message_handler::ListenerSession;
|
||||||
use crate::DResult;
|
use crate::DResult;
|
||||||
use crate::models::session::Session;
|
use crate::models::{session::Session, user::User, item::Item};
|
||||||
use crate::models::user::User;
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
|
||||||
@ -193,9 +193,43 @@ impl DBTrans {
|
|||||||
Ok(())
|
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<()> {
|
pub async fn commit(mut self: Self) -> DResult<()> {
|
||||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
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?;
|
trans.commit().await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -13,6 +13,7 @@ mod help;
|
|||||||
mod quit;
|
mod quit;
|
||||||
mod less_explicit_mode;
|
mod less_explicit_mode;
|
||||||
mod register;
|
mod register;
|
||||||
|
mod agree;
|
||||||
|
|
||||||
pub struct VerbContext<'l> {
|
pub struct VerbContext<'l> {
|
||||||
session: &'l ListenerSession,
|
session: &'l ListenerSession,
|
||||||
@ -34,6 +35,13 @@ pub trait UserVerb {
|
|||||||
|
|
||||||
pub type UResult<A> = Result<A, CommandHandlingError>;
|
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 {
|
impl From<Box<dyn std::error::Error + Send + Sync>> for CommandHandlingError {
|
||||||
fn from(input: Box<dyn std::error::Error + Send + Sync>) -> CommandHandlingError {
|
fn from(input: Box<dyn std::error::Error + Send + Sync>) -> CommandHandlingError {
|
||||||
SystemError(input)
|
SystemError(input)
|
||||||
@ -58,14 +66,26 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
|
|||||||
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||||
"less_explicit_mode" => less_explicit_mode::VERB,
|
"less_explicit_mode" => less_explicit_mode::VERB,
|
||||||
"register" => register::VERB,
|
"register" => register::VERB,
|
||||||
|
"agree" => agree::VERB
|
||||||
|
};
|
||||||
|
|
||||||
|
static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||||
};
|
};
|
||||||
|
|
||||||
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
|
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
|
||||||
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
|
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
|
||||||
|
|
||||||
match ctx.user_dat {
|
match &ctx.user_dat {
|
||||||
None => { result = result.or_else(|| UNREGISTERED_COMMANDS.get(cmd)); }
|
None => {
|
||||||
Some(_) => {}
|
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
|
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"
|
"That's not a command I know. Try <bold>help<reset>\r\n"
|
||||||
))
|
))
|
||||||
).await?;
|
).await?;
|
||||||
|
trans.commit().await?;
|
||||||
}
|
}
|
||||||
Some(handler) => {
|
Some(handler) => {
|
||||||
match handler.handle(&mut ctx, cmd, params).await {
|
match handler.handle(&mut ctx, cmd, params).await {
|
||||||
Ok(()) => {}
|
Ok(()) => {
|
||||||
|
trans.commit().await?;
|
||||||
|
}
|
||||||
Err(UserError(err_msg)) => {
|
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)?
|
Err(SystemError(e)) => Err(e)?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trans.commit().await?;
|
|
||||||
Ok(())
|
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 ansi_macro::ansi;
|
||||||
use phf::phf_map;
|
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 \
|
ansi!("Type <bold>help <lt>topicname><reset> to learn about a topic. Most \
|
||||||
commands can be used as a topicname.\r\n\
|
commands can be used as a topicname.\r\n\
|
||||||
@ -30,10 +46,19 @@ pub struct Verb;
|
|||||||
impl UserVerb for Verb {
|
impl UserVerb for Verb {
|
||||||
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
||||||
let mut help = None;
|
let mut help = None;
|
||||||
|
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 {
|
if !ctx.session_dat.less_explicit_mode {
|
||||||
help = help.or_else(|| EXPLICIT_HELP_PAGES.get(remaining))
|
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(
|
let help_final = help.ok_or(
|
||||||
UserError("No help available on that".to_string()))?;
|
UserError("No help available on that".to_string()))?;
|
||||||
ctx.trans.queue_for_session(ctx.session,
|
ctx.trans.queue_for_session(ctx.session,
|
||||||
|
@ -1,16 +1,53 @@
|
|||||||
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
|
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use super::{user_error, parsing::parse_username};
|
use super::{user_error, parsing::parse_username};
|
||||||
|
use crate::models::{user::User, item::Item};
|
||||||
|
use chrono::Utc;
|
||||||
|
use ansi_macro::ansi;
|
||||||
|
|
||||||
pub struct Verb;
|
pub struct Verb;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl UserVerb for Verb {
|
impl UserVerb for Verb {
|
||||||
async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
||||||
let (username, password) = match parse_username(remaining) {
|
let (username, mut password) = match parse_username(remaining) {
|
||||||
Err(e) => user_error("Invalid username: ".to_owned() + e)?,
|
Err(e) => user_error("Invalid username: ".to_owned() + e)?,
|
||||||
Ok(r) => r
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,8 @@ pub enum LocationActionType {
|
|||||||
pub struct Item {
|
pub struct Item {
|
||||||
pub item_code: String,
|
pub item_code: String,
|
||||||
pub item_type: 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 location: String, // Item reference as item_type/item_code.
|
||||||
pub action_type: LocationActionType,
|
pub action_type: LocationActionType,
|
||||||
pub presence_target: Option<String>, // e.g. what are they sitting on.
|
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 total_skills: BTreeMap<SkillType, u64>,
|
||||||
pub temporary_buffs: Vec<Buff>,
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct UserTermData {
|
pub struct UserTermData {
|
||||||
pub accepted_terms: BTreeMap<String, DateTime<Utc>>,
|
pub accepted_terms: BTreeMap<String, DateTime<Utc>>,
|
||||||
|
pub terms_complete: bool, // Recalculated on accept and login.
|
||||||
pub last_presented_term: Option<String>,
|
pub last_presented_term: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ pub enum StatType {
|
|||||||
pub struct User {
|
pub struct User {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password_hash: String, // bcrypted.
|
pub password_hash: String, // bcrypted.
|
||||||
pub player_item_id: u64,
|
pub player_item_id: i64,
|
||||||
pub registered_at: Option<DateTime<Utc>>,
|
pub registered_at: Option<DateTime<Utc>>,
|
||||||
pub banned_until: Option<DateTime<Utc>>,
|
pub banned_until: Option<DateTime<Utc>>,
|
||||||
pub abandoned_at: Option<DateTime<Utc>>,
|
pub abandoned_at: Option<DateTime<Utc>>,
|
||||||
@ -83,6 +84,7 @@ impl Default for UserTermData {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
UserTermData {
|
UserTermData {
|
||||||
accepted_terms: BTreeMap::new(),
|
accepted_terms: BTreeMap::new(),
|
||||||
|
terms_complete: false,
|
||||||
last_presented_term: None
|
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 INDEX item_by_static ON items ((cast(details->>'is_static' as boolean)));
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
|
-- Username here is all lower case, but details has correct case version.
|
||||||
username TEXT NOT NULL PRIMARY KEY,
|
username TEXT NOT NULL PRIMARY KEY,
|
||||||
current_session UUID REFERENCES sessions(session),
|
current_session UUID REFERENCES sessions(session),
|
||||||
current_listener UUID REFERENCES listeners(listener),
|
current_listener UUID REFERENCES listeners(listener),
|
||||||
|
Loading…
Reference in New Issue
Block a user