Add ability to invite people to corps.

This commit is contained in:
Condorra 2023-03-19 22:59:35 +11:00
parent 8084b020c3
commit cb05843ee9
5 changed files with 186 additions and 2 deletions

View File

@ -814,6 +814,31 @@ impl DBTrans {
}) })
} }
pub async fn match_user_corp_by_name(&self, corpname: &str, username: &str) ->
DResult<Option<(CorpId, Corp, CorpMembership)>> {
Ok(match self.pg_trans()?
.query_opt("SELECT c.corp_id, c.details AS cdetails, m.details AS mdetails FROM corps c \
JOIN corp_membership m ON m.corp_id = c.corp_id \
WHERE LOWER(c.details->>'name') LIKE $1 AND \
m.member_username = $2 \
ORDER BY \
ABS(length(c.details->>'name')-length($1)) DESC \
LIMIT 1",
&[&(corpname.replace("\\", "\\\\")
.replace("_", "\\_")
.replace("%", "")
.to_lowercase() + "%"), &(username.to_lowercase())])
.await? {
None => None,
Some(row) =>
Some(
(CorpId(row.get("corp_id")),
serde_json::from_value(row.get("cdetails"))?,
serde_json::from_value(row.get("mdetails"))?)
)
})
}
pub async fn create_corp(&self, details: &Corp) -> DResult<CorpId> { pub async fn create_corp(&self, details: &Corp) -> DResult<CorpId> {
let id = self.pg_trans()? let id = self.pg_trans()?
.query_one("INSERT INTO corps (details) VALUES ($1) RETURNING corp_id", &[&serde_json::to_value(details)?]).await? .query_one("INSERT INTO corps (details) VALUES ($1) RETURNING corp_id", &[&serde_json::to_value(details)?]).await?
@ -821,6 +846,15 @@ impl DBTrans {
Ok(CorpId(id)) Ok(CorpId(id))
} }
pub async fn expire_old_invites(&self) -> DResult<()> {
self.pg_trans()?
.execute(
"DELETE FROM corp_membership WHERE details->>'invited_at' <= $1",
&[&(Utc::now() - chrono::Duration::hours(4)).to_rfc3339_opts(chrono::SecondsFormat::Nanos, true)]
).await?;
Ok(())
}
pub async fn upsert_corp_membership(&self, corp: &CorpId, username: &str, details: &CorpMembership) -> DResult<()> { pub async fn upsert_corp_membership(&self, corp: &CorpId, username: &str, details: &CorpMembership) -> DResult<()> {
self.pg_trans()? self.pg_trans()?
.execute("INSERT INTO corp_membership (corp_id, member_username, details) \ .execute("INSERT INTO corp_membership (corp_id, member_username, details) \
@ -837,7 +871,10 @@ impl DBTrans {
Ok(self.pg_trans()? Ok(self.pg_trans()?
.query("SELECT m.corp_id, c.details->>'name', m.details FROM corp_membership m \ .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 \ 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()]) AND m.details->>'joined_at' IS NOT NULL \
ORDER BY (m.details->>'priority')::int DESC NULLS LAST, \
(m.details->>'joined_at') :: TIMESTAMPTZ ASC",
&[&username.to_lowercase()])
.await? .await?
.into_iter() .into_iter()
.filter_map(|row| .filter_map(|row|

View File

@ -15,6 +15,7 @@ mod agree;
mod allow; mod allow;
pub mod attack; pub mod attack;
mod buy; mod buy;
mod corp;
pub mod drop; pub mod drop;
pub mod get; pub mod get;
mod describe; mod describe;
@ -118,6 +119,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"attack" => attack::VERB, "attack" => attack::VERB,
"buy" => buy::VERB, "buy" => buy::VERB,
"corp" => corp::VERB,
"drop" => drop::VERB, "drop" => drop::VERB,
"get" => get::VERB, "get" => get::VERB,
"inventory" => inventory::VERB, "inventory" => inventory::VERB,

View File

@ -0,0 +1,142 @@
use super::{
VerbContext,
UserVerb,
UserVerbRef,
UResult,
user_error,
get_user_or_fail,
get_player_item_or_fail,
search_item_for_user,
parsing::parse_command_name,
};
use crate::{
models::corp::{CorpMembership, CorpPermission},
db::ItemSearchParams,
};
use chrono::Utc;
use async_trait::async_trait;
use ansi::ansi;
fn check_corp_perm(perm: &CorpPermission, mem: &CorpMembership) -> bool {
mem.permissions.iter().any(|p| *p == CorpPermission::Holder || *p == *perm)
}
async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (target_raw, into_raw) = match remaining.rsplit_once(" into ") {
None => user_error(
ansi!("Usage: <bold>corp hire<reset> username <bold>into<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::Hire, &mem) || mem.joined_at.is_none() {
user_error("You don't have hire 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())
}).await?;
if target_user.item_type != "player" {
user_error("Only players can be hired.".to_owned())?;
}
let (their_sess, their_sess_dat) = match ctx.trans.find_session_for_player(&target_user.item_code).await? {
None => user_error("The user needs to be logged in while you hire them.".to_owned())?,
Some(c) => c
};
ctx.trans.expire_old_invites().await?;
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())?,
Some((_, _, _)) =>
user_error("They're already hired.".to_owned())?,
};
let new_mem = CorpMembership {
invited_at: Some(Utc::now()),
allow_combat: corp.allow_combat_required,
..Default::default()
};
ctx.trans.upsert_corp_membership(&corp_id, &target_user.item_code, &new_mem).await?;
ctx.trans.queue_for_session(&their_sess, Some(&format!(
ansi!("{} wants to hire you into {}! Type <bold>corp join {}<reset> to accept.{}\n"),
&player.display_for_sentence(!their_sess_dat.less_explicit_mode, 1, true),
&corp.name,
&corp.name,
if new_mem.allow_combat {
" This corp is configured to allow the leadership to declare war on other corps; if you join, you may be attacked by members of other corps, even without your personal consent."
} else {
""
}
))).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?;
Ok(())
}
async fn corp_join(_ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> {
user_error("Join not implemented yet".to_owned())?
}
async fn corp_list(ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> {
let mut msg = String::new();
let user = get_user_or_fail(ctx)?;
let corps = ctx.trans.get_corp_memberships_for_user(&user.username).await?;
if corps.is_empty() {
msg.push_str(ansi!(
"You don't yet belong to any corps - try <bold>who<reset> to see the \
corps of people online, and message someone in a corp to see if they \
will hire you! Or buy a new corp licence at the Kings Office in Melbs.\n"
));
} else {
msg.push_str("You belong to the following corps:\n");
msg.push_str(&format!(ansi!("<bgblue><white><bold>| {:20} | {:7} | {:5} |<reset>\n"),
"Name", "Combat?", "Order"));
for corp in &corps {
match corp {
(_, name, CorpMembership { allow_combat: combat, priority: p, .. }) => {
msg.push_str(&format!("| {:20} | {:7} | {:5} |\n",
name, if *combat { "Y" } else { "N" }, p));
}
}
}
}
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(())
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (command, remaining) = parse_command_name(remaining);
match command.to_lowercase().as_str() {
"hire" | "invite" => corp_invite(ctx, remaining).await?,
"join" => corp_join(ctx, remaining).await?,
"" | "list" => corp_list(ctx, remaining).await?,
_ => user_error("Unknown command".to_owned())?
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,7 +1,7 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, PartialEq)]
pub enum CorpPermission { pub enum CorpPermission {
Holder, // Implies all permissions. Holder, // Implies all permissions.
Hire, Hire,
@ -12,6 +12,7 @@ pub enum CorpPermission {
pub struct CorpId(pub i64); pub struct CorpId(pub i64);
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(default)]
pub struct Corp { pub struct Corp {
pub name: String, pub name: String,
// If true, new members get allow_combat on, and members cannot turn // If true, new members get allow_combat on, and members cannot turn
@ -32,6 +33,7 @@ impl Default for Corp {
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(default)]
pub struct CorpMembership { pub struct CorpMembership {
pub invited_at: Option<DateTime<Utc>>, pub invited_at: Option<DateTime<Utc>>,
pub joined_at: Option<DateTime<Utc>>, pub joined_at: Option<DateTime<Utc>>,

View File

@ -66,6 +66,7 @@ CREATE TABLE corp_membership (
PRIMARY KEY (corp_id, member_username) PRIMARY KEY (corp_id, member_username)
); );
CREATE INDEX corp_membership_by_username ON corp_membership(member_username); CREATE INDEX corp_membership_by_username ON corp_membership(member_username);
CREATE INDEX corp_membership_by_invited ON corp_membership((details->>'invited_at'));
CREATE TABLE user_consent ( CREATE TABLE user_consent (
consenting_user TEXT NOT NULL REFERENCES users(username), consenting_user TEXT NOT NULL REFERENCES users(username),