Allow creation of new corps.

This commit is contained in:
Condorra 2023-03-19 00:04:59 +11:00
parent 158b590c35
commit 929a64f93e
10 changed files with 309 additions and 31 deletions

View File

@ -18,6 +18,7 @@ use crate::models::{
},
task::{Task, TaskParse},
consent::{Consent, ConsentType},
corp::{Corp, CorpId, CorpMembership},
};
use tokio_postgres::types::ToSql;
use std::collections::BTreeSet;
@ -789,6 +790,54 @@ impl DBTrans {
Ok(())
}
pub async fn find_corp_by_name(&self, name: &str) -> DResult<Option<(CorpId, Corp)>> {
Ok(match self.pg_trans()?
.query_opt("SELECT corp_id, details FROM corps WHERE LOWER(details->>'name') = $1",
&[&name.to_lowercase()])
.await? {
None => None,
Some(row) =>
Some(
(CorpId(row.get("corp_id")), serde_json::from_value(row.get("details"))?)
)
})
}
pub async fn create_corp(&self, details: &Corp) -> DResult<CorpId> {
let id = self.pg_trans()?
.query_one("INSERT INTO corps (details) VALUES ($1) RETURNING corp_id", &[&serde_json::to_value(details)?]).await?
.get("corp_id");
Ok(CorpId(id))
}
pub async fn upsert_corp_membership(&self, corp: &CorpId, username: &str, details: &CorpMembership) -> DResult<()> {
self.pg_trans()?
.execute("INSERT INTO corp_membership (corp_id, member_username, details) \
VALUES ($1, $2, $3) \
ON CONFLICT (corp_id, member_username) DO UPDATE SET \
details = excluded.details",
&[&corp.0, &username.to_lowercase(),
&serde_json::to_value(details)?]
).await?;
Ok(())
}
pub async fn get_corp_memberships_for_user(&self, username: &str) -> DResult<Vec<(CorpId, String, CorpMembership)>> {
Ok(self.pg_trans()?
.query("SELECT m.corp_id, c.details->>'name', m.details FROM corp_membership m \
JOIN corps c ON c.corp_id = m.corp_id WHERE m.member_username = $1 \
AND m.details->>'joined_at' IS NOT NULL", &[&username.to_lowercase()])
.await?
.into_iter()
.filter_map(|row|
match serde_json::from_value(row.get(2)) {
Err(_) => None,
Ok(j) =>
Some((CorpId(row.get(0)), row.get(1), j))
})
.collect())
}
pub async fn commit(mut self: Self) -> DResult<()> {
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
if let Some(trans) = trans_opt {

View File

@ -30,14 +30,16 @@ pub mod movement;
mod page;
pub mod parsing;
mod quit;
mod register;
pub mod register;
pub mod say;
mod score;
mod sign;
mod status;
pub mod use_cmd;
mod whisper;
mod who;
pub mod wield;
mod write;
pub struct VerbContext<'l> {
pub session: &'l ListenerSession,
@ -145,6 +147,8 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"sc" => score::VERB,
"score" => score::VERB,
"sign" => sign::VERB,
"st" => status::VERB,
"stat" => status::VERB,
"status" => status::VERB,
@ -157,6 +161,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"wield" => wield::VERB,
"who" => who::VERB,
"write" => write::VERB,
};
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {

View File

@ -60,6 +60,9 @@ impl UserVerb for Verb {
if ctx.trans.find_by_username(username).await?.is_some() {
user_error("Username already exists".to_owned())?;
}
if ctx.trans.find_corp_by_name(username).await?.is_some() {
user_error("Username clashes with existing corp name".to_owned())?;
}
if password.len() < 6 {
user_error("Password must be 6 characters long or longer".to_owned())?;
} else if !validator::validate_email(email) {

View File

@ -0,0 +1,37 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error, get_player_item_or_fail, search_item_for_user};
use crate::{
db::ItemSearchParams,
static_content::possession_type::possession_data,
};
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let item_name = remaining.trim();
if item_name == "" {
user_error("Sign what? Try: sign something".to_owned())?;
}
let player_item = get_player_item_or_fail(ctx).await?;
let item = search_item_for_user(ctx, &ItemSearchParams {
include_contents: true,
..ItemSearchParams::base(&player_item, item_name)
}).await?;
if item.item_type != "possession" {
user_error("You can't sign that!".to_owned())?;
}
let handler = match item.possession_type.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.sign_handler) {
None => user_error("You can't sign that!".to_owned())?,
Some(h) => h
};
handler.cmd(ctx, &player_item, &item).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -0,0 +1,37 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error, get_player_item_or_fail, search_item_for_user};
use crate::{
db::ItemSearchParams,
static_content::possession_type::possession_data,
};
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (write_what_raw, on_what_raw) = match remaining.rsplit_once(" on ") {
None => user_error("Write on what? Try write something on something".to_owned())?,
Some(v) => v
};
let player_item = get_player_item_or_fail(ctx).await?;
let item = search_item_for_user(ctx, &ItemSearchParams {
include_contents: true,
..ItemSearchParams::base(&player_item, on_what_raw.trim())
}).await?;
if item.item_type != "possession" {
user_error("You can't write on that!".to_owned())?;
}
let handler = match item.possession_type.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.write_handler) {
None => user_error("You can't write on that!".to_owned())?,
Some(h) => h
};
handler.write_cmd(ctx, &player_item, &item, write_what_raw.trim()).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -3,3 +3,4 @@ pub mod user;
pub mod item;
pub mod task;
pub mod consent;
pub mod corp;

