veloren_server/
automod.rs1use crate::settings::ModerationSettings;
2use authc::Uuid;
3use censor::Censor;
4use common::comp::{AdminRole, ChatMsg, ChatType, Group};
5use hashbrown::HashMap;
6use std::{
7 fmt,
8 sync::Arc,
9 time::{Duration, Instant},
10};
11use tracing::info;
12
13pub enum ActionNote {
14 SpamWarn,
15}
16
17impl fmt::Display for ActionNote {
18 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
19 match self {
20 ActionNote::SpamWarn => write!(
21 f,
22 "You've sent a lot of messages recently. Make sure to reduce the rate of messages \
23 or you will be automatically muted."
24 ),
25 }
26 }
27}
28
29pub enum ActionErr {
30 BannedWord,
31 TooLong,
32 SpamMuted(Duration),
33}
34
35impl fmt::Display for ActionErr {
36 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37 match self {
38 ActionErr::BannedWord => write!(
39 f,
40 "Your message contained a banned word. If you think this is a mistake, please let \
41 a moderator know."
42 ),
43 ActionErr::TooLong => write!(
44 f,
45 "Your message was too long, no more than {} characters are permitted.",
46 ChatMsg::MAX_BYTES_PLAYER_CHAT_MSG
47 ),
48 ActionErr::SpamMuted(dur) => write!(
49 f,
50 "You have sent too many messages and are muted for {} seconds.",
51 dur.as_secs_f32() as u64
52 ),
53 }
54 }
55}
56
57pub struct AutoMod {
58 settings: ModerationSettings,
59 censor: Arc<Censor>,
60 players: HashMap<Uuid, PlayerState>,
61}
62
63impl AutoMod {
64 pub fn new(settings: &ModerationSettings, censor: Arc<Censor>) -> Self {
65 if settings.automod {
66 info!(
67 "Automod enabled, players{} will be subject to automated spam/content filters",
68 if settings.admins_exempt {
69 ""
70 } else {
71 " (and admins)"
72 }
73 );
74 } else {
75 info!("Automod disabled");
76 }
77
78 Self {
79 settings: settings.clone(),
80 censor,
81 players: HashMap::default(),
82 }
83 }
84
85 pub fn enabled(&self) -> bool { self.settings.automod }
86
87 fn player_mut(&mut self, player: Uuid) -> &mut PlayerState {
88 self.players.entry(player).or_default()
89 }
90
91 pub fn validate_chat_msg(
92 &mut self,
93 player: Uuid,
94 role: Option<AdminRole>,
95 now: Instant,
96 chat_type: &ChatType<Group>,
97 msg: &str,
98 ) -> Result<Option<ActionNote>, ActionErr> {
99 if msg.len() > ChatMsg::MAX_BYTES_PLAYER_CHAT_MSG {
101 Err(ActionErr::TooLong)
102 } else if !self.settings.automod
103 || chat_type.is_private().unwrap_or(true)
105 || (role.is_some() && self.settings.admins_exempt)
107 {
108 Ok(None)
109 } else if self.censor.check(msg) {
110 Err(ActionErr::BannedWord)
111 } else {
112 let volume = self.player_mut(player).enforce_message_volume(now);
113
114 if let Some(until) = self.player_mut(player).muted_until {
115 Err(ActionErr::SpamMuted(until.saturating_duration_since(now)))
116 } else if volume > 0.75 {
117 Ok(Some(ActionNote::SpamWarn))
118 } else {
119 Ok(None)
120 }
121 }
122 }
123}
124
125const CHAT_VOLUME_PERIOD: f32 = 30.0;
128const MAX_AVG_MSG_PER_SECOND: f32 = 1.0 / 5.0; const SPAM_MUTE_PERIOD: Duration = Duration::from_secs(180);
134
135#[derive(Default)]
136pub struct PlayerState {
137 last_msg_time: Option<Instant>,
138 chat_volume: f32,
140 muted_until: Option<Instant>,
141}
142
143impl PlayerState {
144 pub fn enforce_message_volume(&mut self, now: Instant) -> f32 {
147 if self.muted_until.is_some_and(|u| u <= now) {
148 self.muted_until = None;
149 }
150
151 if let Some(time_since_last) = self
152 .last_msg_time
153 .map(|last| now.saturating_duration_since(last).as_secs_f32())
154 {
155 let time_proportion = (time_since_last / CHAT_VOLUME_PERIOD).min(1.0);
156 self.chat_volume = self.chat_volume * (1.0 - time_proportion)
157 + (1.0 / time_since_last) * time_proportion;
158 } else {
159 self.chat_volume = 0.0;
160 }
161 self.last_msg_time = Some(now);
162
163 let min_level = 1.0 / CHAT_VOLUME_PERIOD;
164 let max_level = MAX_AVG_MSG_PER_SECOND;
165
166 let volume = ((self.chat_volume - min_level) / (max_level - min_level)).max(0.0);
167
168 if volume > 1.0 && self.muted_until.is_none() {
169 self.muted_until = now.checked_add(SPAM_MUTE_PERIOD);
170 }
171
172 volume
173 }
174}