diff --git a/common/src/comp/chat.rs b/common/src/comp/chat.rs index 791e47580c..228f12cbab 100644 --- a/common/src/comp/chat.rs +++ b/common/src/comp/chat.rs @@ -146,6 +146,25 @@ impl ChatType { ChatType::Meta => None, } } + + /// `None` means that the chat type is automated. + pub fn is_private(&self) -> Option { + match self { + ChatType::Online(_) + | ChatType::Offline(_) + | ChatType::CommandInfo + | ChatType::CommandError + | ChatType::FactionMeta(_) + | ChatType::GroupMeta(_) + | ChatType::Npc(_, _) + | ChatType::NpcSay(_, _) + | ChatType::NpcTell(_, _, _) + | ChatType::Meta + | ChatType::Kill(_, _) => None, + ChatType::Tell(_, _) | ChatType::Group(_, _) | ChatType::Faction(_, _) => Some(true), + ChatType::Say(_) | ChatType::Region(_) | ChatType::World(_) => Some(false), + } + } } // Stores chat text, type diff --git a/server/src/automod.rs b/server/src/automod.rs index c9bc3b16e8..4f6e683895 100644 --- a/server/src/automod.rs +++ b/server/src/automod.rs @@ -1,7 +1,7 @@ use crate::settings::ModerationSettings; use authc::Uuid; use censor::Censor; -use common::comp::AdminRole; +use common::comp::{AdminRole, ChatType, Group}; use hashbrown::HashMap; use std::{ fmt, @@ -95,12 +95,18 @@ impl AutoMod { player: Uuid, role: Option, now: Instant, + chat_type: &ChatType, msg: &str, ) -> Result, ActionErr> { // TODO: Consider using grapheme cluster count instead of size in bytes if msg.len() > MAX_BYTES_CHAT_MSG { Err(ActionErr::TooLong) - } else if !self.settings.automod || (role.is_some() && self.settings.admins_exempt) { + } else if !self.settings.automod + // Is this a private chat message? + || chat_type.is_private().unwrap_or(true) + // Is the user exempt from automoderation? + || (role.is_some() && self.settings.admins_exempt) + { Ok(None) } else if self.censor.check(msg) { Err(ActionErr::BannedWord) @@ -123,7 +129,7 @@ impl AutoMod { const CHAT_VOLUME_PERIOD: f32 = 30.0; /// The maximum permitted average number of chat messages over the chat volume /// period. -const MAX_AVG_MSG_PER_SECOND: f32 = 1.0 / 7.0; // No more than a message every 7 seconds on average +const MAX_AVG_MSG_PER_SECOND: f32 = 1.0 / 5.0; // No more than a message every 5 seconds on average /// The period for which a player should be muted when they exceed the message /// spam threshold. const SPAM_MUTE_PERIOD: Duration = Duration::from_secs(180); diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index fe3761795a..dd1241f5b8 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -114,7 +114,12 @@ pub trait StateExt { /// Performed after loading component data from the database fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents); /// Iterates over registered clients and send each `ServerMsg` - fn validate_chat_msg(&self, player: EcsEntity, msg: &str) -> bool; + fn validate_chat_msg( + &self, + player: EcsEntity, + chat_type: &comp::ChatType, + msg: &str, + ) -> bool; fn send_chat(&self, msg: comp::UnresolvedChatMsg); fn notify_players(&self, msg: ServerGeneral); fn notify_in_game_clients(&self, msg: ServerGeneral); @@ -676,7 +681,12 @@ impl StateExt for State { } } - fn validate_chat_msg(&self, entity: EcsEntity, msg: &str) -> bool { + fn validate_chat_msg( + &self, + entity: EcsEntity, + chat_type: &comp::ChatType, + msg: &str, + ) -> bool { let mut automod = self.ecs().write_resource::(); let Some(client) = self.ecs().read_storage::().get(entity) else { return true }; let Some(player) = self.ecs().read_storage::().get(entity) else { return true }; @@ -688,6 +698,7 @@ impl StateExt for State { .get(entity) .map(|a| a.0), Instant::now(), + chat_type, msg, ) { Ok(note) => { @@ -727,7 +738,9 @@ impl StateExt for State { if msg.chat_type.uid().map_or(true, |sender| { (*ecs.read_resource::()) .retrieve_entity_internal(sender.0) - .map_or(false, |e| self.validate_chat_msg(e, &msg.message)) + .map_or(false, |e| { + self.validate_chat_msg(e, &msg.chat_type, &msg.message) + }) }) { match &msg.chat_type { comp::ChatType::Offline(_)