View File

@ -1,4 +1,15 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
#[derive(Serialize, Deserialize)]
pub enum CorpPermission {
Holder, // Implies all permissions.
Hire,
Fire,
ChangeJobTitle,
}
pub struct CorpId(pub i64);
#[derive(Serialize, Deserialize)]
pub struct Corp {
@ -7,4 +18,36 @@ pub struct Corp {
// allow_combat off. This will allow duly authorised corp members to
// consent to combat with other corps, and have it apply to members.
pub allow_combat_required: bool,
pub member_permissions: Vec<CorpPermission>,
}
impl Default for Corp {
fn default() -> Self {
Self {
name: "Unset".to_owned(),
allow_combat_required: false,
member_permissions: vec!(),
}
}
}
#[derive(Serialize, Deserialize)]
pub struct CorpMembership {
pub invited_at: Option<DateTime<Utc>>,
pub joined_at: Option<DateTime<Utc>>,
pub permissions: Vec<CorpPermission>,
pub allow_combat: bool,
pub job_title: String,
}
impl Default for CorpMembership {
fn default() -> CorpMembership {
CorpMembership {
invited_at: None,
joined_at: None,
permissions: vec!(),
allow_combat: false,
job_title: "Employee".to_owned(),
}
}
}

View File

@ -2,11 +2,13 @@ use serde::{Serialize, Deserialize};
use crate::{
models::item::{SkillType, Item, Pronouns},
models::consent::ConsentType,
message_handler::user_commands::{UResult, VerbContext},
};
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
use rand::seq::SliceRandom;
use super::species::BodyPart;
use async_trait::async_trait;
mod fangs;
mod antenna_whip;
@ -80,7 +82,7 @@ pub enum UseEffect {
BroadcastMessage { messagef: Box<dyn Fn(&Item, &Item, &Item) -> (String, String) + Sync + Send>},
// skill_multiplier is always positive - sign flipped for crit fails.
ChangeTargetHealth { delay_secs: u64, base_effect: i64, skill_multiplier: f64,
max_effect: i64, message: Box<dyn Fn(&Item) -> (String, String) + Sync + Send> }
max_effect: i64, message: Box<dyn Fn(&Item) -> (String, String) + Sync + Send> },
}
pub struct UseData {
@ -109,6 +111,16 @@ impl Default for UseData {
}
}
#[async_trait]
pub trait WriteHandler {
async fn write_cmd(&self, ctx: &mut VerbContext, player: &Item, on_what: &Item, write_what: &str) -> UResult<()>;
}
#[async_trait]
pub trait ArglessHandler {
async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()>;
}
pub struct PossessionData {
pub weapon_data: Option<WeaponData>,
pub display: &'static str,
@ -121,6 +133,8 @@ pub struct PossessionData {
pub use_data: Option<UseData>,
pub becomes_on_spent: Option<PossessionType>,
pub weight: u64,
pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>,
pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
}
impl Default for PossessionData {
@ -137,6 +151,8 @@ impl Default for PossessionData {
charge_data: None,
becomes_on_spent: None,
use_data: None,
write_handler: None,
sign_handler: None,
}
}
}

View File

