569 lines
14 KiB
Rust
569 lines
14 KiB
Rust
use super::ListenerSession;
|
||
#[cfg(not(test))]
|
||
use crate::db::is_concurrency_error;
|
||
#[double]
|
||
use crate::db::DBTrans;
|
||
use crate::db::{DBPool, ItemSearchParams};
|
||
use crate::models::user::UserFlag;
|
||
use crate::models::{item::Item, session::Session, user::User};
|
||
use crate::DResult;
|
||
#[cfg(not(test))]
|
||
use ansi::ansi;
|
||
use async_trait::async_trait;
|
||
#[cfg(not(test))]
|
||
use log::warn;
|
||
use mockall_double::double;
|
||
use once_cell::sync::OnceCell;
|
||
use phf::phf_map;
|
||
use std::sync::Arc;
|
||
|
||
mod agree;
|
||
mod allow;
|
||
mod attack;
|
||
mod butcher;
|
||
mod buy;
|
||
mod c;
|
||
pub mod close;
|
||
pub mod corp;
|
||
pub mod cut;
|
||
pub mod delete;
|
||
mod describe;
|
||
pub mod drink;
|
||
pub mod drop;
|
||
pub mod eat;
|
||
mod feint;
|
||
pub mod fill;
|
||
mod fire;
|
||
pub mod follow;
|
||
mod gear;
|
||
pub mod get;
|
||
mod hack;
|
||
mod help;
|
||
pub mod hire;
|
||
mod ignore;
|
||
pub mod improvise;
|
||
mod install;
|
||
mod inventory;
|
||
mod invincible;
|
||
mod list;
|
||
pub mod load;
|
||
mod login;
|
||
mod look;
|
||
pub mod make;
|
||
mod map;
|
||
pub mod movement;
|
||
pub mod open;
|
||
mod page;
|
||
pub mod parsing;
|
||
pub mod pay;
|
||
pub mod plug;
|
||
mod pow;
|
||
pub mod put;
|
||
mod quit;
|
||
pub mod recline;
|
||
pub mod register;
|
||
pub mod remove;
|
||
pub mod rent;
|
||
mod report;
|
||
mod reset_spawns;
|
||
pub mod say;
|
||
mod scan;
|
||
pub mod scavenge;
|
||
mod score;
|
||
mod sell;
|
||
mod share;
|
||
mod sign;
|
||
pub mod sit;
|
||
mod staff_show;
|
||
pub mod stand;
|
||
mod status;
|
||
mod stop;
|
||
mod turn;
|
||
mod uninstall;
|
||
pub mod use_cmd;
|
||
mod vacate;
|
||
pub mod wear;
|
||
mod whisper;
|
||
mod who;
|
||
pub mod wield;
|
||
mod write;
|
||
|
||
pub struct VerbContext<'l> {
|
||
pub session: &'l ListenerSession,
|
||
pub session_dat: &'l mut Session,
|
||
pub user_dat: &'l mut Option<User>,
|
||
pub trans: &'l DBTrans,
|
||
}
|
||
|
||
pub enum CommandHandlingError {
|
||
UserError(String),
|
||
SystemError(Box<dyn std::error::Error + Send + Sync>),
|
||
}
|
||
pub use CommandHandlingError::*;
|
||
|
||
#[async_trait]
|
||
pub trait UserVerb {
|
||
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str)
|
||
-> UResult<()>;
|
||
}
|
||
|
||
pub type UResult<A> = Result<A, CommandHandlingError>;
|
||
|
||
impl<T> From<T> for CommandHandlingError
|
||
where
|
||
T: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||
{
|
||
fn from(input: T) -> CommandHandlingError {
|
||
SystemError(input.into())
|
||
}
|
||
}
|
||
|
||
pub fn user_error<A>(msg: String) -> UResult<A> {
|
||
Err(UserError(msg))
|
||
}
|
||
|
||
/* Verb registries list types of commands available in different circumstances. */
|
||
pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send);
|
||
type UserVerbRegistry = phf::Map<&'static str, UserVerbRef>;
|
||
|
||
static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
|
||
"" => ignore::VERB,
|
||
"help" => help::VERB,
|
||
"quit" => quit::VERB,
|
||
};
|
||
|
||
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||
"agree" => agree::VERB,
|
||
"connect" => login::VERB,
|
||
"login" => login::VERB,
|
||
"register" => register::VERB,
|
||
};
|
||
|
||
static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||
// Movement comments first:
|
||
"north" => movement::VERB,
|
||
"n" => movement::VERB,
|
||
"northeast" => movement::VERB,
|
||
"ne" => movement::VERB,
|
||
"east" => movement::VERB,
|
||
"e" => movement::VERB,
|
||
"southeast" => movement::VERB,
|
||
"se" => movement::VERB,
|
||
"south" => movement::VERB,
|
||
"s" => movement::VERB,
|
||
"southwest" => movement::VERB,
|
||
"sw" => movement::VERB,
|
||
"west" => movement::VERB,
|
||
"w" => movement::VERB,
|
||
"northwest" => movement::VERB,
|
||
"nw" => movement::VERB,
|
||
"up" => movement::VERB,
|
||
"down" => movement::VERB,
|
||
"in" => movement::VERB,
|
||
|
||
// Other commands (alphabetical except aliases grouped):
|
||
"allow" => allow::VERB,
|
||
"disallow" => allow::VERB,
|
||
|
||
"attack" => attack::VERB,
|
||
"butcher" => butcher::VERB,
|
||
"buy" => buy::VERB,
|
||
"c" => c::VERB,
|
||
"close" => close::VERB,
|
||
"corp" => corp::VERB,
|
||
"cut" => cut::VERB,
|
||
"delete" => delete::VERB,
|
||
"drink" => drink::VERB,
|
||
"drop" => drop::VERB,
|
||
"eat" => eat::VERB,
|
||
"fill" => fill::VERB,
|
||
"fire" => fire::VERB,
|
||
|
||
"follow" => follow::VERB,
|
||
"unfollow" => follow::VERB,
|
||
|
||
"gear" => gear::VERB,
|
||
"get" => get::VERB,
|
||
"hack" => hack::VERB,
|
||
"hire" => hire::VERB,
|
||
|
||
"improv" => improvise::VERB,
|
||
"improvise" => improvise::VERB,
|
||
"improvize" => improvise::VERB,
|
||
|
||
"install" => install::VERB,
|
||
"inventory" => inventory::VERB,
|
||
"inv" => inventory::VERB,
|
||
"i" => inventory::VERB,
|
||
"kill" => attack::VERB,
|
||
"k" => attack::VERB,
|
||
|
||
"describe" => describe::VERB,
|
||
"l" => look::VERB,
|
||
"look" => look::VERB,
|
||
"read" => look::VERB,
|
||
"examine" => look::VERB,
|
||
"ex" => look::VERB,
|
||
|
||
"feint" => feint::VERB,
|
||
"list" => list::VERB,
|
||
|
||
"load" => load::VERB,
|
||
"unload" => load::VERB,
|
||
|
||
"lm" => map::VERB,
|
||
"lmap" => map::VERB,
|
||
"gm" => map::VERB,
|
||
"gmap" => map::VERB,
|
||
|
||
"make" => make::VERB,
|
||
"open" => open::VERB,
|
||
|
||
"p" => page::VERB,
|
||
"page" => page::VERB,
|
||
"pg" => page::VERB,
|
||
"rep" => page::VERB,
|
||
"repl" => page::VERB,
|
||
"reply" => page::VERB,
|
||
|
||
"pay" => pay::VERB,
|
||
"plug" => plug::VERB,
|
||
|
||
"pow" => pow::VERB,
|
||
"power" => pow::VERB,
|
||
"powerattack" => pow::VERB,
|
||
|
||
"put" => put::VERB,
|
||
"recline" => recline::VERB,
|
||
"remove" => remove::VERB,
|
||
"rent" => rent::VERB,
|
||
"report" => report::VERB,
|
||
|
||
"\'" => say::VERB,
|
||
"say" => say::VERB,
|
||
|
||
"scan" => scan::VERB,
|
||
|
||
"scavenge" => scavenge::VERB,
|
||
"search" => scavenge::VERB,
|
||
|
||
"sc" => score::VERB,
|
||
"score" => score::VERB,
|
||
|
||
"sell" => sell::VERB,
|
||
|
||
"share" => share::VERB,
|
||
"serious" => share::VERB,
|
||
"amicable" => share::VERB,
|
||
"joking" => share::VERB,
|
||
"parody" => share::VERB,
|
||
"play" => share::VERB,
|
||
"thoughts" => share::VERB,
|
||
"exploring" => share::VERB,
|
||
"roaming" => share::VERB,
|
||
"fishing" => share::VERB,
|
||
"good" => share::VERB,
|
||
"surviving" => share::VERB,
|
||
"slow" => share::VERB,
|
||
"normal" => share::VERB,
|
||
"intense" => share::VERB,
|
||
|
||
"sign" => sign::VERB,
|
||
"sit" => sit::VERB,
|
||
"stand" => stand::VERB,
|
||
|
||
"st" => status::VERB,
|
||
"stat" => status::VERB,
|
||
"stats" => status::VERB,
|
||
"status" => status::VERB,
|
||
|
||
"stop" => stop::VERB,
|
||
"turn" => turn::VERB,
|
||
"uninstall" => uninstall::VERB,
|
||
"use" => use_cmd::VERB,
|
||
"vacate" => vacate::VERB,
|
||
|
||
"-" => whisper::VERB,
|
||
"whisper" => whisper::VERB,
|
||
"tell" => whisper::VERB,
|
||
|
||
"wear" => wear::VERB,
|
||
"wield" => wield::VERB,
|
||
"who" => who::VERB,
|
||
"write" => write::VERB,
|
||
};
|
||
|
||
static STAFF_COMMANDS: UserVerbRegistry = phf_map! {
|
||
"staff_invincible" => invincible::VERB,
|
||
"staff_reset_spawns" => reset_spawns::VERB,
|
||
"staff_show" => staff_show::VERB,
|
||
};
|
||
|
||
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
|
||
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
|
||
|
||
match &ctx.user_dat {
|
||
None => {
|
||
result = result.or_else(|| UNREGISTERED_COMMANDS.get(cmd));
|
||
}
|
||
Some(user_dat) => {
|
||
if user_dat.terms.terms_complete {
|
||
result = result.or_else(|| REGISTERED_COMMANDS.get(cmd));
|
||
|
||
if user_dat.user_flags.contains(&UserFlag::Staff) {
|
||
result = result.or_else(|| STAFF_COMMANDS.get(cmd));
|
||
}
|
||
} else if cmd == "agree" {
|
||
result = Some(&agree::VERB);
|
||
}
|
||
}
|
||
}
|
||
|
||
result
|
||
}
|
||
|
||
#[cfg(not(test))]
|
||
pub async fn handle_in_trans(
|
||
session: &ListenerSession,
|
||
msg: &str,
|
||
pool: &DBPool,
|
||
trans: DBTrans,
|
||
) -> DResult<()> {
|
||
let (cmd, params) = parsing::parse_command_name(msg);
|
||
let (mut session_dat, mut user_dat) = match trans.get_session_user_model(session).await? {
|
||
None => {
|
||
// If the session has been cleaned up from the database, there is
|
||
// nowhere to go from here, so just ignore it.
|
||
warn!(
|
||
"Got command from session not in database: {}",
|
||
session.session
|
||
);
|
||
return Ok(());
|
||
}
|
||
Some(v) => v,
|
||
};
|
||
|
||
let mut ctx = VerbContext {
|
||
session,
|
||
trans: &trans,
|
||
session_dat: &mut session_dat,
|
||
user_dat: &mut user_dat,
|
||
};
|
||
let handler_opt = resolve_handler(&ctx, cmd);
|
||
|
||
match handler_opt {
|
||
None => {
|
||
trans
|
||
.queue_for_session(
|
||
session,
|
||
Some(ansi!(
|
||
"That's not a command I know. Try <bold>help<reset>\r\n"
|
||
)),
|
||
)
|
||
.await?;
|
||
trans.commit().await?;
|
||
}
|
||
Some(handler) => match handler.handle(&mut ctx, cmd, params).await {
|
||
Ok(()) => {
|
||
trans.commit().await?;
|
||
}
|
||
Err(UserError(err_msg)) => {
|
||
pool.queue_for_session(session, Some(&(err_msg + "\r\n")))
|
||
.await?;
|
||
}
|
||
Err(SystemError(e)) => Err(e)?,
|
||
},
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(not(test))]
|
||
pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DResult<()> {
|
||
loop {
|
||
let trans = pool.start_transaction().await?;
|
||
match handle_in_trans(session, msg, pool, trans).await {
|
||
Ok(_) => break,
|
||
Err(e) => {
|
||
if is_concurrency_error(e.as_ref()) {
|
||
continue;
|
||
} else {
|
||
return Err(e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
pool.bump_session_time(&session).await?;
|
||
Ok(())
|
||
}
|
||
#[cfg(test)]
|
||
pub async fn handle(_session: &ListenerSession, _msg: &str, _pool: &DBPool) -> DResult<()> {
|
||
unimplemented!();
|
||
}
|
||
|
||
pub fn is_likely_illegal(msg: &str) -> bool {
|
||
static ILLEGAL_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
|
||
let illegal_markers = ILLEGAL_MARKER_WORDS.get_or_init(|| {
|
||
vec![
|
||
"lolita",
|
||
"jailbait",
|
||
"abu sayyaf",
|
||
"al-qa’ida",
|
||
"al-qaida",
|
||
"al-shabaab",
|
||
"boko haram",
|
||
"hamas",
|
||
"tahrir al-sham",
|
||
"hizballah",
|
||
"hezbollah",
|
||
"hurras al-din",
|
||
"islamic state",
|
||
"jaish-e-mohammad",
|
||
"jama’at mujahideen",
|
||
"jamaat mujahideen",
|
||
"jama’at nusrat",
|
||
"jamaat nusrat",
|
||
"jemaah islamiyah",
|
||
"kurdistan workers",
|
||
"lashkar-e-tayyiba",
|
||
"likud",
|
||
"national socialist order",
|
||
"palestinian islamic jihad",
|
||
"sonnenkrieg",
|
||
"race war",
|
||
// For now we'll block all URLs - we could allow by domain perhaps?
|
||
"http:",
|
||
"https:",
|
||
"ftp:",
|
||
]
|
||
});
|
||
static MINOR_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
|
||
let minor_markers =
|
||
MINOR_MARKER_WORDS.get_or_init(|| vec!["young", "underage", "child", "teen", "minor"]);
|
||
static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
|
||
let explicit_markers = EXPLICIT_MARKER_WORDS.get_or_init(|| {
|
||
vec![
|
||
"fuck",
|
||
"sex",
|
||
"cock",
|
||
"cunt",
|
||
"dick",
|
||
"pussy",
|
||
"whore",
|
||
"orgasm",
|
||
"erection",
|
||
"nipple",
|
||
"boob",
|
||
"tit",
|
||
"xxx",
|
||
"nsfw",
|
||
"uncensored",
|
||
]
|
||
});
|
||
let msg_lower = msg.to_lowercase();
|
||
for word in illegal_markers {
|
||
if msg_lower.contains(word) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
let mut minor_word = false;
|
||
let mut explicit_word = false;
|
||
for word in minor_markers {
|
||
if msg_lower.contains(word) {
|
||
minor_word = true;
|
||
}
|
||
}
|
||
for word in explicit_markers {
|
||
if msg_lower.contains(word) {
|
||
explicit_word = true;
|
||
}
|
||
}
|
||
|
||
explicit_word && minor_word
|
||
}
|
||
|
||
pub fn get_user_or_fail<'l>(ctx: &'l VerbContext) -> UResult<&'l User> {
|
||
ctx.user_dat
|
||
.as_ref()
|
||
.ok_or_else(|| UserError("Not logged in".to_owned()))
|
||
}
|
||
|
||
pub fn get_user_or_fail_mut<'l>(ctx: &'l mut VerbContext) -> UResult<&'l mut User> {
|
||
ctx.user_dat
|
||
.as_mut()
|
||
.ok_or_else(|| UserError("Not logged in".to_owned()))
|
||
}
|
||
|
||
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Arc<Item>> {
|
||
Ok(ctx
|
||
.trans
|
||
.find_item_by_type_code("player", &get_user_or_fail(ctx)?.username.to_lowercase())
|
||
.await?
|
||
.ok_or_else(|| {
|
||
UserError(
|
||
"Your character is gone, you'll need to re-register or ask an admin".to_owned(),
|
||
)
|
||
})?)
|
||
}
|
||
|
||
pub async fn search_item_for_user<'l>(
|
||
ctx: &'l VerbContext<'l>,
|
||
search: &'l ItemSearchParams<'l>,
|
||
) -> UResult<Arc<Item>> {
|
||
Ok(
|
||
match &ctx
|
||
.trans
|
||
.resolve_items_by_display_name_for_player(search)
|
||
.await?[..]
|
||
{
|
||
[] => user_error(format!(
|
||
"Sorry, I couldn't find anything matching \"{}\".",
|
||
search.query
|
||
))?,
|
||
[match_it] => match_it.clone(),
|
||
[item1, ..] => item1.clone(),
|
||
},
|
||
)
|
||
}
|
||
|
||
pub async fn search_items_for_user<'l>(
|
||
ctx: &'l VerbContext<'l>,
|
||
search: &'l ItemSearchParams<'l>,
|
||
) -> UResult<Vec<Arc<Item>>> {
|
||
Ok(
|
||
match &ctx
|
||
.trans
|
||
.resolve_items_by_display_name_for_player(search)
|
||
.await?[..]
|
||
{
|
||
[] => user_error(format!(
|
||
"Sorry, I couldn't find anything matching \"{}\".",
|
||
search.query
|
||
))?,
|
||
v => v.into_iter().map(|it| it.clone()).collect(),
|
||
},
|
||
)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod test {
|
||
use crate::db::MockDBTrans;
|
||
|
||
#[test]
|
||
fn resolve_handler_finds_unregistered() {
|
||
use super::*;
|
||
|
||
let trans = MockDBTrans::new();
|
||
let sess: ListenerSession = Default::default();
|
||
let mut user_dat: Option<User> = None;
|
||
let mut session_dat: Session = Default::default();
|
||
let ctx = VerbContext {
|
||
session: &sess,
|
||
trans: &trans,
|
||
session_dat: &mut session_dat,
|
||
user_dat: &mut user_dat,
|
||
};
|
||
assert_eq!(resolve_handler(&ctx, "agree").is_some(), true);
|
||
}
|
||
}
|