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; pub 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; 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 less_explicit_mode; 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 put; mod quit; pub mod recline; pub mod register; pub mod remove; pub mod rent; mod report; mod reset_spawns; pub mod say; pub mod scavenge; mod score; mod sign; pub mod sit; mod staff_show; pub mod stand; mod status; 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, pub trans: &'l DBTrans, } pub enum CommandHandlingError { UserError(String), SystemError(Box), } pub use CommandHandlingError::*; #[async_trait] pub trait UserVerb { async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()>; } pub type UResult = Result; impl From for CommandHandlingError where T: Into>, { fn from(input: T) -> CommandHandlingError { SystemError(input.into()) } } pub fn user_error(msg: String) -> UResult { 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, "less_explicit_mode" => less_explicit_mode::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, "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, "put" => put::VERB, "recline" => recline::VERB, "remove" => remove::VERB, "rent" => rent::VERB, "report" => report::VERB, "\'" => say::VERB, "say" => say::VERB, "scavenge" => scavenge::VERB, "search" => scavenge::VERB, "sc" => score::VERB, "score" => score::VERB, "sign" => sign::VERB, "sit" => sit::VERB, "stand" => stand::VERB, "st" => status::VERB, "stat" => status::VERB, "stats" => status::VERB, "status" => status::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_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 help\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_explicit(msg: &str) -> bool { static EXPLICIT_MARKER_WORDS: OnceCell> = OnceCell::new(); let markers = EXPLICIT_MARKER_WORDS.get_or_init(|| { vec![ "fuck", "sex", "cock", "cunt", "dick", "pussy", "whore", "orgasm", "erection", "nipple", "boob", "tit", ] }); for word in markers { if msg.contains(word) { return true; } } false } 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> { 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> { 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>> { 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 = 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, "less_explicit_mode").is_some(), true); } }