@ -1,41 +1,127 @@
use super::{PossessionData, UseData, UseEffect};
use crate::models::item::{SkillType, ItemSpecialData};
use super::{PossessionData, WriteHandler, ArglessHandler};
use crate::{
models::{
item::{Item, ItemSpecialData},
corp::{Corp, CorpMembership, CorpPermission},
},
message_handler::user_commands::{
register::is_invalid_username,
parsing::parse_username,
user_error,
UResult,
VerbContext,
},
services::broadcast_to_room,
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc;
use super::PossessionType::*;
pub struct CorpLicenceHandler {
}
#[async_trait]
impl WriteHandler for CorpLicenceHandler {
async fn write_cmd(&self, ctx: &mut VerbContext, _player: &Item, on_what: &Item, write_what: &str) -> UResult<()> {
let name = match parse_username(write_what) {
Err(e) => user_error("Invalid corp name: ".to_owned() + e)?,
Ok((_, rest)) if rest != "" =>
user_error("No spaces allowed in corp names!".to_owned())?,
Ok((name, _)) => name
};
if is_invalid_username(name) {
user_error("Sorry, that corp name isn't allowed. Try another".to_owned())?;
}
if ctx.trans.find_by_username(name).await?.is_some() {
user_error("Corp name clashes with existing user name".to_owned())?;
}
if ctx.trans.find_corp_by_name(&name).await?.is_some() {
user_error("Corp name already taken!".to_owned())?;
}
let mut item_clone = on_what.clone();
item_clone.special_data = Some(ItemSpecialData::ItemWriting {
text: name.to_owned()
});
ctx.trans.save_item_model(&item_clone).await?;
ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!(
"The pencil makes a scratching sound as you mark the paper with the attached \
pencil and write \"{}\" on it. [Hint: Try the <bold>use<reset> command to submit \
your signed paperwork and register the corporation, or <bold>write<reset> again \
to erase and change the name].\n"), name))).await?;
Ok(())
}
}
#[async_trait]
impl ArglessHandler for CorpLicenceHandler {
async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()> {
let name = match what.special_data.as_ref() {
Some(ItemSpecialData::ItemWriting { text }) => text,
_ => user_error("You have to write your corp's name on it first!".to_owned())?
};
if ctx.trans.find_by_username(&name).await?.is_some() {
user_error("Corp name clashes with existing user name".to_owned())?;
}
if ctx.trans.find_corp_by_name(&name).await?.is_some() {
user_error("Corp name already taken!".to_owned())?;
}
if ctx.trans.get_corp_memberships_for_user(&player.item_code).await?.len() >= 5 {
user_error("You can't be in more than 5 corps".to_owned())?;
}
broadcast_to_room(ctx.trans, &player.location, None,
&format!(
"{} signs a contract establishing {} as a corp\n",
&player.display_for_sentence(true, 1, true),
name
),
Some(
&format!("{} signs a contract establishing {} as a corp\n",
&player.display_for_sentence(false, 1, true),
name
)
)).await?;
let corp_id = ctx.trans.create_corp(&Corp {
name: name.to_owned(),
..Default::default()
}).await?;
ctx.trans.upsert_corp_membership(
&corp_id, &player.item_code,
&CorpMembership {
joined_at: Some(Utc::now()),
permissions: vec!(CorpPermission::Holder),
allow_combat: true,
job_title: "Founder".to_owned(),
..Default::default()
}).await?;
let mut what_mut = what.clone();
what_mut.possession_type = Some(CertificateOfIncorporation);
let cp_data = cert_data();
what_mut.display = cp_data.display.to_owned();
what_mut.details = Some(cp_data.details.to_owned());
ctx.trans.save_item_model(&what_mut).await?;
Ok(())
}
}
static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {};
pub fn data() -> PossessionData {
PossessionData {
display: "new corp licence",
details: ansi!("A blank form that you can <bold>use<reset> to establish a new corp. It rests on a clipboard with a pencil attached by a chain. There is a space to <bold>write<reset> on it [try <bold>write Blah on licence<reset> followed by <bold>use licence<reset> to create a corp named Blah]"),
details: ansi!("A blank form that you can <bold>use<reset> to establish a new corp. It rests on a clipboard with a pencil attached by a chain. There is a space to <bold>write<reset> on it [try <bold>write Blah on licence<reset> followed by <bold>sign licence<reset> to create a corp named Blah]"),
aliases: vec!("form", "license", "licence", "new"),
use_data: Some(UseData {
uses_skill: SkillType::Persuade,
diff_level: 4.0,
crit_fail_effects: vec!(),
fail_effects: vec!(),
success_effects: vec!(
UseEffect::BroadcastMessage {
messagef: Box::new(|player, _item, _target| (
format!(
"{} signs a contract establishing Blah as a corp\n",
&player.display_for_sentence(true, 1, true),
),
format!("{} signs a contract establishing Blah as a corp\n",
&player.display_for_sentence(false, 1, true),
)))
},
),
errorf: Box::new(
|item, _target|
match item.special_data {
Some(ItemSpecialData::ItemWriting { .. }) => None,
_ => Some("You have to your corp's name on it first!".to_owned())
}),
..Default::default()
}),
weight: 10,
becomes_on_spent: Some(CertificateOfIncorporation),
write_handler: Some(&CORP_LICENCE_HANDLER),
sign_handler: Some(&CORP_LICENCE_HANDLER),
..Default::default()
}
}

View File

@ -58,13 +58,14 @@ CREATE TABLE corps (
corp_id BIGSERIAL NOT NULL PRIMARY KEY,
details JSONB NOT NULL
);
CREATE INDEX corp_by_name ON corps((details->>'name'));
CREATE INDEX corp_by_name ON corps((LOWER(details->>'name')));
CREATE TABLE corp_membership (
corp_id BIGSERIAL NOT NULL REFERENCES corps(corp_id),
member_username TEXT NOT NULL REFERENCES users(username),
details JSONB NOT NULL,
PRIMARY KEY (corp_id, member_username)
);
CREATE INDEX corp_membership_by_username ON corp_membership(member_username);
CREATE TABLE user_consent (
consenting_user TEXT NOT NULL REFERENCES users(username),