mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'zesterer/automod' into 'master'
Automod See merge request veloren/veloren!3513
This commit is contained in:
commit
900454552a
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -553,6 +553,15 @@ dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "censor"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5563d2728feef9a6186acdd148bccbe850dad63c5ba55a3b3355abc9137cb3eb"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
@ -6711,6 +6720,7 @@ dependencies = [
|
||||
"atomicwrites",
|
||||
"authc",
|
||||
"bincode",
|
||||
"censor",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"crossbeam-channel",
|
||||
|
@ -54,12 +54,11 @@ use common::{
|
||||
use common_base::{prof_span, span};
|
||||
use common_net::{
|
||||
msg::{
|
||||
self, validate_chat_msg,
|
||||
self,
|
||||
world_msg::{EconomyInfo, PoiInfo, SiteId, SiteInfo},
|
||||
ChatMsgValidationError, ClientGeneral, ClientMsg, ClientRegister, ClientType,
|
||||
DisconnectReason, InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate,
|
||||
PresenceKind, RegisterError, ServerGeneral, ServerInit, ServerRegisterAnswer,
|
||||
MAX_BYTES_CHAT_MSG,
|
||||
ClientGeneral, ClientMsg, ClientRegister, ClientType, DisconnectReason, InviteAnswer,
|
||||
Notification, PingMsg, PlayerInfo, PlayerListUpdate, PresenceKind, RegisterError,
|
||||
ServerGeneral, ServerInit, ServerRegisterAnswer,
|
||||
},
|
||||
sync::WorldSyncExt,
|
||||
};
|
||||
@ -1551,15 +1550,7 @@ impl Client {
|
||||
pub fn inventories(&self) -> ReadStorage<comp::Inventory> { self.state.read_storage() }
|
||||
|
||||
/// Send a chat message to the server.
|
||||
pub fn send_chat(&mut self, message: String) {
|
||||
match validate_chat_msg(&message) {
|
||||
Ok(()) => self.send_msg(ClientGeneral::ChatMsg(message)),
|
||||
Err(ChatMsgValidationError::TooLong) => warn!(
|
||||
"Attempted to send a message that's too long (Over {} bytes)",
|
||||
MAX_BYTES_CHAT_MSG
|
||||
),
|
||||
}
|
||||
}
|
||||
pub fn send_chat(&mut self, message: String) { self.send_msg(ClientGeneral::ChatMsg(message)); }
|
||||
|
||||
/// Send a command to the server.
|
||||
pub fn send_command(&mut self, name: String, args: Vec<String>) {
|
||||
|
@ -41,18 +41,3 @@ pub enum PingMsg {
|
||||
Ping,
|
||||
Pong,
|
||||
}
|
||||
|
||||
pub const MAX_BYTES_CHAT_MSG: usize = 256;
|
||||
|
||||
pub enum ChatMsgValidationError {
|
||||
TooLong,
|
||||
}
|
||||
|
||||
pub fn validate_chat_msg(msg: &str) -> Result<(), ChatMsgValidationError> {
|
||||
// TODO: Consider using grapheme cluster count instead of size in bytes
|
||||
if msg.len() <= MAX_BYTES_CHAT_MSG {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChatMsgValidationError::TooLong)
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +124,28 @@ impl<G> ChatType<G> {
|
||||
message: msg.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uid(&self) -> Option<Uid> {
|
||||
match self {
|
||||
ChatType::Online(_) => None,
|
||||
ChatType::Offline(_) => None,
|
||||
ChatType::CommandInfo => None,
|
||||
ChatType::CommandError => None,
|
||||
ChatType::FactionMeta(_) => None,
|
||||
ChatType::GroupMeta(_) => None,
|
||||
ChatType::Kill(_, _) => None,
|
||||
ChatType::Tell(u, _t) => Some(*u),
|
||||
ChatType::Say(u) => Some(*u),
|
||||
ChatType::Group(u, _s) => Some(*u),
|
||||
ChatType::Faction(u, _s) => Some(*u),
|
||||
ChatType::Region(u) => Some(*u),
|
||||
ChatType::World(u) => Some(*u),
|
||||
ChatType::Npc(u, _r) => Some(*u),
|
||||
ChatType::NpcSay(u, _r) => Some(*u),
|
||||
ChatType::NpcTell(u, _t, _r) => Some(*u),
|
||||
ChatType::Meta => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stores chat text, type
|
||||
@ -226,27 +248,7 @@ impl<G> GenericChatMsg<G> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uid(&self) -> Option<Uid> {
|
||||
match &self.chat_type {
|
||||
ChatType::Online(_) => None,
|
||||
ChatType::Offline(_) => None,
|
||||
ChatType::CommandInfo => None,
|
||||
ChatType::CommandError => None,
|
||||
ChatType::FactionMeta(_) => None,
|
||||
ChatType::GroupMeta(_) => None,
|
||||
ChatType::Kill(_, _) => None,
|
||||
ChatType::Tell(u, _t) => Some(*u),
|
||||
ChatType::Say(u) => Some(*u),
|
||||
ChatType::Group(u, _s) => Some(*u),
|
||||
ChatType::Faction(u, _s) => Some(*u),
|
||||
ChatType::Region(u) => Some(*u),
|
||||
ChatType::World(u) => Some(*u),
|
||||
ChatType::Npc(u, _r) => Some(*u),
|
||||
ChatType::NpcSay(u, _r) => Some(*u),
|
||||
ChatType::NpcTell(u, _t, _r) => Some(*u),
|
||||
ChatType::Meta => None,
|
||||
}
|
||||
}
|
||||
pub fn uid(&self) -> Option<Uid> { self.chat_type.uid() }
|
||||
}
|
||||
|
||||
/// Player factions are used to coordinate pvp vs hostile factions or segment
|
||||
|
@ -58,6 +58,7 @@ slab = "0.4"
|
||||
rand_distr = "0.4.0"
|
||||
enumset = "1.0.8"
|
||||
noise = { version = "0.7", default-features = false }
|
||||
censor = "0.2"
|
||||
|
||||
rusqlite = { version = "0.24.2", features = ["array", "vtab", "bundled", "trace"] }
|
||||
refinery = { git = "https://gitlab.com/veloren/refinery.git", rev = "8ecf4b4772d791e6c8c0a3f9b66a7530fad1af3e", features = ["rusqlite"] }
|
||||
|
@ -1,139 +0,0 @@
|
||||
use common::character::MAX_NAME_LENGTH;
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AliasValidator {
|
||||
banned_substrings: Vec<String>,
|
||||
}
|
||||
|
||||
impl AliasValidator {
|
||||
pub fn new(banned_substrings: Vec<String>) -> Self {
|
||||
let banned_substrings = banned_substrings
|
||||
.iter()
|
||||
.map(|string| string.to_lowercase())
|
||||
.collect();
|
||||
|
||||
AliasValidator { banned_substrings }
|
||||
}
|
||||
|
||||
pub fn validate(&self, alias: &str) -> Result<(), ValidatorError> {
|
||||
if alias.len() > MAX_NAME_LENGTH {
|
||||
return Err(ValidatorError::TooLong(alias.to_owned(), alias.len()));
|
||||
}
|
||||
|
||||
let lowercase_alias = alias.to_lowercase();
|
||||
|
||||
for banned_word in self.banned_substrings.iter() {
|
||||
if lowercase_alias.contains(banned_word) {
|
||||
return Err(ValidatorError::Forbidden(
|
||||
alias.to_owned(),
|
||||
banned_word.to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ValidatorError {
|
||||
Forbidden(String, String),
|
||||
TooLong(String, usize),
|
||||
}
|
||||
|
||||
impl Display for ValidatorError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Forbidden(name, _) => write!(
|
||||
formatter,
|
||||
"Character name \"{}\" contains a banned word",
|
||||
name
|
||||
),
|
||||
Self::TooLong(name, _) => write!(formatter, "Character name \"{}\" too long", name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn multiple_matches() {
|
||||
let banned_substrings = vec!["bad".to_owned(), "worse".to_owned()];
|
||||
let validator = AliasValidator::new(banned_substrings);
|
||||
|
||||
let bad_alias = "BadplayerMcWorseFace";
|
||||
let result = validator.validate(bad_alias);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ValidatorError::Forbidden(
|
||||
bad_alias.to_owned(),
|
||||
"bad".to_owned()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_lowercase_match() {
|
||||
let banned_substrings = vec!["blue".to_owned()];
|
||||
let validator = AliasValidator::new(banned_substrings);
|
||||
|
||||
let bad_alias = "blueName";
|
||||
let result = validator.validate(bad_alias);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ValidatorError::Forbidden(
|
||||
bad_alias.to_owned(),
|
||||
"blue".to_owned()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_case_insensitive_match() {
|
||||
let banned_substrings = vec!["GrEEn".to_owned()];
|
||||
let validator = AliasValidator::new(banned_substrings);
|
||||
|
||||
let bad_alias = "gReenName";
|
||||
let result = validator.validate(bad_alias);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ValidatorError::Forbidden(
|
||||
bad_alias.to_owned(),
|
||||
"green".to_owned()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mp_matches() {
|
||||
let banned_substrings = vec!["orange".to_owned()];
|
||||
let validator = AliasValidator::new(banned_substrings);
|
||||
|
||||
let good_alias = "ReasonableName";
|
||||
let result = validator.validate(good_alias);
|
||||
|
||||
assert_eq!(result, Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_long() {
|
||||
let banned_substrings = vec!["orange".to_owned()];
|
||||
let validator = AliasValidator::new(banned_substrings);
|
||||
|
||||
let bad_alias = "Thisnameistoolong Muchtoolong MuchTooLongByFar";
|
||||
let result = validator.validate(bad_alias);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ValidatorError::TooLong(
|
||||
bad_alias.to_owned(),
|
||||
bad_alias.chars().count()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
170
server/src/automod.rs
Normal file
170
server/src/automod.rs
Normal file
@ -0,0 +1,170 @@
|
||||
use crate::settings::ModerationSettings;
|
||||
use authc::Uuid;
|
||||
use censor::Censor;
|
||||
use common::comp::AdminRole;
|
||||
use hashbrown::HashMap;
|
||||
use std::{
|
||||
fmt,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
pub const MAX_BYTES_CHAT_MSG: usize = 256;
|
||||
|
||||
pub enum ActionNote {
|
||||
SpamWarn,
|
||||
}
|
||||
|
||||
impl fmt::Display for ActionNote {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ActionNote::SpamWarn => write!(
|
||||
f,
|
||||
"You've sent a lot of messages recently. Make sure to reduce the rate of messages \
|
||||
or you will be automatically muted."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActionErr {
|
||||
BannedWord,
|
||||
TooLong,
|
||||
SpamMuted(Duration),
|
||||
}
|
||||
|
||||
impl fmt::Display for ActionErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ActionErr::BannedWord => write!(
|
||||
f,
|
||||
"Your message contained a banned word. If you think this is a mistake, please let \
|
||||
a moderator know."
|
||||
),
|
||||
ActionErr::TooLong => write!(
|
||||
f,
|
||||
"Your message was too long, no more than {} characters are permitted.",
|
||||
MAX_BYTES_CHAT_MSG
|
||||
),
|
||||
ActionErr::SpamMuted(dur) => write!(
|
||||
f,
|
||||
"You have sent too many messages and are muted for {} seconds.",
|
||||
dur.as_secs_f32() as u64
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AutoMod {
|
||||
settings: ModerationSettings,
|
||||
censor: Arc<Censor>,
|
||||
players: HashMap<Uuid, PlayerState>,
|
||||
}
|
||||
|
||||
impl AutoMod {
|
||||
pub fn new(settings: &ModerationSettings, censor: Arc<Censor>) -> Self {
|
||||
if settings.automod {
|
||||
info!(
|
||||
"Automod enabled, players{} will be subject to automated spam/content filters",
|
||||
if settings.admins_exempt {
|
||||
""
|
||||
} else {
|
||||
" (and admins)"
|
||||
}
|
||||
);
|
||||
} else {
|
||||
info!("Automod disabled");
|
||||
}
|
||||
|
||||
Self {
|
||||
settings: settings.clone(),
|
||||
censor,
|
||||
players: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool { self.settings.automod }
|
||||
|
||||
fn player_mut(&mut self, player: Uuid) -> &mut PlayerState {
|
||||
self.players.entry(player).or_default()
|
||||
}
|
||||
|
||||
pub fn validate_chat_msg(
|
||||
&mut self,
|
||||
player: Uuid,
|
||||
role: Option<AdminRole>,
|
||||
now: Instant,
|
||||
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) {
|
||||
Ok(None)
|
||||
} else if self.censor.check(msg) {
|
||||
Err(ActionErr::BannedWord)
|
||||
} else {
|
||||
let volume = self.player_mut(player).enforce_message_volume(now);
|
||||
|
||||
if let Some(until) = self.player_mut(player).muted_until {
|
||||
Err(ActionErr::SpamMuted(until.saturating_duration_since(now)))
|
||||
} else if volume > 0.75 {
|
||||
Ok(Some(ActionNote::SpamWarn))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The period, in seconds, over which chat volume should be tracked to detect
|
||||
/// spam.
|
||||
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
|
||||
/// 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);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PlayerState {
|
||||
last_msg_time: Option<Instant>,
|
||||
/// The average number of messages per second over the last N seconds.
|
||||
chat_volume: f32,
|
||||
muted_until: Option<Instant>,
|
||||
}
|
||||
|
||||
impl PlayerState {
|
||||
// 0.0 => message is permitted, nothing unusual
|
||||
// >=1.0 => message is not permitted, chat volume exceeded
|
||||
pub fn enforce_message_volume(&mut self, now: Instant) -> f32 {
|
||||
if self.muted_until.map_or(false, |u| u <= now) {
|
||||
self.muted_until = None;
|
||||
}
|
||||
|
||||
if let Some(time_since_last) = self
|
||||
.last_msg_time
|
||||
.map(|last| now.saturating_duration_since(last).as_secs_f32())
|
||||
{
|
||||
let time_proportion = (time_since_last / CHAT_VOLUME_PERIOD).min(1.0);
|
||||
self.chat_volume = self.chat_volume * (1.0 - time_proportion)
|
||||
+ (1.0 / time_since_last) * time_proportion;
|
||||
} else {
|
||||
self.chat_volume = 0.0;
|
||||
}
|
||||
self.last_msg_time = Some(now);
|
||||
|
||||
let min_level = 1.0 / CHAT_VOLUME_PERIOD;
|
||||
let max_level = MAX_AVG_MSG_PER_SECOND;
|
||||
|
||||
let volume = ((self.chat_volume - min_level) / (max_level - min_level)).max(0.0);
|
||||
|
||||
if volume > 1.0 && self.muted_until.is_none() {
|
||||
self.muted_until = now.checked_add(SPAM_MUTE_PERIOD);
|
||||
}
|
||||
|
||||
volume
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
)]
|
||||
#![cfg_attr(not(feature = "worldgen"), feature(const_panic))]
|
||||
|
||||
pub mod alias_validator;
|
||||
pub mod automod;
|
||||
mod character_creator;
|
||||
pub mod chunk_generator;
|
||||
mod chunk_serialize;
|
||||
@ -54,7 +54,7 @@ pub use crate::{
|
||||
#[cfg(feature = "persistent_world")]
|
||||
use crate::terrain_persistence::TerrainPersistence;
|
||||
use crate::{
|
||||
alias_validator::AliasValidator,
|
||||
automod::AutoMod,
|
||||
chunk_generator::ChunkGenerator,
|
||||
client::Client,
|
||||
cmd::ChatCommandExt,
|
||||
@ -68,6 +68,7 @@ use crate::{
|
||||
state_ext::StateExt,
|
||||
sys::sentinel::{DeletedEntities, TrackedStorages},
|
||||
};
|
||||
use censor::Censor;
|
||||
#[cfg(not(feature = "worldgen"))]
|
||||
use common::grid::Grid;
|
||||
use common::{
|
||||
@ -338,36 +339,15 @@ impl Server {
|
||||
state.ecs_mut().register::<login_provider::PendingLogin>();
|
||||
state.ecs_mut().register::<RepositionOnChunkLoad>();
|
||||
|
||||
//Alias validator
|
||||
let banned_words_paths = &settings.banned_words_files;
|
||||
let mut banned_words = Vec::new();
|
||||
for path in banned_words_paths {
|
||||
let mut list = match std::fs::File::open(&path) {
|
||||
Ok(file) => match ron::de::from_reader(&file) {
|
||||
Ok(vec) => vec,
|
||||
Err(error) => {
|
||||
warn!(?error, ?file, "Couldn't deserialize banned words file");
|
||||
return Err(Error::Other(format!(
|
||||
"Couldn't read banned words file \"{}\"",
|
||||
path.to_string_lossy()
|
||||
)));
|
||||
},
|
||||
},
|
||||
Err(error) => {
|
||||
warn!(?error, ?path, "Couldn't open banned words file");
|
||||
return Err(Error::Other(format!(
|
||||
"Couldn't open banned words file \"{}\". Error: {}",
|
||||
path.to_string_lossy(),
|
||||
error
|
||||
)));
|
||||
},
|
||||
};
|
||||
banned_words.append(&mut list);
|
||||
}
|
||||
let banned_words_count = banned_words.len();
|
||||
debug!(?banned_words_count);
|
||||
trace!(?banned_words);
|
||||
state.ecs_mut().insert(AliasValidator::new(banned_words));
|
||||
// Load banned words list
|
||||
let banned_words = settings.moderation.load_banned_words(data_dir);
|
||||
let censor = Arc::new(Censor::Custom(banned_words.into_iter().collect()));
|
||||
state.ecs_mut().insert(Arc::clone(&censor));
|
||||
|
||||
// Init automod
|
||||
state
|
||||
.ecs_mut()
|
||||
.insert(AutoMod::new(&settings.moderation, censor));
|
||||
|
||||
#[cfg(feature = "worldgen")]
|
||||
let (world, index) = World::generate(
|
||||
|
@ -95,6 +95,44 @@ impl Default for GameplaySettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ModerationSettings {
|
||||
#[serde(default)]
|
||||
pub banned_words_files: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub automod: bool,
|
||||
#[serde(default)]
|
||||
pub admins_exempt: bool,
|
||||
}
|
||||
|
||||
impl ModerationSettings {
|
||||
pub fn load_banned_words(&self, data_dir: &Path) -> Vec<String> {
|
||||
let mut banned_words = Vec::new();
|
||||
for fname in self.banned_words_files.iter() {
|
||||
let mut path = with_config_dir(data_dir);
|
||||
path.push(fname);
|
||||
match std::fs::File::open(&path) {
|
||||
Ok(file) => match ron::de::from_reader(&file) {
|
||||
Ok(mut words) => banned_words.append(&mut words),
|
||||
Err(error) => error!(?error, ?file, "Couldn't read banned words file"),
|
||||
},
|
||||
Err(error) => error!(?error, ?path, "Couldn't open banned words file"),
|
||||
}
|
||||
}
|
||||
banned_words
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ModerationSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
banned_words_files: Vec::new(),
|
||||
automod: false,
|
||||
admins_exempt: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum CalendarMode {
|
||||
None,
|
||||
@ -132,7 +170,6 @@ pub struct Settings {
|
||||
/// uses the value of the file options to decide how to proceed.
|
||||
pub map_file: Option<FileOpts>,
|
||||
pub max_view_distance: Option<u32>,
|
||||
pub banned_words_files: Vec<PathBuf>,
|
||||
pub max_player_group_size: u32,
|
||||
pub client_timeout: Duration,
|
||||
pub spawn_town: Option<String>,
|
||||
@ -146,6 +183,8 @@ pub struct Settings {
|
||||
|
||||
#[serde(default)]
|
||||
pub gameplay: GameplaySettings,
|
||||
#[serde(default)]
|
||||
pub moderation: ModerationSettings,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@ -167,7 +206,6 @@ impl Default for Settings {
|
||||
start_time: 9.0 * 3600.0,
|
||||
map_file: None,
|
||||
max_view_distance: Some(65),
|
||||
banned_words_files: Vec::new(),
|
||||
max_player_group_size: 6,
|
||||
calendar_mode: CalendarMode::Auto,
|
||||
client_timeout: Duration::from_secs(40),
|
||||
@ -175,6 +213,7 @@ impl Default for Settings {
|
||||
max_player_for_kill_broadcast: None,
|
||||
experimental_terrain_persistence: false,
|
||||
gameplay: GameplaySettings::default(),
|
||||
moderation: ModerationSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -265,7 +304,7 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_config_dir(path: &Path) -> PathBuf {
|
||||
pub fn with_config_dir(path: &Path) -> PathBuf {
|
||||
let mut path = PathBuf::from(path);
|
||||
path.push(CONFIG_DIR);
|
||||
path
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
automod::AutoMod,
|
||||
client::Client,
|
||||
events::update_map_markers,
|
||||
persistence::PersistedComponents,
|
||||
@ -17,7 +18,7 @@ use common::{
|
||||
self,
|
||||
item::MaterialStatManifest,
|
||||
skills::{GeneralSkill, Skill},
|
||||
Group, Inventory, Item, Poise,
|
||||
ChatType, Group, Inventory, Item, Player, Poise,
|
||||
},
|
||||
effect::Effect,
|
||||
link::{Link, LinkHandle},
|
||||
@ -36,7 +37,7 @@ use specs::{
|
||||
saveload::MarkerAllocator, Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder,
|
||||
Join, WorldExt,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{trace, warn};
|
||||
use vek::*;
|
||||
|
||||
@ -113,6 +114,7 @@ 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 send_chat(&self, msg: comp::UnresolvedChatMsg);
|
||||
fn notify_players(&self, msg: ServerGeneral);
|
||||
fn notify_in_game_clients(&self, msg: ServerGeneral);
|
||||
@ -674,6 +676,39 @@ impl StateExt for State {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_chat_msg(&self, entity: EcsEntity, 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 };
|
||||
|
||||
match automod.validate_chat_msg(
|
||||
player.uuid(),
|
||||
self.ecs()
|
||||
.read_storage::<comp::Admin>()
|
||||
.get(entity)
|
||||
.map(|a| a.0),
|
||||
Instant::now(),
|
||||
msg,
|
||||
) {
|
||||
Ok(note) => {
|
||||
if let Some(note) = note {
|
||||
let _ = client.send(ServerGeneral::server_msg(
|
||||
ChatType::CommandInfo,
|
||||
format!("{}", note),
|
||||
));
|
||||
}
|
||||
true
|
||||
},
|
||||
Err(err) => {
|
||||
let _ = client.send(ServerGeneral::server_msg(
|
||||
ChatType::CommandError,
|
||||
format!("{}", err),
|
||||
));
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the chat message to the proper players. Say and region are limited
|
||||
/// by location. Faction and group are limited by component.
|
||||
fn send_chat(&self, msg: comp::UnresolvedChatMsg) {
|
||||
@ -689,165 +724,183 @@ impl StateExt for State {
|
||||
.clone()
|
||||
.map_group(|_| group_info.map_or_else(|| "???".to_string(), |i| i.name.clone()));
|
||||
|
||||
match &msg.chat_type {
|
||||
comp::ChatType::Offline(_)
|
||||
| comp::ChatType::CommandInfo
|
||||
| comp::ChatType::CommandError
|
||||
| comp::ChatType::Meta
|
||||
| comp::ChatType::World(_) => self.notify_players(ServerGeneral::ChatMsg(resolved_msg)),
|
||||
comp::ChatType::Online(u) => {
|
||||
for (client, uid) in
|
||||
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join()
|
||||
{
|
||||
if uid != u {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
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))
|
||||
}) {
|
||||
match &msg.chat_type {
|
||||
comp::ChatType::Offline(_)
|
||||
| comp::ChatType::CommandInfo
|
||||
| comp::ChatType::CommandError
|
||||
| comp::ChatType::Meta
|
||||
| comp::ChatType::World(_) => {
|
||||
self.notify_players(ServerGeneral::ChatMsg(resolved_msg))
|
||||
},
|
||||
comp::ChatType::Online(u) => {
|
||||
for (client, uid) in
|
||||
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join()
|
||||
{
|
||||
if uid != u {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::Tell(u, t) => {
|
||||
for (client, uid) in
|
||||
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join()
|
||||
{
|
||||
if uid == u || uid == t {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::Kill(kill_source, uid) => {
|
||||
let comp::chat::GenericChatMsg { message, .. } = msg;
|
||||
let clients = ecs.read_storage::<Client>();
|
||||
let clients_count = clients.count();
|
||||
// Avoid chat spam, send kill message only to group or nearby players if a
|
||||
// certain amount of clients are online
|
||||
if clients_count
|
||||
> ecs
|
||||
.fetch::<Settings>()
|
||||
.max_player_for_kill_broadcast
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Send kill message to the dead player's group
|
||||
let killed_entity =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
let groups = ecs.read_storage::<Group>();
|
||||
let killed_group = killed_entity.and_then(|e| groups.get(e));
|
||||
if let Some(g) = &killed_group {
|
||||
send_to_group(g, ecs, &resolved_msg);
|
||||
},
|
||||
comp::ChatType::Tell(from, to) => {
|
||||
for (client, uid) in
|
||||
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join()
|
||||
{
|
||||
if uid == from || uid == to {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::Kill(kill_source, uid) => {
|
||||
let comp::chat::GenericChatMsg { message, .. } = msg;
|
||||
let clients = ecs.read_storage::<Client>();
|
||||
let clients_count = clients.count();
|
||||
// Avoid chat spam, send kill message only to group or nearby players if a
|
||||
// certain amount of clients are online
|
||||
if clients_count
|
||||
> ecs
|
||||
.fetch::<Settings>()
|
||||
.max_player_for_kill_broadcast
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Send kill message to the dead player's group
|
||||
let killed_entity =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
let groups = ecs.read_storage::<Group>();
|
||||
let killed_group = killed_entity.and_then(|e| groups.get(e));
|
||||
if let Some(g) = &killed_group {
|
||||
send_to_group(g, ecs, &resolved_msg);
|
||||
}
|
||||
|
||||
// Send kill message to nearby players that aren't part of the deceased's group
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(died_player_pos) = killed_entity.and_then(|e| positions.get(e)) {
|
||||
for (ent, client, pos) in (&*ecs.entities(), &clients, &positions).join() {
|
||||
let client_group = groups.get(ent);
|
||||
let is_different_group =
|
||||
!(killed_group == client_group && client_group.is_some());
|
||||
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, died_player_pos)
|
||||
&& is_different_group
|
||||
// Send kill message to nearby players that aren't part of the deceased's
|
||||
// group
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(died_player_pos) = killed_entity.and_then(|e| positions.get(e))
|
||||
{
|
||||
for (ent, client, pos) in
|
||||
(&*ecs.entities(), &clients, &positions).join()
|
||||
{
|
||||
let client_group = groups.get(ent);
|
||||
let is_different_group =
|
||||
!(killed_group == client_group && client_group.is_some());
|
||||
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, died_player_pos)
|
||||
&& is_different_group
|
||||
{
|
||||
client.send_fallible(ServerGeneral::ChatMsg(
|
||||
resolved_msg.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.notify_players(ServerGeneral::server_msg(
|
||||
comp::ChatType::Kill(kill_source.clone(), *uid),
|
||||
message,
|
||||
))
|
||||
}
|
||||
},
|
||||
comp::ChatType::Say(uid) => {
|
||||
let entity_opt =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||
for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
|
||||
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.notify_players(ServerGeneral::server_msg(
|
||||
comp::ChatType::Kill(kill_source.clone(), *uid),
|
||||
message,
|
||||
))
|
||||
}
|
||||
},
|
||||
comp::ChatType::Say(uid) => {
|
||||
let entity_opt =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||
for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
|
||||
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::Region(uid) => {
|
||||
let entity_opt =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||
for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
|
||||
if is_within(comp::ChatMsg::REGION_DISTANCE, pos, speaker_pos) {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::Npc(uid, _r) => {
|
||||
let entity_opt =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||
for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
|
||||
if is_within(comp::ChatMsg::NPC_DISTANCE, pos, speaker_pos) {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::NpcSay(uid, _r) => {
|
||||
let entity_opt =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||
for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
|
||||
if is_within(comp::ChatMsg::NPC_SAY_DISTANCE, pos, speaker_pos) {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::NpcTell(from, to, _r) => {
|
||||
for (client, uid) in
|
||||
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join()
|
||||
{
|
||||
if uid == from || uid == to {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::FactionMeta(s) | comp::ChatType::Faction(_, s) => {
|
||||
for (client, faction) in (
|
||||
&ecs.read_storage::<Client>(),
|
||||
&ecs.read_storage::<comp::Faction>(),
|
||||
)
|
||||
.join()
|
||||
{
|
||||
if s == &faction.0 {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::Group(from, g) => {
|
||||
if group_info.is_none() {
|
||||
// group not found, reply with command error
|
||||
let reply = comp::ChatMsg {
|
||||
chat_type: comp::ChatType::CommandError,
|
||||
message: "You are using group chat but do not belong to a group. Use \
|
||||
/world or /region to change chat."
|
||||
.into(),
|
||||
};
|
||||
},
|
||||
comp::ChatType::Region(uid) => {
|
||||
let entity_opt =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
|
||||
if let Some((client, _)) =
|
||||
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>())
|
||||
.join()
|
||||
.find(|(_, uid)| *uid == from)
|
||||
{
|
||||
client.send_fallible(ServerGeneral::ChatMsg(reply));
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||
for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
|
||||
if is_within(comp::ChatMsg::REGION_DISTANCE, pos, speaker_pos) {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
send_to_group(g, ecs, &resolved_msg);
|
||||
},
|
||||
comp::ChatType::GroupMeta(g) => {
|
||||
send_to_group(g, ecs, &resolved_msg);
|
||||
},
|
||||
},
|
||||
comp::ChatType::Npc(uid, _r) => {
|
||||
let entity_opt =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||
for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
|
||||
if is_within(comp::ChatMsg::NPC_DISTANCE, pos, speaker_pos) {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::NpcSay(uid, _r) => {
|
||||
let entity_opt =
|
||||
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
|
||||
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||
for (client, pos) in (&ecs.read_storage::<Client>(), &positions).join() {
|
||||
if is_within(comp::ChatMsg::NPC_SAY_DISTANCE, pos, speaker_pos) {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::NpcTell(from, to, _r) => {
|
||||
for (client, uid) in
|
||||
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>()).join()
|
||||
{
|
||||
if uid == from || uid == to {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::FactionMeta(s) | comp::ChatType::Faction(_, s) => {
|
||||
for (client, faction) in (
|
||||
&ecs.read_storage::<Client>(),
|
||||
&ecs.read_storage::<comp::Faction>(),
|
||||
)
|
||||
.join()
|
||||
{
|
||||
if s == &faction.0 {
|
||||
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
|
||||
}
|
||||
}
|
||||
},
|
||||
comp::ChatType::Group(from, g) => {
|
||||
if group_info.is_none() {
|
||||
// group not found, reply with command error
|
||||
let reply = comp::ChatMsg {
|
||||
chat_type: comp::ChatType::CommandError,
|
||||
message: "You are using group chat but do not belong to a group. Use \
|
||||
/world or /region to change chat."
|
||||
.into(),
|
||||
};
|
||||
|
||||
if let Some((client, _)) =
|
||||
(&ecs.read_storage::<Client>(), &ecs.read_storage::<Uid>())
|
||||
.join()
|
||||
.find(|(_, uid)| *uid == from)
|
||||
{
|
||||
client.send_fallible(ServerGeneral::ChatMsg(reply));
|
||||
}
|
||||
return;
|
||||
}
|
||||
send_to_group(g, ecs, &resolved_msg);
|
||||
},
|
||||
comp::ChatType::GroupMeta(g) => {
|
||||
send_to_group(g, ecs, &resolved_msg);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
alias_validator::AliasValidator,
|
||||
automod::AutoMod,
|
||||
character_creator,
|
||||
client::Client,
|
||||
persistence::{character_loader::CharacterLoader, character_updater::CharacterUpdater},
|
||||
@ -14,7 +14,7 @@ use common::{
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use common_net::msg::{ClientGeneral, ServerGeneral};
|
||||
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, WriteExpect};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{atomic::Ordering, Arc};
|
||||
use tracing::debug;
|
||||
|
||||
impl Sys {
|
||||
@ -29,7 +29,8 @@ impl Sys {
|
||||
admins: &ReadStorage<'_, Admin>,
|
||||
presences: &ReadStorage<'_, Presence>,
|
||||
editable_settings: &ReadExpect<'_, EditableSettings>,
|
||||
alias_validator: &ReadExpect<'_, AliasValidator>,
|
||||
censor: &ReadExpect<'_, Arc<censor::Censor>>,
|
||||
automod: &AutoMod,
|
||||
msg: ClientGeneral,
|
||||
) -> Result<(), crate::error::Error> {
|
||||
let mut send_join_messages = || -> Result<(), crate::error::Error> {
|
||||
@ -41,6 +42,14 @@ impl Sys {
|
||||
))?;
|
||||
}
|
||||
|
||||
// Warn them about automod
|
||||
if automod.enabled() {
|
||||
client.send(ServerGeneral::server_msg(
|
||||
ChatType::CommandInfo,
|
||||
"Automatic moderation is enabled: play nice and have fun!",
|
||||
))?;
|
||||
}
|
||||
|
||||
if !client.login_msg_sent.load(Ordering::Relaxed) {
|
||||
if let Some(player_uid) = uids.get(entity) {
|
||||
server_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg {
|
||||
@ -128,9 +137,12 @@ impl Sys {
|
||||
offhand,
|
||||
body,
|
||||
} => {
|
||||
if let Err(error) = alias_validator.validate(&alias) {
|
||||
debug!(?error, ?alias, "denied alias as it contained a banned word");
|
||||
client.send(ServerGeneral::CharacterActionError(error.to_string()))?;
|
||||
if censor.check(&alias) {
|
||||
debug!(?alias, "denied alias as it contained a banned word");
|
||||
client.send(ServerGeneral::CharacterActionError(format!(
|
||||
"Alias '{}' contains a banned word",
|
||||
alias
|
||||
)))?;
|
||||
} else if let Some(player) = players.get(entity) {
|
||||
if let Err(error) = character_creator::create_character(
|
||||
entity,
|
||||
@ -153,9 +165,12 @@ impl Sys {
|
||||
}
|
||||
},
|
||||
ClientGeneral::EditCharacter { id, alias, body } => {
|
||||
if let Err(error) = alias_validator.validate(&alias) {
|
||||
debug!(?error, ?alias, "denied alias as it contained a banned word");
|
||||
client.send(ServerGeneral::CharacterActionError(error.to_string()))?;
|
||||
if censor.check(&alias) {
|
||||
debug!(?alias, "denied alias as it contained a banned word");
|
||||
client.send(ServerGeneral::CharacterActionError(format!(
|
||||
"Alias '{}' contains a banned word",
|
||||
alias
|
||||
)))?;
|
||||
} else if let Some(player) = players.get(entity) {
|
||||
if let Err(error) = character_creator::edit_character(
|
||||
entity,
|
||||
@ -210,7 +225,8 @@ impl<'a> System<'a> for Sys {
|
||||
ReadStorage<'a, Admin>,
|
||||
ReadStorage<'a, Presence>,
|
||||
ReadExpect<'a, EditableSettings>,
|
||||
ReadExpect<'a, AliasValidator>,
|
||||
ReadExpect<'a, Arc<censor::Censor>>,
|
||||
ReadExpect<'a, AutoMod>,
|
||||
);
|
||||
|
||||
const NAME: &'static str = "msg::character_screen";
|
||||
@ -230,7 +246,8 @@ impl<'a> System<'a> for Sys {
|
||||
admins,
|
||||
presences,
|
||||
editable_settings,
|
||||
alias_validator,
|
||||
censor,
|
||||
automod,
|
||||
): Self::SystemData,
|
||||
) {
|
||||
let mut server_emitter = server_event_bus.emitter();
|
||||
@ -248,7 +265,8 @@ impl<'a> System<'a> for Sys {
|
||||
&admins,
|
||||
&presences,
|
||||
&editable_settings,
|
||||
&alias_validator,
|
||||
&censor,
|
||||
&automod,
|
||||
msg,
|
||||
)
|
||||
});
|
||||
|
@ -6,9 +6,7 @@ use common::{
|
||||
uid::Uid,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use common_net::msg::{
|
||||
validate_chat_msg, ChatMsgValidationError, ClientGeneral, MAX_BYTES_CHAT_MSG,
|
||||
};
|
||||
use common_net::msg::ClientGeneral;
|
||||
use specs::{Entities, Join, Read, ReadStorage};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
@ -25,24 +23,16 @@ impl Sys {
|
||||
match msg {
|
||||
ClientGeneral::ChatMsg(message) => {
|
||||
if player.is_some() {
|
||||
match validate_chat_msg(&message) {
|
||||
Ok(()) => {
|
||||
if let Some(from) = uids.get(entity) {
|
||||
const CHAT_MODE_DEFAULT: &ChatMode = &ChatMode::default();
|
||||
let mode = chat_modes.get(entity).unwrap_or(CHAT_MODE_DEFAULT);
|
||||
// Send chat message
|
||||
server_emitter
|
||||
.emit(ServerEvent::Chat(mode.new_message(*from, message)));
|
||||
} else {
|
||||
error!("Could not send message. Missing player uid");
|
||||
}
|
||||
},
|
||||
Err(ChatMsgValidationError::TooLong) => {
|
||||
let max = MAX_BYTES_CHAT_MSG;
|
||||
let len = message.len();
|
||||
warn!(?len, ?max, "Received a chat message that's too long")
|
||||
},
|
||||
if let Some(from) = uids.get(entity) {
|
||||
const CHAT_MODE_DEFAULT: &ChatMode = &ChatMode::default();
|
||||
let mode = chat_modes.get(entity).unwrap_or(CHAT_MODE_DEFAULT);
|
||||
// Send chat message
|
||||
server_emitter.emit(ServerEvent::Chat(mode.new_message(*from, message)));
|
||||
} else {
|
||||
error!("Could not send message. Missing player uid");
|
||||
}
|
||||
} else {
|
||||
warn!("Received a chat message from an unregistered client");
|
||||
}
|
||||
},
|
||||
ClientGeneral::Command(name, args) => {
|
||||
@ -93,7 +83,7 @@ impl<'a> System<'a> for Sys {
|
||||
) {
|
||||
let mut server_emitter = server_event_bus.emitter();
|
||||
|
||||
for (entity, client, player) in (&entities, &clients, (&players).maybe()).join() {
|
||||
for (entity, client, player) in (&entities, &clients, players.maybe()).join() {
|
||||
let res = super::try_recv_all(client, 3, |client, msg| {
|
||||
Self::handle_general_msg(
|
||||
&mut server_emitter,
|
||||
|
@ -9,7 +9,6 @@ use common::comp::{
|
||||
group::Role,
|
||||
BuffKind, ChatMode, ChatMsg, ChatType,
|
||||
};
|
||||
use common_net::msg::validate_chat_msg;
|
||||
use conrod_core::{
|
||||
color,
|
||||
input::Key,
|
||||
@ -120,9 +119,7 @@ impl<'a> Chat<'a> {
|
||||
}
|
||||
|
||||
pub fn input(mut self, input: String) -> Self {
|
||||
if let Ok(()) = validate_chat_msg(&input) {
|
||||
self.force_input = Some(input);
|
||||
}
|
||||
self.force_input = Some(input);
|
||||
self
|
||||
}
|
||||
|
||||
@ -388,9 +385,7 @@ impl<'a> Widget for Chat<'a> {
|
||||
.set(state.ids.chat_input, ui)
|
||||
{
|
||||
input.retain(|c| c != '\n');
|
||||
if let Ok(()) = validate_chat_msg(&input) {
|
||||
state.update(|s| s.input.message = input);
|
||||
}
|
||||
state.update(|s| s.input.message = input);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user