diff --git a/data/sql/updates/pending_db_auth/rev_1780327604603266200.sql b/data/sql/updates/pending_db_auth/rev_1780327604603266200.sql new file mode 100644 index 0000000000..5bd98f5661 --- /dev/null +++ b/data/sql/updates/pending_db_auth/rev_1780327604603266200.sql @@ -0,0 +1,12 @@ +-- +DELETE FROM `rbac_permissions` WHERE `id` IN (923, 924, 925); +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES +(923, 'Command: chatfilter list'), +(924, 'Command: chatfilter add'), +(925, 'Command: chatfilter remove'); + +DELETE FROM `rbac_linked_permissions` WHERE `id` = 197 AND `linkedId` IN (923, 924, 925); +INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES +(197, 923), +(197, 924), +(197, 925); diff --git a/data/sql/updates/pending_db_characters/rev_1780325063053000000.sql b/data/sql/updates/pending_db_characters/rev_1780325063053000000.sql new file mode 100644 index 0000000000..f20182ba62 --- /dev/null +++ b/data/sql/updates/pending_db_characters/rev_1780325063053000000.sql @@ -0,0 +1,12 @@ +-- Add `chat_filter` table for chat content filtering. +DROP TABLE IF EXISTS `chat_filter`; +CREATE TABLE `chat_filter` ( + `ID` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `Word` VARCHAR(255) NOT NULL, + PRIMARY KEY (`ID`) USING BTREE +) +CHARSET = utf8mb4 +COLLATE = utf8mb4_unicode_ci +ENGINE = InnoDB +ROW_FORMAT = DEFAULT +; diff --git a/data/sql/updates/pending_db_world/rev_1780326174734000000.sql b/data/sql/updates/pending_db_world/rev_1780326174734000000.sql new file mode 100644 index 0000000000..5a9e7b7d58 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1780326174734000000.sql @@ -0,0 +1,14 @@ +-- Add chatfilter and reload chat_filter commands. +DELETE FROM `command` WHERE `name` IN ('chatfilter', 'chatfilter list', 'chatfilter add', 'chatfilter remove', 'reload chat_filter'); +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('chatfilter', 2, 'Syntax: .chatfilter $subcommand\nType .chatfilter to see a list of subcommands or .help chatfilter $subcommand to see info on subcommands.'), +('chatfilter list', 2, 'Syntax: .chatfilter list\nList all entries from the `chat_filter` table.'), +('chatfilter add', 3, 'Syntax: .chatfilter add $word\nAdd $word to the `chat_filter` table and reload the chat filter.'), +('chatfilter remove', 3, 'Syntax: .chatfilter remove $word\nRemove $word from the `chat_filter` table and reload the chat filter.'), +('reload chat_filter', 3, 'Syntax: .reload chat_filter\n\nReload the `chat_filter` table.'); + +DELETE FROM `acore_string` WHERE `entry` IN (5144, 5145, 5146); +INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES +(5144, '|cffff0000Unable to send emote because your message contained reserved words.|r', '|cffff0000감정표현에 예약된 단어가 포함되어 있어 전송할 수 없습니다.|r', '|cffff0000Impossible d''envoyer l''emote car votre message contient des mots réservés.|r', '|cffff0000Emote konnte nicht gesendet werden, da Ihre Nachricht reservierte Wörter enthielt.|r', '|cffff0000无法发送表情,因为您的消息包含保留字。|r', '|cffff0000無法傳送表情,因為您的訊息包含保留字。|r', '|cffff0000No se puede enviar el gesto porque tu mensaje contenía palabras reservadas.|r', '|cffff0000No se puede enviar el gesto porque tu mensaje contenía palabras reservadas.|r', '|cffff0000Невозможно отправить эмоцию, так как ваше сообщение содержит зарезервированные слова.|r'), +(5145, '|cffff0000Unable to send message because your message contained reserved words.|r', '|cffff0000메시지에 예약된 단어가 포함되어 있어 전송할 수 없습니다.|r', '|cffff0000Impossible d''envoyer le message car votre message contient des mots réservés.|r', '|cffff0000Nachricht konnte nicht gesendet werden, da Ihre Nachricht reservierte Wörter enthielt.|r', '|cffff0000无法发送消息,因为您的消息包含保留字。|r', '|cffff0000無法傳送訊息,因為您的訊息包含保留字。|r', '|cffff0000No se puede enviar el mensaje porque tu mensaje contenía palabras reservadas.|r', '|cffff0000No se puede enviar el mensaje porque tu mensaje contenía palabras reservadas.|r', '|cffff0000Невозможно отправить сообщение, так как ваше сообщение содержит зарезервированные слова.|r'), +(5146, '|cffff0000Unable to send yell because your message contained reserved words.|r', '|cffff0000외치기에 예약된 단어가 포함되어 있어 전송할 수 없습니다.|r', '|cffff0000Impossible d''envoyer le cri car votre message contient des mots réservés.|r', '|cffff0000Ruf konnte nicht gesendet werden, da Ihre Nachricht reservierte Wörter enthielt.|r', '|cffff0000无法发送大喊,因为您的消息包含保留字。|r', '|cffff0000無法傳送大喊,因為您的訊息包含保留字。|r', '|cffff0000No se puede enviar el grito porque tu mensaje contenía palabras reservadas.|r', '|cffff0000No se puede enviar el grito porque tu mensaje contenía palabras reservadas.|r', '|cffff0000Невозможно отправить крик, так как ваше сообщение содержит зарезервированные слова.|r'); diff --git a/src/common/Utilities/AhoCorasick.h b/src/common/Utilities/AhoCorasick.h new file mode 100644 index 0000000000..eb6586606a --- /dev/null +++ b/src/common/Utilities/AhoCorasick.h @@ -0,0 +1,157 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef AhoCorasick_h__ +#define AhoCorasick_h__ + +#include "Define.h" +#include +#include +#include +#include +#include + +namespace Acore +{ + /** + * @class AhoCorasick + * + * @brief Multi-pattern substring matcher. + * + * Insert all patterns, call Build() once, then query in O(text length) + * regardless of pattern count. Insertion after Build() is not supported + * (it would invalidate failure links — call Build() again if you must). + * + * Matching is exact on the @p CharT alphabet — pre-lowercase patterns and + * inputs in the caller if you need case-insensitive matching. + * + * Usage: + * Acore::AhoCorasick a; + * a.Insert(L"foo"); + * a.Insert(L"bar"); + * a.Build(); + * bool hit = a.ContainsAny(L"the bar is open"); // -> true + */ + template + class AhoCorasick + { + public: + using StringType = std::basic_string; + using StringViewType = std::basic_string_view; + + AhoCorasick() { _nodes.emplace_back(); } + + void Insert(StringViewType pattern) + { + if (pattern.empty()) + return; + + uint32 cur = 0; + for (CharT c : pattern) + { + auto it = _nodes[cur].next.find(c); + if (it != _nodes[cur].next.end()) + { + cur = it->second; + continue; + } + + uint32 idx = static_cast(_nodes.size()); + _nodes.emplace_back(); + _nodes[cur].next.emplace(c, idx); + cur = idx; + } + _nodes[cur].output = true; + } + + void Build() + { + std::queue bfs; + for (auto const& kv : _nodes[0].next) + { + _nodes[kv.second].fail = 0; + bfs.push(kv.second); + } + + while (!bfs.empty()) + { + uint32 u = bfs.front(); + bfs.pop(); + + for (auto const& kv : _nodes[u].next) + { + CharT c = kv.first; + uint32 v = kv.second; + + uint32 f = _nodes[u].fail; + while (f != 0 && _nodes[f].next.find(c) == _nodes[f].next.end()) + f = _nodes[f].fail; + + auto it = _nodes[f].next.find(c); + _nodes[v].fail = (it != _nodes[f].next.end() && it->second != v) ? it->second : 0; + + if (_nodes[_nodes[v].fail].output) + _nodes[v].output = true; + + bfs.push(v); + } + } + } + + [[nodiscard]] bool ContainsAny(StringViewType text) const + { + if (_nodes.size() <= 1) + return false; + + uint32 cur = 0; + for (CharT c : text) + { + while (cur != 0 && _nodes[cur].next.find(c) == _nodes[cur].next.end()) + cur = _nodes[cur].fail; + + auto it = _nodes[cur].next.find(c); + if (it != _nodes[cur].next.end()) + cur = it->second; + + if (_nodes[cur].output) + return true; + } + return false; + } + + [[nodiscard]] bool Empty() const { return _nodes.size() <= 1; } + + void Clear() + { + _nodes.clear(); + _nodes.emplace_back(); + } + + private: + struct Node + { + std::unordered_map next; + uint32 fail = 0; + bool output = false; + }; + + std::vector _nodes; + }; +} +//! namespace Acore + +#endif // AhoCorasick_h__ diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 42051b97cf..aa9c0eda38 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -4381,6 +4381,21 @@ PreserveCustomChannels = 0 PreserveCustomChannelDuration = 14 +# +# ChatFilter +# Description: Blocks chat messages when they contain any entry from the `chat_filter` +# table (substring, case-insensitive). +# Manage entries with .chatfilter add / remove / list and .reload chat_filter. +# Blizzlike is only whispers are enabled. +# Default: 0 - (Disbaled) +# 1 - (Enabled) +# + +ChatFilter.Whisper = 1 +ChatFilter.Say = 0 +ChatFilter.Yell = 0 +ChatFilter.Emote = 0 + # ################################################################################################### diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index de3cdeb37c..ca1bfa5e3e 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -609,6 +609,12 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_INS_RESERVED_PLAYER_NAME, "INSERT IGNORE INTO reserved_name (name) VALUES (?)", CONNECTION_ASYNC); PrepareStatement(CHAR_INS_PROFANITY_PLAYER_NAME, "INSERT IGNORE INTO profanity_name (name) VALUES (?)", CONNECTION_ASYNC); + // Chat filter + PrepareStatement(CHAR_SEL_CHAT_FILTER, "SELECT ID, Word FROM chat_filter ORDER BY ID", CONNECTION_SYNCH); + PrepareStatement(CHAR_SEL_CHAT_FILTER_WORD, "SELECT ID FROM chat_filter WHERE Word = ?", CONNECTION_SYNCH); + PrepareStatement(CHAR_INS_CHAT_FILTER_WORD, "INSERT INTO chat_filter (Word) VALUES (?)", CONNECTION_SYNCH); + PrepareStatement(CHAR_DEL_CHAT_FILTER_WORD, "DELETE FROM chat_filter WHERE Word = ?", CONNECTION_SYNCH); + // Character settings PrepareStatement(CHAR_SEL_CHAR_SETTINGS, "SELECT source, data FROM character_settings WHERE guid = ?", CONNECTION_BOTH); PrepareStatement(CHAR_REP_CHAR_SETTINGS, "REPLACE INTO character_settings (guid, source, data) VALUES (?, ?, ?)", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h index 313a87dd5c..666be52551 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.h +++ b/src/server/database/Database/Implementation/CharacterDatabase.h @@ -523,6 +523,11 @@ enum CharacterDatabaseStatements : uint32 CHAR_INS_RESERVED_PLAYER_NAME, CHAR_INS_PROFANITY_PLAYER_NAME, + CHAR_SEL_CHAT_FILTER, + CHAR_SEL_CHAT_FILTER_WORD, + CHAR_INS_CHAT_FILTER_WORD, + CHAR_DEL_CHAT_FILTER_WORD, + CHAR_SEL_CHAR_SETTINGS, CHAR_REP_CHAR_SETTINGS, CHAR_DEL_CHAR_SETTINGS, diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index 076c1e5d94..2989bbf5e0 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -684,6 +684,9 @@ enum RBACPermissions RBAC_PERM_COMMAND_DEBUG_INFO = 920, RBAC_PERM_COMMAND_DEBUG_COSMETIC = 921, RBAC_PERM_COMMAND_PET_RENAME = 922, + RBAC_PERM_COMMAND_CHATFILTER_LIST = 923, + RBAC_PERM_COMMAND_CHATFILTER_ADD = 924, + RBAC_PERM_COMMAND_CHATFILTER_REMOVE = 925, // custom permissions 1000+ RBAC_PERM_MAX }; diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index f04bc00edc..902fb413d4 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -9390,6 +9390,12 @@ void Player::Say(std::string_view text, Language language, WorldObject const* /* if (!sScriptMgr->OnPlayerCanUseChat(this, CHAT_MSG_SAY, language, _text)) return; + if (sWorld->getBoolConfig(CONFIG_CHAT_FILTER_SAY) && IsChatFiltered(text)) + { + ChatHandler(GetSession()).SendSysMessage(LANG_CHATFILTER_SAY); + return; + } + WorldPacket data; ChatHandler::BuildChatPacket(data, CHAT_MSG_SAY, language, this, this, _text); @@ -9412,6 +9418,12 @@ void Player::Yell(std::string_view text, Language language, WorldObject const* / if (!sScriptMgr->OnPlayerCanUseChat(this, CHAT_MSG_YELL, language, _text)) return; + if (sWorld->getBoolConfig(CONFIG_CHAT_FILTER_YELL) && IsChatFiltered(text)) + { + ChatHandler(GetSession()).SendSysMessage(LANG_CHATFILTER_YELL); + return; + } + WorldPacket data; ChatHandler::BuildChatPacket(data, CHAT_MSG_YELL, language, this, this, _text); @@ -9434,6 +9446,12 @@ void Player::TextEmote(std::string_view text, WorldObject const* /*= nullptr*/, if (!sScriptMgr->OnPlayerCanUseChat(this, CHAT_MSG_EMOTE, LANG_UNIVERSAL, _text)) return; + if (sWorld->getBoolConfig(CONFIG_CHAT_FILTER_EMOTE) && IsChatFiltered(text)) + { + ChatHandler(GetSession()).SendSysMessage(LANG_CHATFILTER_EMOTE); + return; + } + WorldPacket data; ChatHandler::BuildChatPacket(data, CHAT_MSG_EMOTE, LANG_UNIVERSAL, this, this, _text); @@ -9463,15 +9481,24 @@ void Player::Whisper(std::string_view text, Language language, Player* target, b if (!sScriptMgr->OnPlayerCanUseChat(this, CHAT_MSG_WHISPER, language, _text, target)) return; + bool isFiltered = sWorld->getBoolConfig(CONFIG_CHAT_FILTER_WHISPER) && IsChatFiltered(text); + WorldPacket data; - ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER, language, this, this, _text); - target->SendDirectMessage(&data); + if (!isFiltered || isAddonMessage) + { + ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER, language, this, this, _text); + target->SendDirectMessage(&data); + } // rest stuff shouldn't happen in case of addon message if (isAddonMessage) return; - ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER_INFORM, Language(language), target, target, _text); + ChatMsg msgType = CHAT_MSG_WHISPER_INFORM; + if (isFiltered) + msgType = CHAT_MSG_FILTERED; + + ChatHandler::BuildChatPacket(data, msgType, Language(language), target, target, _text); SendDirectMessage(&data); if (!isAcceptWhispers() && !IsGameMaster() && !target->IsGameMaster()) @@ -9512,6 +9539,11 @@ void Player::Whisper(uint32 textId, Player* target, bool isBossWhisper) target->SendDirectMessage(&data); } +bool Player::IsChatFiltered(std::string_view text) +{ + return sObjectMgr->IsChatFiltered(text); +} + void Player::PetSpellInitialize() { Pet* pet = GetPet(); diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 71831c18bc..1187ba7ab8 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1245,6 +1245,8 @@ public: /// Handles whispers from Addons and players based on sender, receiver's guid and language. void Whisper(std::string_view text, Language language, Player* receiver, bool = false) override; void Whisper(uint32 textId, Player* target, bool isBossWhisper = false) override; + /// Returns true if @p text contains any word from the `chat_filter` DB table (substring, case-insensitive). + static bool IsChatFiltered(std::string_view text); /*********************************************************/ /*** STORAGE SYSTEM ***/ diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 2590926cc6..b934ffa5cf 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -9197,6 +9197,73 @@ void ObjectMgr::AddProfanityPlayerName(std::string const& name) } } +void ObjectMgr::LoadChatFilter() +{ + uint32 oldMSTime = getMSTime(); + + _chatFilterAutomaton.reset(); // need for reload case + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAT_FILTER); + PreparedQueryResult result = CharacterDatabase.Query(stmt); + + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 chat filter words. DB table `chat_filter` is empty!"); + LOG_INFO("server.loading", " "); + return; + } + + auto automaton = std::make_unique>(); + uint32 count = 0; + + do + { + Field* fields = result->Fetch(); + std::string word = fields[1].Get(); + + if (word.empty()) + continue; + + std::wstring wstr; + if (!Utf8toWStr(word, wstr)) + { + LOG_ERROR("sql.sql", "Table `chat_filter` has invalid word: {}", word); + continue; + } + + wstrToLower(wstr); + + if (wstr.empty()) + continue; + + automaton->Insert(wstr); + ++count; + } while (result->NextRow()); + + if (count > 0) + { + automaton->Build(); + _chatFilterAutomaton = std::move(automaton); + } + + LOG_INFO("server.loading", ">> Loaded {} chat filter words in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +bool ObjectMgr::IsChatFiltered(std::string_view text) const +{ + if (!_chatFilterAutomaton || text.empty()) + return false; + + std::wstring wtext; + if (!Utf8toWStr(text, wtext)) + return false; + + wstrToLower(wtext); + + return _chatFilterAutomaton->ContainsAny(wtext); +} + enum LanguageType { LT_BASIC_LATIN = 0x0000, diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index 24f44441e9..8a89fc3fcd 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -18,6 +18,7 @@ #ifndef _OBJECTMGR_H #define _OBJECTMGR_H +#include "AhoCorasick.h" #include "Bag.h" #include "ConditionMgr.h" #include "Creature.h" @@ -38,6 +39,7 @@ #include #include #include +#include #include class Item; @@ -1418,6 +1420,10 @@ public: [[nodiscard]] bool IsProfanityName(std::string_view name) const; void AddProfanityPlayerName(std::string const& name); + // chat filter (substring chat content filter) + void LoadChatFilter(); + [[nodiscard]] bool IsChatFiltered(std::string_view text) const; + // name with valid structure and symbols static uint8 CheckPlayerName(std::string_view name, bool create = false); static PetNameInvalidReason CheckPetName(std::string_view name); @@ -1587,6 +1593,9 @@ private: typedef std::set ProfanityNamesContainer; ProfanityNamesContainer _profanityNamesStore; + //chat filter (Aho-Corasick automaton; matches any banned word as substring of input) + std::unique_ptr> _chatFilterAutomaton; + GameTeleContainer _gameTeleStore; ScriptNameContainer _scriptNamesStore; diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 9067bff5c3..20bd52792c 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -1229,7 +1229,12 @@ enum AcoreStrings LANG_MAIL_RETURN_ALREADY_RETURNED = 5142, LANG_MAIL_RETURN_HOOK_BLOCKED = 5143, - // Room for more strings 5144-9999 + // Chat filter + LANG_CHATFILTER_EMOTE = 5144, + LANG_CHATFILTER_SAY = 5145, + LANG_CHATFILTER_YELL = 5146, + + // Room for more strings 5147-9999 // Level requirement notifications LANG_SAY_REQ = 6604, diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index e1ec20a475..c214656124 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -764,6 +764,9 @@ void World::SetInitialWorldSettings() sObjectMgr->LoadProfanityNamesFromDB(); sObjectMgr->LoadProfanityNamesFromDBC(); // Needs to be after LoadProfanityNamesFromDB() + LOG_INFO("server.loading", "Loading Chat Filter..."); + sObjectMgr->LoadChatFilter(); + LOG_INFO("server.loading", "Loading GameObjects for Quests..."); sObjectMgr->LoadGameObjectForQuests(); diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 4d99a309fa..f1cf5803ce 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -198,6 +198,11 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_STRICT_CHANNEL_NAMES, "StrictChannelNames", 0); SetConfigValue(CONFIG_STRICT_PET_NAMES, "StrictPetNames", 0); + SetConfigValue(CONFIG_CHAT_FILTER_WHISPER, "ChatFilter.Whisper", true); + SetConfigValue(CONFIG_CHAT_FILTER_SAY, "ChatFilter.Say", false); + SetConfigValue(CONFIG_CHAT_FILTER_YELL, "ChatFilter.Yell", false); + SetConfigValue(CONFIG_CHAT_FILTER_EMOTE, "ChatFilter.Emote", false); + SetConfigValue(CONFIG_ALLOW_TWO_SIDE_ACCOUNTS, "AllowTwoSide.Accounts", true); SetConfigValue(CONFIG_ALLOW_TWO_SIDE_INTERACTION_CALENDAR, "AllowTwoSide.Interaction.Calendar", false); SetConfigValue(CONFIG_ALLOW_TWO_SIDE_INTERACTION_CHAT, "AllowTwoSide.Interaction.Chat", false); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index 0f890bfb9b..b9e79c95f9 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -139,6 +139,10 @@ enum ServerConfigs CONFIG_OBJECT_QUEST_MARKERS, CONFIG_STRICT_NAMES_RESERVED, CONFIG_STRICT_NAMES_PROFANITY, + CONFIG_CHAT_FILTER_WHISPER, + CONFIG_CHAT_FILTER_SAY, + CONFIG_CHAT_FILTER_YELL, + CONFIG_CHAT_FILTER_EMOTE, CONFIG_ALLOWS_RANK_MOD_FOR_PET_HEALTH, CONFIG_MUNCHING_BLIZZLIKE, CONFIG_ENABLE_DAZE, diff --git a/src/server/scripts/Commands/cs_chatfilter.cpp b/src/server/scripts/Commands/cs_chatfilter.cpp new file mode 100644 index 0000000000..8b2757c2a1 --- /dev/null +++ b/src/server/scripts/Commands/cs_chatfilter.cpp @@ -0,0 +1,127 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "Chat.h" +#include "CommandScript.h" +#include "DatabaseEnv.h" +#include "ObjectMgr.h" + +using namespace Acore::ChatCommands; + +class chatfilter_commandscript : public CommandScript +{ +public: + chatfilter_commandscript() : CommandScript("chatfilter_commandscript") { } + + ChatCommandTable GetCommands() const override + { + static ChatCommandTable chatfilterCommandTable = + { + { "list", HandleChatFilterListCommand, rbac::RBAC_PERM_COMMAND_CHATFILTER_LIST, Console::Yes }, + { "add", HandleChatFilterAddCommand, rbac::RBAC_PERM_COMMAND_CHATFILTER_ADD, Console::Yes }, + { "remove", HandleChatFilterRemoveCommand, rbac::RBAC_PERM_COMMAND_CHATFILTER_REMOVE, Console::Yes } + }; + + static ChatCommandTable commandTable = + { + { "chatfilter", chatfilterCommandTable } + }; + + return commandTable; + } + + static bool HandleChatFilterListCommand(ChatHandler* handler) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAT_FILTER); + PreparedQueryResult result = CharacterDatabase.Query(stmt); + + if (!result) + { + handler->SendSysMessage("No chat filter words found."); + return true; + } + + handler->SendSysMessage("Chat filter words:"); + uint32 count = 0; + do + { + Field* fields = result->Fetch(); + uint32 id = fields[0].Get(); + std::string word = fields[1].Get(); + handler->PSendSysMessage(" ID: {} | Word: {}", id, word); + ++count; + } while (result->NextRow()); + + handler->PSendSysMessage("{} chat filter word(s) total.", count); + return true; + } + + static bool HandleChatFilterAddCommand(ChatHandler* handler, Tail word) + { + if (word.empty()) + return false; + + std::string text(word); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAT_FILTER_WORD); + stmt->SetData(0, text); + if (CharacterDatabase.Query(stmt)) + { + handler->SendErrorMessage("Chat filter word \"{}\" already exists.", text); + return true; + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAT_FILTER_WORD); + stmt->SetData(0, text); + CharacterDatabase.DirectExecute(stmt); + + sObjectMgr->LoadChatFilter(); + + handler->PSendSysMessage("Chat filter word \"{}\" added.", text); + return true; + } + + static bool HandleChatFilterRemoveCommand(ChatHandler* handler, Tail word) + { + if (word.empty()) + return false; + + std::string text(word); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAT_FILTER_WORD); + stmt->SetData(0, text); + if (!CharacterDatabase.Query(stmt)) + { + handler->SendErrorMessage("Chat filter word \"{}\" not found.", text); + return true; + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAT_FILTER_WORD); + stmt->SetData(0, text); + CharacterDatabase.DirectExecute(stmt); + + sObjectMgr->LoadChatFilter(); + + handler->PSendSysMessage("Chat filter word \"{}\" removed.", text); + return true; + } +}; + +void AddSC_chatfilter_commandscript() +{ + new chatfilter_commandscript(); +} diff --git a/src/server/scripts/Commands/cs_reload.cpp b/src/server/scripts/Commands/cs_reload.cpp index 8808d2bdef..ba7d9c44d4 100644 --- a/src/server/scripts/Commands/cs_reload.cpp +++ b/src/server/scripts/Commands/cs_reload.cpp @@ -146,6 +146,7 @@ public: { "reference_loot_template", HandleReloadLootTemplatesReferenceCommand, rbac::RBAC_PERM_COMMAND_RELOAD_REFERENCE_LOOT_TEMPLATE, Console::Yes }, { "reserved_name", HandleReloadReservedNameCommand, rbac::RBAC_PERM_COMMAND_RELOAD_RESERVED_NAME, Console::Yes }, { "profanity_name", HandleReloadProfanityNameCommand, rbac::RBAC_PERM_COMMAND_RELOAD, Console::Yes }, + { "chat_filter", HandleReloadChatFilterCommand, rbac::RBAC_PERM_COMMAND_RELOAD, Console::Yes }, { "reputation_reward_rate", HandleReloadReputationRewardRateCommand, rbac::RBAC_PERM_COMMAND_RELOAD_REPUTATION_REWARD_RATE, Console::Yes }, { "reputation_spillover_template", HandleReloadReputationRewardRateCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPILLOVER_TEMPLATE, Console::Yes }, { "skill_discovery_template", HandleReloadSkillDiscoveryTemplateCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SKILL_DISCOVERY_TEMPLATE, Console::Yes }, @@ -210,6 +211,7 @@ public: HandleReloadCommandCommand(handler); HandleReloadReservedNameCommand(handler); HandleReloadProfanityNameCommand(handler); + HandleReloadChatFilterCommand(handler); HandleReloadAcoreStringCommand(handler); HandleReloadGameTeleCommand(handler); HandleReloadCreatureMovementOverrideCommand(handler); @@ -832,6 +834,14 @@ public: return true; } + static bool HandleReloadChatFilterCommand(ChatHandler* handler) + { + LOG_INFO("server.loading", "Reloading Chat Filter!"); + sObjectMgr->LoadChatFilter(); + handler->SendGlobalGMSysMessage("Chat Filter reloaded."); + return true; + } + static bool HandleReloadReputationRewardRateCommand(ChatHandler* handler) { LOG_INFO("server.loading", "Reloading `reputation_reward_rate` Table!" ); diff --git a/src/server/scripts/Commands/cs_script_loader.cpp b/src/server/scripts/Commands/cs_script_loader.cpp index 9ad077a6a5..bd76bf10ac 100644 --- a/src/server/scripts/Commands/cs_script_loader.cpp +++ b/src/server/scripts/Commands/cs_script_loader.cpp @@ -30,6 +30,7 @@ void AddSC_debug_commandscript(); void AddSC_deserter_commandscript(); void AddSC_disable_commandscript(); void AddSC_event_commandscript(); +void AddSC_chatfilter_commandscript(); void AddSC_gear_commandscript(); void AddSC_gm_commandscript(); void AddSC_go_commandscript(); @@ -88,6 +89,7 @@ void AddCommandsScripts() AddSC_deserter_commandscript(); AddSC_disable_commandscript(); AddSC_event_commandscript(); + AddSC_chatfilter_commandscript(); AddSC_gear_commandscript(); AddSC_gm_commandscript(); AddSC_go_commandscript();