Allow people to join, leave, and fire from corps.
This commit is contained in:
parent
cb05843ee9
commit
3bd0412e4a
@ -18,7 +18,7 @@ use crate::models::{
|
||||
},
|
||||
task::{Task, TaskParse},
|
||||
consent::{Consent, ConsentType},
|
||||
corp::{Corp, CorpId, CorpMembership},
|
||||
corp::{Corp, CorpId, CorpMembership, CorpCommType},
|
||||
};
|
||||
use tokio_postgres::types::ToSql;
|
||||
use std::collections::BTreeSet;
|
||||
@ -886,6 +886,70 @@ impl DBTrans {
|
||||
.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, ¶ms).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<()> {
|
||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
||||
if let Some(trans) = trans_opt {
|
||||
|
@ -10,8 +10,9 @@ use super::{
|
||||
parsing::parse_command_name,
|
||||
};
|
||||
use crate::{
|
||||
models::corp::{CorpMembership, CorpPermission},
|
||||
models::corp::{CorpMembership, CorpPermission, CorpCommType},
|
||||
db::ItemSearchParams,
|
||||
language::caps_first,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use async_trait::async_trait;
|
||||
@ -36,7 +37,7 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()>
|
||||
Some(c) => c
|
||||
};
|
||||
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 {
|
||||
@ -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? {
|
||||
None => (),
|
||||
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((_, _, _)) =>
|
||||
user_error("They're already hired.".to_owned())?,
|
||||
user_error(
|
||||
format!("{}'s already hired.",
|
||||
&caps_first(&target_user.pronouns.subject)
|
||||
))?,
|
||||
};
|
||||
|
||||
let new_mem = CorpMembership {
|
||||
@ -81,18 +88,36 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()>
|
||||
""
|
||||
}
|
||||
))).await?;
|
||||
ctx.trans.queue_for_session(
|
||||
&ctx.session,
|
||||
Some(&format!(
|
||||
"You offer to hire {} into {}.\n",
|
||||
&target_user.display_for_sentence(!their_sess_dat.less_explicit_mode, 1, false),
|
||||
&corp.name
|
||||
))).await?;
|
||||
ctx.trans.broadcast_to_corp(
|
||||
&corp_id,
|
||||
&CorpCommType::Notice, None,
|
||||
&format!("{} has invited {} to join {}!\n",
|
||||
user.username,
|
||||
&target_user.display_for_sentence(false, 1, false),
|
||||
corp.name)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn corp_join(_ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> {
|
||||
user_error("Join not implemented yet".to_owned())?
|
||||
async fn corp_join(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
|
||||
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<()> {
|
||||
@ -124,6 +149,105 @@ async fn corp_list(ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> {
|
||||
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;
|
||||
#[async_trait]
|
||||
impl UserVerb for Verb {
|
||||
@ -133,6 +257,8 @@ impl UserVerb for Verb {
|
||||
"hire" | "invite" => corp_invite(ctx, remaining).await?,
|
||||
"join" => corp_join(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())?
|
||||
}
|
||||
Ok(())
|
||||
|
@ -21,7 +21,7 @@ use crate::{
|
||||
TaskRunContext,
|
||||
},
|
||||
services::{
|
||||
broadcast_to_room,
|
||||
comms::broadcast_to_room,
|
||||
capacity::{
|
||||
check_item_capacity,
|
||||
CapacityLevel,
|
||||
|
@ -17,7 +17,7 @@ use crate::{
|
||||
queue_command
|
||||
},
|
||||
services::{
|
||||
broadcast_to_room,
|
||||
comms::broadcast_to_room,
|
||||
capacity::{
|
||||
check_item_capacity,
|
||||
CapacityLevel,
|
||||
|
@ -19,7 +19,7 @@ use crate::{
|
||||
LocationActionType
|
||||
},
|
||||
services::{
|
||||
broadcast_to_room,
|
||||
comms::broadcast_to_room,
|
||||
skills::skill_check_and_grind,
|
||||
combat::stop_attacking_mut,
|
||||
combat::handle_resurrect,
|
||||
|
@ -3,7 +3,7 @@ use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError,
|
||||
get_player_item_or_fail, is_likely_explicit};
|
||||
use crate::{
|
||||
models::item::{Item, ItemFlag},
|
||||
services::broadcast_to_room,
|
||||
services::comms::broadcast_to_room,
|
||||
};
|
||||
use mockall_double::double;
|
||||
#[double] use crate::db::DBTrans;
|
||||
|
@ -22,7 +22,7 @@ use crate::{
|
||||
SkillType,
|
||||
},
|
||||
services::{
|
||||
broadcast_to_room,
|
||||
comms::broadcast_to_room,
|
||||
skills::skill_check_and_grind,
|
||||
effect::run_effects,
|
||||
check_consent,
|
||||
|
@ -20,7 +20,7 @@ use crate::{
|
||||
SkillType,
|
||||
},
|
||||
services::{
|
||||
broadcast_to_room,
|
||||
comms::broadcast_to_room,
|
||||
skills::skill_check_and_grind,
|
||||
},
|
||||
};
|
||||
|
@ -9,6 +9,15 @@ pub enum CorpPermission {
|
||||
ChangeJobTitle,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq)]
|
||||
pub enum CorpCommType {
|
||||
Chat,
|
||||
Notice,
|
||||
Connect,
|
||||
Reward,
|
||||
Death,
|
||||
}
|
||||
|
||||
pub struct CorpId(pub i64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -41,11 +50,12 @@ pub struct CorpMembership {
|
||||
pub allow_combat: bool,
|
||||
pub job_title: String,
|
||||
pub priority: i64,
|
||||
pub chat_on: bool
|
||||
pub comms_on: Vec<CorpCommType>,
|
||||
}
|
||||
|
||||
impl Default for CorpMembership {
|
||||
fn default() -> CorpMembership {
|
||||
use CorpCommType::*;
|
||||
CorpMembership {
|
||||
invited_at: None,
|
||||
joined_at: None,
|
||||
@ -53,7 +63,13 @@ impl Default for CorpMembership {
|
||||
allow_combat: false,
|
||||
job_title: "Employee".to_owned(),
|
||||
priority: 100,
|
||||
chat_on: true,
|
||||
comms_on: vec!(
|
||||
Chat,
|
||||
Notice,
|
||||
Connect,
|
||||
Reward,
|
||||
Death,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,30 +7,12 @@ use crate::{
|
||||
use mockall_double::double;
|
||||
#[double] use crate::db::DBTrans;
|
||||
|
||||
pub mod skills;
|
||||
pub mod comms;
|
||||
pub mod combat;
|
||||
pub mod skills;
|
||||
pub mod capacity;
|
||||
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 {
|
||||
if let Some((loctype, loccode)) = target.location.split_once("/") {
|
||||
if !consent.only_in.is_empty() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
services::{
|
||||
broadcast_to_room,
|
||||
comms::broadcast_to_room,
|
||||
skills::skill_check_and_grind,
|
||||
skills::skill_check_only,
|
||||
},
|
||||
|
25
blastmud_game/src/services/comms.rs
Normal file
25
blastmud_game/src/services/comms.rs
Normal 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(())
|
||||
}
|
@ -18,7 +18,7 @@ use crate::{
|
||||
}
|
||||
};
|
||||
use super::{
|
||||
broadcast_to_room,
|
||||
comms::broadcast_to_room,
|
||||
combat::change_health,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
@ -11,7 +11,7 @@ use crate::{
|
||||
UResult,
|
||||
VerbContext,
|
||||
},
|
||||
services::broadcast_to_room,
|
||||
services::comms::broadcast_to_room,
|
||||
};
|
||||
use ansi::ansi;
|
||||
use async_trait::async_trait;
|
||||
|
Loading…
Reference in New Issue
Block a user