From 51d90707b5fe1d1864b096c8a8cf24949dc52093 Mon Sep 17 00:00:00 2001
From: Joshua Barretto <joshua.s.barretto@gmail.com>
Date: Tue, 23 Aug 2022 10:03:06 +0100
Subject: [PATCH] Relaxed automod somewhat

---
 common/src/comp/chat.rs | 19 +++++++++++++++++++
 server/src/automod.rs   | 12 +++++++++---
 server/src/state_ext.rs | 19 ++++++++++++++++---
 3 files changed, 44 insertions(+), 6 deletions(-)

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<G> ChatType<G> {
             ChatType::Meta => None,
         }
     }
+
+    /// `None` means that the chat type is automated.
+    pub fn is_private(&self) -> Option<bool> {
+        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<AdminRole>,
         now: Instant,
+        chat_type: &ChatType<Group>,
         msg: &str,
     ) -> Result<Option<ActionNote>, 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<comp::Group>,
+        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<comp::Group>,
+        msg: &str,
+    ) -> bool {
         let mut automod = self.ecs().write_resource::<AutoMod>();
         let Some(client) = self.ecs().read_storage::<Client>().get(entity) else { return true };
         let Some(player) = self.ecs().read_storage::<Player>().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::<UidAllocator>())
                 .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(_)