diff --git a/blastmud_game/src/message_handler/user_commands/corp.rs b/blastmud_game/src/message_handler/user_commands/corp.rs index 93086985..98286650 100644 --- a/blastmud_game/src/message_handler/user_commands/corp.rs +++ b/blastmud_game/src/message_handler/user_commands/corp.rs @@ -16,7 +16,10 @@ use crate::{ }; use chrono::Utc; use async_trait::async_trait; -use ansi::ansi; +use ansi::{ansi, ignore_special_characters}; +use std::collections::BTreeSet; +use itertools::Itertools; +use humantime; fn check_corp_perm(perm: &CorpPermission, mem: &CorpMembership) -> bool { mem.permissions.iter().any(|p| *p == CorpPermission::Holder || *p == *perm) @@ -33,13 +36,13 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> 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())?, + None => user_error("You don't seem to belong to a matching corp!".to_owned())?, Some(c) => c }; if !check_corp_perm(&CorpPermission::Hire, &mem) || mem.joined_at.is_none() { user_error("You don't have hiring permissions for that corp".to_owned())?; } - + let target_user = search_item_for_user(ctx, &ItemSearchParams { include_loc_contents: true, ..ItemSearchParams::base(&player, target_raw.trim()) @@ -54,6 +57,11 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> Some(c) => c }; + if ctx.trans.list_corp_members(&corp_id).await?.len() > 40 { + user_error("Your corp seems a bit too big to manage already \ + - fire someone first!".to_owned())?; + } + ctx.trans.expire_old_invites().await?; match ctx.trans.match_user_corp_by_name(into_raw.trim(), &target_user.item_code).await? { @@ -173,7 +181,7 @@ async fn corp_leave(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { if members.len() == 1 { delete_corp = true; } else if !members.iter().any( - |(name, mem)| *name != username_l && + |(name, mem)| *name != username_l && mem.joined_at.is_some() && mem.permissions.contains(&CorpPermission::Holder)) { user_error("The last holder cannot resign from a non-empty \ corp - fire everyone else first, or promote a \ @@ -198,7 +206,7 @@ async fn corp_fire(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { 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())?, + None => user_error("You don't seem to belong to a matching corp!".to_owned())?, Some(c) => c }; if !check_corp_perm(&CorpPermission::Fire, &mem) || mem.joined_at.is_none() { @@ -231,7 +239,7 @@ async fn corp_fire(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { } } }, - _ => {} + _ => {}, } @@ -248,6 +256,167 @@ async fn corp_fire(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { Ok(()) } +async fn corp_promote(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { + let usage_error = || user_error( + ansi!("Usage: corp promote username in corpname to title privileges permissions\n\ + Title can be any plain text up to 20 characters long.\n\ + Permissions start with + or - (to add or take away) followed immediately by a permission name (e.g. holder, hire, fire, war)").to_owned() + ); + let (remaining, permissions_raw) = match remaining.rsplit_once(" privileges ") { + None => usage_error()?, + Some(c) => c + }; + let (target_raw, remaining) = match remaining.split_once(" in ") { + None => usage_error()?, + Some(c) => c + }; + let (corpname_raw, title_raw) = match remaining.split_once(" to ") { + None => usage_error()?, + Some(c) => c + }; + let title = ignore_special_characters(title_raw.trim()); + if title.len() > 20 { + user_error("New title must be 20 characters or less".to_owned())?; + } + let psplit = permissions_raw.split(" "); + let mut perm_add: BTreeSet = BTreeSet::new(); + let mut perm_rem: BTreeSet = BTreeSet::new(); + for perm in psplit { + let add = + if perm.starts_with("+") { + true + } else if perm.starts_with("-") { + false + } else { + user_error(format!("Expected {} to start with + or -", + ignore_special_characters(perm)))? + }; + let perm = ignore_special_characters(&perm[1..]).to_lowercase(); + let perm = match CorpPermission::parse(&perm) { + None => user_error(format!("Unknown permission {}", perm))?, + Some(v) => v + }; + (if add { &mut perm_add } else { &mut perm_rem }).insert(perm); + } + match perm_add.intersection(&perm_rem).next() { + Some(perm) => user_error(format!("You tried to both add and remove privilege {} - make up your mind!", + perm.display()))?, + None => {} + } + + 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(corpname_raw.trim(), &user.username).await? { + None => user_error("You don't seem to belong to a matching corp!".to_owned())?, + Some(c) => c + }; + if !check_corp_perm(&CorpPermission::Promote, &mem) || mem.joined_at.is_none() { + user_error("You don't have promote 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 promoted.".to_owned())?; + } + + let mut their_mem = match ctx.trans.match_user_corp_by_name(corpname_raw.trim(), &target_user.item_code).await? { + None => user_error(format!( + "{} isn't currently hired.", + &caps_first(&target_user.pronouns.subject) + ))?, + Some((_, _, v)) => v + }; + match their_mem { + CorpMembership { permissions: ref 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 promote/demote holders!".to_owned())?; + } + if their_join < mem.joined_at.unwrap_or(Utc::now()) { + user_error("Whoah there young whippersnapper, holders can't promote/demote more senior holders!".to_owned())?; + } + } + }, + _ => {} + } + if target_user.item_code == player.item_code { + if !mem.permissions.contains(&CorpPermission::Holder) { + user_error("Only holders can promote / demote themselves".to_owned())? + } else if perm_rem.contains(&CorpPermission::Holder) { + let members = ctx.trans.list_corp_members(&corp_id).await?; + if !members.iter().any( + |(name, mem)| *name != player.item_code && mem.joined_at.is_some() && + mem.permissions.contains(&CorpPermission::Holder)) { + user_error("The last holder cannot demote themselves - \ + promote a successor to holder first".to_owned())? + } + } + } + if !mem.permissions.contains(&CorpPermission::Holder) && + !(&perm_add | &perm_rem).is_subset(&mem.permissions.clone().into_iter().collect()) { + user_error("You can only change permissions you have yourself.".to_owned())? + } + + let perm_str_raw = + perm_add.iter() + .map(|v| "+".to_owned() + v.display()).join(" ") + + " " + + &perm_rem.iter().map(|v| "-".to_owned() + v.display()) + .join(" "); + + ctx.trans.broadcast_to_corp( + &corp_id, + &CorpCommType::Notice, None, + &format!("Everyone looks up from their desk as {} changes {}'s job title in {} to {} ({}).\n", + user.username, + target_user.display_for_sentence(false, 1, false), + corp.name, &title, &perm_str_raw.trim())).await?; + + their_mem.job_title = title; + their_mem.permissions = (&(&their_mem.permissions.clone().into_iter().collect::>() | + &perm_add) - &perm_rem).into_iter().collect(); + ctx.trans.upsert_corp_membership(&corp_id, &target_user.item_code, &their_mem).await?; + Ok(()) +} + +async fn corp_info(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { + let user = get_user_or_fail(ctx)?; + let (corp_id, corp, _) = + match ctx.trans.match_user_corp_by_name(remaining.trim(), &user.username).await? { + None => user_error("You don't seem to belong to a matching corp!".to_owned())?, + Some(c) => c + }; + let mut msg = String::new(); + let founded_ago = + humantime::format_duration( + std::time::Duration::from_secs( + (Utc::now() - corp.founded).num_seconds() as u64)); + msg.push_str(&format!(ansi!("{}'s essential information\nFounded: {} ago\n"), + &corp.name, &founded_ago)); + msg.push_str("Members:\n"); + msg.push_str(&format!(ansi!("| {:20} | {:20} | {:20} |\n"), "Name", "Title", "Permissions" + )); + for (user, mem) in ctx.trans.list_corp_members(&corp_id).await? { + msg.push_str( + &format!(ansi!("| {:20} | {:20} | {:20} |\n"), + caps_first(&user), + mem.job_title, + mem.permissions.iter().map(|p| p.display()) + .join(" ") + ) + ); + } + + ctx.trans.queue_for_session(&ctx.session, Some(&msg)).await?; + Ok(()) +} + pub struct Verb; #[async_trait] impl UserVerb for Verb { @@ -259,6 +428,8 @@ impl UserVerb for Verb { "" | "list" => corp_list(ctx, remaining).await?, "leave" | "resign" => corp_leave(ctx, remaining).await?, "fire" | "dismiss" => corp_fire(ctx, remaining).await?, + "promote" | "demote" => corp_promote(ctx, remaining).await?, + "info" => corp_info(ctx, remaining).await?, _ => user_error("Unknown command".to_owned())? } Ok(()) diff --git a/blastmud_game/src/models/corp.rs b/blastmud_game/src/models/corp.rs index dba54d94..87e745d5 100644 --- a/blastmud_game/src/models/corp.rs +++ b/blastmud_game/src/models/corp.rs @@ -1,13 +1,44 @@ use serde::{Serialize, Deserialize}; use chrono::{DateTime, Utc}; -#[derive(Serialize, Deserialize, PartialEq)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Clone)] pub enum CorpPermission { Holder, // Implies all permissions. Hire, Fire, - ChangeJobTitle, + Promote, + War, + Configure, + Finance, } +impl CorpPermission { + pub fn parse(s: &str) -> Option { + use CorpPermission::*; + match s { + "holder" => Some(Holder), + "hire" => Some(Hire), + "fire" => Some(Fire), + "promote" => Some(Promote), + "war" => Some(War), + "config" | "configure" => Some(Configure), + "finance" | "finances" => Some(Finance), + _ => None + } + } + pub fn display(&self) -> &'static str { + use CorpPermission::*; + match self { + Holder => "holder", + Hire => "hire", + Fire => "fire", + Promote => "promote", + War => "war", + Configure => "configure", + Finance => "finance", + } + } +} + #[derive(Serialize, Deserialize, PartialEq)] pub enum CorpCommType { @@ -24,6 +55,7 @@ pub struct CorpId(pub i64); #[serde(default)] pub struct Corp { pub name: String, + pub founded: DateTime, // If true, new members get allow_combat on, and members cannot turn // allow_combat off. This will allow duly authorised corp members to // consent to combat with other corps, and have it apply to members. @@ -37,6 +69,7 @@ impl Default for Corp { name: "Unset".to_owned(), allow_combat_required: false, member_permissions: vec!(), + founded: Utc::now(), } } }