Allow people to join, leave, and fire from corps.

This commit is contained in:
Condorra 2023-03-25 00:58:19 +11:00
parent cb05843ee9
commit 3bd0412e4a
14 changed files with 258 additions and 45 deletions

View File

@ -18,7 +18,7 @@ use crate::models::{
}, },
task::{Task, TaskParse}, task::{Task, TaskParse},
consent::{Consent, ConsentType}, consent::{Consent, ConsentType},
corp::{Corp, CorpId, CorpMembership}, corp::{Corp, CorpId, CorpMembership, CorpCommType},
}; };
use tokio_postgres::types::ToSql; use tokio_postgres::types::ToSql;
use std::collections::BTreeSet; use std::collections::BTreeSet;
@ -886,6 +886,70 @@ impl DBTrans {
.collect()) .collect())
} }
pub async fn broadcast_to_corp<'a>(self: &'a Self,
corp_id: &'a CorpId,
comm_type: &'a CorpCommType,
except_user: Option<&'a str>,
message: &'a str) -> DResult<()> {
let comm_type_s = serde_json::to_value(comm_type)?.as_str()
.ok_or("comm type doesn't serialise to JSON string")?
.to_owned();
let mut params : Vec<&(dyn ToSql + Sync)> =
vec!(&message, &corp_id.0, &comm_type_s);
let mut query = "INSERT INTO sendqueue (session, listener, message) \
SELECT s.session, s.listener, $1 FROM \
sessions s \
JOIN users u ON u.current_session = s.session \
JOIN corp_membership m ON m.member_username = u.username \
WHERE m.corp_id = $2 AND \
(m.details->'comms_on') ? $3 AND \
m.details->>'joined_at' IS NOT NULL".to_owned();
match except_user.as_ref() {
None => {},
Some(u) => {
query.push_str(" AND u.username <> $4");
params.push(u);
}
}
self.pg_trans()?
.execute(&query, &params).await?;
Ok(())
}
pub async fn list_corp_members<'a>(self: &'a Self,
corp_id: &'a CorpId) ->
DResult<Vec<(String, CorpMembership)>> {
Ok(self.pg_trans()?
.query("SELECT member_username, details \
FROM corp_membership WHERE \
corp_id = $1", &[&corp_id.0]).await?
.iter()
.filter_map(|i| Some(
(i.get("member_username"),
serde_json::from_value(i.get("details")).ok()?)))
.collect())
}
pub async fn delete_corp<'a>(self: &'a Self,
corp_id: &'a CorpId) -> DResult<()> {
self.pg_trans()?
.execute("DELETE FROM corps WHERE \
corp_id = $1", &[&corp_id.0]).await?;
Ok(())
}
pub async fn delete_corp_membership<'a>(
self: &'a Self,
corp_id: &'a CorpId,
username: &'a str
) -> DResult<()> {
self.pg_trans()?
.execute("DELETE FROM corp_membership WHERE \
corp_id = $1 AND member_username = $2",
&[&corp_id.0, &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));
if let Some(trans) = trans_opt { if let Some(trans) = trans_opt {

View File

@ -10,8 +10,9 @@ use super::{
parsing::parse_command_name, parsing::parse_command_name,
}; };
use crate::{ use crate::{
models::corp::{CorpMembership, CorpPermission}, models::corp::{CorpMembership, CorpPermission, CorpCommType},
db::ItemSearchParams, db::ItemSearchParams,
language::caps_first,
}; };
use chrono::Utc; use chrono::Utc;
use async_trait::async_trait; use async_trait::async_trait;
@ -36,7 +37,7 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()>
Some(c) => c Some(c) => c
}; };
if !check_corp_perm(&CorpPermission::Hire, &mem) || mem.joined_at.is_none() { if !check_corp_perm(&CorpPermission::Hire, &mem) || mem.joined_at.is_none() {
user_error("You don't have hire permissions for that corp".to_owned())?; user_error("You don't have hiring permissions for that corp".to_owned())?;
} }
let target_user = search_item_for_user(ctx, &ItemSearchParams { let target_user = search_item_for_user(ctx, &ItemSearchParams {
@ -58,9 +59,15 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()>
match ctx.trans.match_user_corp_by_name(into_raw.trim(), &target_user.item_code).await? { match ctx.trans.match_user_corp_by_name(into_raw.trim(), &target_user.item_code).await? {
None => (), None => (),
Some((_, _, CorpMembership { invited_at: Some(_), .. })) => Some((_, _, CorpMembership { invited_at: Some(_), .. })) =>
user_error("They've already been invited.".to_owned())?, user_error(
format!("{}'s already been invited.",
&caps_first(&target_user.pronouns.subject)
))?,
Some((_, _, _)) => Some((_, _, _)) =>
user_error("They're already hired.".to_owned())?, user_error(
format!("{}'s already hired.",
&caps_first(&target_user.pronouns.subject)
))?,
}; };
let new_mem = CorpMembership { let new_mem = CorpMembership {
@ -81,18 +88,36 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()>
"" ""
} }
))).await?; ))).await?;
ctx.trans.queue_for_session( ctx.trans.broadcast_to_corp(
&ctx.session, &corp_id,
Some(&format!( &CorpCommType::Notice, None,
"You offer to hire {} into {}.\n", &format!("{} has invited {} to join {}!\n",
&target_user.display_for_sentence(!their_sess_dat.less_explicit_mode, 1, false), user.username,
&corp.name &target_user.display_for_sentence(false, 1, false),
))).await?; corp.name)).await?;
Ok(()) Ok(())
} }
async fn corp_join(_ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> { async fn corp_join(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
user_error("Join not implemented yet".to_owned())? let user = get_user_or_fail(ctx)?;
let (corpid, corp, mut mem) = match ctx.trans.match_user_corp_by_name(remaining, &user.username).await? {
None => user_error("Corp not found".to_owned())?,
Some(v) => v
};
if mem.joined_at.is_some() {
user_error("You have already joined that corp!".to_owned())?;
}
mem.joined_at = Some(Utc::now());
mem.invited_at = None;
ctx.trans.upsert_corp_membership(&corpid, &user.username, &mem).await?;
ctx.trans.broadcast_to_corp(
&corpid,
&CorpCommType::Notice, None,
&format!(ansi!("There is a loud <red>cheer<reset> as {} accepts an offer to join {}!\n"),
&user.username, &corp.name)).await?;
Ok(())
} }
async fn corp_list(ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> { async fn corp_list(ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> {
@ -124,6 +149,105 @@ async fn corp_list(ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> {
Ok(()) Ok(())
} }
async fn corp_leave(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let user = get_user_or_fail(ctx)?;
let (corpid, corp, mem) = match ctx.trans.match_user_corp_by_name(remaining, &user.username).await? {
None => user_error("Corp not found".to_owned())?,
Some(v) => v
};
if mem.joined_at.is_some() {
ctx.trans.broadcast_to_corp(
&corpid,
&CorpCommType::Notice, None,
&format!(ansi!("There is a loud <red>boo<reset> as {} resigns from {}!\n"), user.username, corp.name)).await?;
} else {
ctx.trans.queue_for_session(ctx.session, Some(
&format!("You decline your invitation to join {}\n",
corp.name))).await?;
}
let mut delete_corp = false;
if mem.permissions.contains(&CorpPermission::Holder) {
let username_l = user.username.to_lowercase();
let members = ctx.trans.list_corp_members(&corpid).await?;
if members.len() == 1 {
delete_corp = true;
} else if !members.iter().any(
|(name, mem)| *name != username_l &&
mem.permissions.contains(&CorpPermission::Holder)) {
user_error("The last holder cannot resign from a non-empty \
corp - fire everyone else first, or promote a \
successor to holder".to_owned())?;
}
}
ctx.trans.delete_corp_membership(&corpid, &user.username).await?;
if delete_corp {
ctx.trans.delete_corp(&corpid).await?;
}
Ok(())
}
async fn corp_fire(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (target_raw, into_raw) = match remaining.rsplit_once(" from ") {
None => user_error(
ansi!("Usage: <bold>corp fire<reset> username <bold>from<reset> corpname").to_owned()
)?,
Some(c) => c
};
let user = get_user_or_fail(ctx)?;
let player = get_player_item_or_fail(ctx).await?;
let (corp_id, corp, mem) =
match ctx.trans.match_user_corp_by_name(into_raw.trim(), &user.username).await? {
None => user_error("No such corp!".to_owned())?,
Some(c) => c
};
if !check_corp_perm(&CorpPermission::Fire, &mem) || mem.joined_at.is_none() {
user_error("You don't have firing permissions for that corp".to_owned())?;
}
let target_user = search_item_for_user(ctx, &ItemSearchParams {
include_all_players: true,
..ItemSearchParams::base(&player, target_raw.trim())
}).await?;
if target_user.item_type != "player" {
user_error("Only players can be fired.".to_owned())?;
}
match ctx.trans.match_user_corp_by_name(into_raw.trim(), &target_user.item_code).await? {
None => user_error(format!(
"{} isn't currently hired.",
&caps_first(&target_user.pronouns.subject)
))?,
Some((_, _, CorpMembership { permissions: their_perm,
joined_at: Some(their_join), ..})) => {
if their_perm.contains(&CorpPermission::Holder) {
if !mem.permissions.contains(&CorpPermission::Holder) {
user_error("I love the ambition, but only holders can fire holders!".to_owned())?;
}
if their_join < mem.joined_at.unwrap_or(Utc::now()) {
user_error("Whoah there young whippersnapper, holders can't fire more senior holders!".to_owned())?;
}
}
},
_ => {}
}
ctx.trans.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice, None,
&format!(ansi!("A <blue>nervous silence<reset> falls across \
the workers as {} fires {} from {}.\n"),
user.username,
target_user.display_for_sentence(false, 1, false),
corp.name)).await?;
ctx.trans.delete_corp_membership(&corp_id, &target_user.item_code).await?;
Ok(())
}
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
@ -133,6 +257,8 @@ impl UserVerb for Verb {
"hire" | "invite" => corp_invite(ctx, remaining).await?, "hire" | "invite" => corp_invite(ctx, remaining).await?,
"join" => corp_join(ctx, remaining).await?, "join" => corp_join(ctx, remaining).await?,
"" | "list" => corp_list(ctx, remaining).await?, "" | "list" => corp_list(ctx, remaining).await?,
"leave" | "resign" => corp_leave(ctx, remaining).await?,
"fire" | "dismiss" => corp_fire(ctx, remaining).await?,
_ => user_error("Unknown command".to_owned())? _ => user_error("Unknown command".to_owned())?
} }
Ok(()) Ok(())

View File

@ -21,7 +21,7 @@ use crate::{
TaskRunContext, TaskRunContext,
}, },
services::{ services::{
broadcast_to_room, comms::broadcast_to_room,
capacity::{ capacity::{
check_item_capacity, check_item_capacity,
CapacityLevel, CapacityLevel,

View File

@ -17,7 +17,7 @@ use crate::{
queue_command queue_command
}, },
services::{ services::{
broadcast_to_room, comms::broadcast_to_room,
capacity::{ capacity::{
check_item_capacity, check_item_capacity,
CapacityLevel, CapacityLevel,

View File

@ -19,7 +19,7 @@ use crate::{
LocationActionType LocationActionType
}, },
services::{ services::{
broadcast_to_room, comms::broadcast_to_room,
skills::skill_check_and_grind, skills::skill_check_and_grind,
combat::stop_attacking_mut, combat::stop_attacking_mut,
combat::handle_resurrect, combat::handle_resurrect,

View File

@ -3,7 +3,7 @@ use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError,
get_player_item_or_fail, is_likely_explicit}; get_player_item_or_fail, is_likely_explicit};
use crate::{ use crate::{
models::item::{Item, ItemFlag}, models::item::{Item, ItemFlag},
services::broadcast_to_room, services::comms::broadcast_to_room,
}; };
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; #[double] use crate::db::DBTrans;

View File

@ -22,7 +22,7 @@ use crate::{
SkillType, SkillType,
}, },
services::{ services::{
broadcast_to_room, comms::broadcast_to_room,
skills::skill_check_and_grind, skills::skill_check_and_grind,
effect::run_effects, effect::run_effects,
check_consent, check_consent,

View File

@ -20,7 +20,7 @@ use crate::{
SkillType, SkillType,
}, },
services::{ services::{
broadcast_to_room, comms::broadcast_to_room,
skills::skill_check_and_grind, skills::skill_check_and_grind,
}, },
}; };

View File

@ -9,6 +9,15 @@ pub enum CorpPermission {
ChangeJobTitle, ChangeJobTitle,
} }
#[derive(Serialize, Deserialize, PartialEq)]
pub enum CorpCommType {
Chat,
Notice,
Connect,
Reward,
Death,
}
pub struct CorpId(pub i64); pub struct CorpId(pub i64);
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -41,11 +50,12 @@ pub struct CorpMembership {
pub allow_combat: bool, pub allow_combat: bool,
pub job_title: String, pub job_title: String,
pub priority: i64, pub priority: i64,
pub chat_on: bool pub comms_on: Vec<CorpCommType>,
} }
impl Default for CorpMembership { impl Default for CorpMembership {
fn default() -> CorpMembership { fn default() -> CorpMembership {
use CorpCommType::*;
CorpMembership { CorpMembership {
invited_at: None, invited_at: None,
joined_at: None, joined_at: None,
@ -53,7 +63,13 @@ impl Default for CorpMembership {
allow_combat: false, allow_combat: false,
job_title: "Employee".to_owned(), job_title: "Employee".to_owned(),
priority: 100, priority: 100,
chat_on: true, comms_on: vec!(
Chat,
Notice,
Connect,
Reward,
Death,
),
} }
} }
} }

View File

@ -7,30 +7,12 @@ use crate::{
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; #[double] use crate::db::DBTrans;
pub mod skills; pub mod comms;
pub mod combat; pub mod combat;
pub mod skills;
pub mod capacity; pub mod capacity;
pub mod effect; pub mod effect;
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
for item in trans.find_items_by_location(location).await? {
if item.item_type != "player" || item.is_dead {
continue;
}
if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await? {
if session_dat.less_explicit_mode && Some(&item.item_code) != from_item.map(|i| &i.item_code) {
if let Some(msg) = message_nonexplicit {
trans.queue_for_session(&session, Some(msg)).await?;
}
return Ok(());
}
trans.queue_for_session(&session, Some(message_explicit_ok)).await?;
}
}
Ok(())
}
fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool { fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool {
if let Some((loctype, loccode)) = target.location.split_once("/") { if let Some((loctype, loccode)) = target.location.split_once("/") {
if !consent.only_in.is_empty() { if !consent.only_in.is_empty() {

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
services::{ services::{
broadcast_to_room, comms::broadcast_to_room,
skills::skill_check_and_grind, skills::skill_check_and_grind,
skills::skill_check_only, skills::skill_check_only,
}, },

View File

@ -0,0 +1,25 @@
use crate::{
DResult,
models::item::Item,
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
for item in trans.find_items_by_location(location).await? {
if item.item_type != "player" || item.is_dead {
continue;
}
if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await? {
if session_dat.less_explicit_mode && Some(&item.item_code) != from_item.map(|i| &i.item_code) {
if let Some(msg) = message_nonexplicit {
trans.queue_for_session(&session, Some(msg)).await?;
}
return Ok(());
}
trans.queue_for_session(&session, Some(message_explicit_ok)).await?;
}
}
Ok(())
}

View File

@ -18,7 +18,7 @@ use crate::{
} }
}; };
use super::{ use super::{
broadcast_to_room, comms::broadcast_to_room,
combat::change_health, combat::change_health,
}; };
use async_trait::async_trait; use async_trait::async_trait;

View File

@ -11,7 +11,7 @@ use crate::{
UResult, UResult,
VerbContext, VerbContext,
}, },
services::broadcast_to_room, services::comms::broadcast_to_room,
}; };
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait; use async_trait::async_trait;