Allow creation of new corps.
This commit is contained in:
parent
158b590c35
commit
929a64f93e
@ -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;
|
||||
@ -788,6 +789,54 @@ impl DBTrans {
|
||||
&serde_json::to_value(details)?]).await?;
|
||||
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));
|
||||
|
@ -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> {
|
||||
|
@ -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) {
|
||||
|
37
blastmud_game/src/message_handler/user_commands/sign.rs
Normal file
37
blastmud_game/src/message_handler/user_commands/sign.rs
Normal 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;
|
37
blastmud_game/src/message_handler/user_commands/write.rs
Normal file
37
blastmud_game/src/message_handler/user_commands/write.rs
Normal 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;
|
@ -3,3 +3,4 @@ pub mod user;
|
||||
pub mod item;
|
||||
pub mod task;
|
||||
pub mod consent;
|
||||
pub mod corp;
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user