From fab8a1e982c7617e3dd4469804f9fe55c0003d4a Mon Sep 17 00:00:00 2001 From: Jett <55758076+Jettford@users.noreply.github.com> Date: Sun, 17 Jul 2022 07:54:36 +0100 Subject: [PATCH] Implement new chat features --- CMakeLists.txt | 9 ++-- dChatFilter/dChatFilter.cpp | 65 +++++++++++++++++++---------- dChatFilter/dChatFilter.h | 15 +++---- dGame/dComponents/PetComponent.cpp | 2 +- dNet/ClientPackets.cpp | 15 ++++++- dNet/WorldPackets.cpp | 23 +++++----- resources/blacklist.dcf | Bin 0 -> 16160 bytes 7 files changed, 84 insertions(+), 45 deletions(-) create mode 100644 resources/blacklist.dcf diff --git a/CMakeLists.txt b/CMakeLists.txt index 0817412b..caa61e4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,13 +84,14 @@ make_directory(${CMAKE_BINARY_DIR}/locale) make_directory(${CMAKE_BINARY_DIR}/logs) # Copy ini files on first build -set(INI_FILES "authconfig.ini" "chatconfig.ini" "worldconfig.ini" "masterconfig.ini") -foreach(ini ${INI_FILES}) - if (NOT EXISTS ${PROJECT_BINARY_DIR}/${ini}) +set(RESOURCE_FILES "authconfig.ini" "chatconfig.ini" "worldconfig.ini" "masterconfig.ini" "blacklist.dcf") +foreach(resource_file ${RESOURCE_FILES}) + if (NOT EXISTS ${PROJECT_BINARY_DIR}/${resource_file}) configure_file( - ${CMAKE_SOURCE_DIR}/resources/${ini} ${PROJECT_BINARY_DIR}/${ini} + ${CMAKE_SOURCE_DIR}/resources/${resource_file} ${PROJECT_BINARY_DIR}/${resource_file} COPYONLY ) + message("Moved ${resource_file} to project binary directory") endif() endforeach() diff --git a/dChatFilter/dChatFilter.cpp b/dChatFilter/dChatFilter.cpp index 7f8187a5..fe8a0d10 100644 --- a/dChatFilter/dChatFilter.cpp +++ b/dChatFilter/dChatFilter.cpp @@ -8,8 +8,9 @@ #include #include "dCommonVars.h" -#include "Database.h" #include "dLogger.h" +#include "dConfig.h" +#include "Database.h" #include "Game.h" using namespace dChatFilterDCF; @@ -21,25 +22,30 @@ dChatFilter::dChatFilter(const std::string& filepath, bool dontGenerateDCF) { ReadWordlistPlaintext(filepath + ".txt"); if (!m_DontGenerateDCF) ExportWordlistToDCF(filepath + ".dcf"); } - else if (!ReadWordlistDCF(filepath + ".dcf")) { + else if (!ReadWordlistDCF(filepath + ".dcf", true)) { ReadWordlistPlaintext(filepath + ".txt"); ExportWordlistToDCF(filepath + ".dcf"); } + if (BinaryIO::DoesFileExist("blacklist.dcf")) { + ReadWordlistDCF("blacklist.dcf", false); + } + //Read player names that are ok as well: auto stmt = Database::CreatePreppedStmt("select name from charinfo;"); auto res = stmt->executeQuery(); while (res->next()) { std::string line = res->getString(1).c_str(); std::transform(line.begin(), line.end(), line.begin(), ::tolower); //Transform to lowercase - m_Words.push_back(CalculateHash(line)); + m_YesYesWords.push_back(CalculateHash(line)); } delete res; delete stmt; } dChatFilter::~dChatFilter() { - m_Words.clear(); + m_YesYesWords.clear(); + m_NoNoWords.clear(); } void dChatFilter::ReadWordlistPlaintext(const std::string& filepath) { @@ -49,12 +55,12 @@ void dChatFilter::ReadWordlistPlaintext(const std::string& filepath) { while (std::getline(file, line)) { line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); std::transform(line.begin(), line.end(), line.begin(), ::tolower); //Transform to lowercase - m_Words.push_back(CalculateHash(line)); + m_YesYesWords.push_back(CalculateHash(line)); } } } -bool dChatFilter::ReadWordlistDCF(const std::string& filepath) { +bool dChatFilter::ReadWordlistDCF(const std::string& filepath, bool whiteList) { std::ifstream file(filepath, std::ios::binary); if (file) { fileHeader hdr; @@ -67,12 +73,14 @@ bool dChatFilter::ReadWordlistDCF(const std::string& filepath) { if (hdr.formatVersion == formatVersion) { size_t wordsToRead = 0; BinaryIO::BinaryRead(file, wordsToRead); - m_Words.reserve(wordsToRead); + if (whiteList) m_YesYesWords.reserve(wordsToRead); + else m_NoNoWords.reserve(wordsToRead); size_t word = 0; for (size_t i = 0; i < wordsToRead; ++i) { BinaryIO::BinaryRead(file, word); - m_Words.push_back(word); + if (whiteList) m_YesYesWords.push_back(word); + else m_NoNoWords.push_back(word); } return true; @@ -91,9 +99,9 @@ void dChatFilter::ExportWordlistToDCF(const std::string& filepath) { if (file) { BinaryIO::BinaryWrite(file, uint32_t(dChatFilterDCF::header)); BinaryIO::BinaryWrite(file, uint32_t(dChatFilterDCF::formatVersion)); - BinaryIO::BinaryWrite(file, size_t(m_Words.size())); + BinaryIO::BinaryWrite(file, size_t(m_YesYesWords.size())); - for (size_t word : m_Words) { + for (size_t word : m_YesYesWords) { BinaryIO::BinaryWrite(file, word); } @@ -101,31 +109,44 @@ void dChatFilter::ExportWordlistToDCF(const std::string& filepath) { } } -bool dChatFilter::IsSentenceOkay(const std::string& message, int gmLevel) { - if (gmLevel > GAME_MASTER_LEVEL_FORUM_MODERATOR) return true; //If anything but a forum mod, return true. - if (message.empty()) return true; +std::vector dChatFilter::IsSentenceOkay(const std::string& message, int gmLevel, bool whiteList) { + if (gmLevel > GAME_MASTER_LEVEL_FORUM_MODERATOR) return { }; //If anything but a forum mod, return true. + if (message.empty()) return { }; + if (!whiteList && m_NoNoWords.empty()) return { "" }; std::stringstream sMessage(message); std::string segment; std::regex reg("(!*|\\?*|\\;*|\\.*|\\,*)"); + std::vector listOfBadSegments = std::vector(); + while (std::getline(sMessage, segment, ' ')) { + std::string originalSegment = segment; + std::transform(segment.begin(), segment.end(), segment.begin(), ::tolower); //Transform to lowercase segment = std::regex_replace(segment, reg, ""); size_t hash = CalculateHash(segment); if (std::find(m_UserUnapprovedWordCache.begin(), m_UserUnapprovedWordCache.end(), hash) != m_UserUnapprovedWordCache.end()) { - return false; + listOfBadSegments.push_back(originalSegment); // found word that isn't ok, just deny this code works for both white and black list } - if (!IsInWordlist(hash)) { - m_UserUnapprovedWordCache.push_back(hash); - return false; + if (!IsInWordlist(hash, whiteList)) { + if (whiteList) { + m_UserUnapprovedWordCache.push_back(hash); + listOfBadSegments.push_back(originalSegment); + } + } + else { + if (!whiteList) { + m_UserUnapprovedWordCache.push_back(hash); + listOfBadSegments.push_back(originalSegment); + } } } - return true; + return listOfBadSegments; } size_t dChatFilter::CalculateHash(const std::string& word) { @@ -136,6 +157,8 @@ size_t dChatFilter::CalculateHash(const std::string& word) { return value; } -bool dChatFilter::IsInWordlist(size_t word) { - return std::find(m_Words.begin(), m_Words.end(), word) != m_Words.end(); -} +bool dChatFilter::IsInWordlist(size_t word, bool whiteList) { + auto* list = whiteList ? &m_YesYesWords : &m_NoNoWords; + + return std::find(list->begin(), list->end(), word) != list->end(); +} \ No newline at end of file diff --git a/dChatFilter/dChatFilter.h b/dChatFilter/dChatFilter.h index e8ae67d0..5acd6063 100644 --- a/dChatFilter/dChatFilter.h +++ b/dChatFilter/dChatFilter.h @@ -20,17 +20,18 @@ public: dChatFilter(const std::string& filepath, bool dontGenerateDCF); ~dChatFilter(); - void ReadWordlistPlaintext(const std::string & filepath); - bool ReadWordlistDCF(const std::string & filepath); - void ExportWordlistToDCF(const std::string & filepath); - bool IsSentenceOkay(const std::string& message, int gmLevel); + void ReadWordlistPlaintext(const std::string& filepath); + bool ReadWordlistDCF(const std::string& filepath, bool whiteList); + void ExportWordlistToDCF(const std::string& filepath); + std::vector IsSentenceOkay(const std::string& message, int gmLevel, bool whiteList = true); private: bool m_DontGenerateDCF; - std::vector m_Words; + std::vector m_NoNoWords; + std::vector m_YesYesWords; std::vector m_UserUnapprovedWordCache; //Private functions: size_t CalculateHash(const std::string& word); - bool IsInWordlist(size_t word); -}; + bool IsInWordlist(size_t word, bool whiteList); +}; \ No newline at end of file diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index d54087aa..3cfb4e8d 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -1193,7 +1193,7 @@ void PetComponent::SetPetNameForModeration(const std::string& petName) { int approved = 1; //default, in mod //Make sure that the name isn't already auto-approved: - if (Game::chatFilter->IsSentenceOkay(petName, 0)) { + if (Game::chatFilter->IsSentenceOkay(petName, 0).empty()) { approved = 2; //approved } diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index ef5b68ef..187ee12f 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -276,6 +276,7 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa std::string message = ""; stream.Read(chatLevel); + printf("%d", chatLevel); stream.Read(requestID); for (uint32_t i = 0; i < 42; ++i) { @@ -292,9 +293,19 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa } std::unordered_map unacceptedItems; - bool bAllClean = Game::chatFilter->IsSentenceOkay(message, user->GetLastUsedChar()->GetGMLevel()); + std::vector segments = Game::chatFilter->IsSentenceOkay(message, entity->GetGMLevel()); + + bool bAllClean = segments.empty(); + if (!bAllClean) { - unacceptedItems.insert(std::make_pair((char)0, (char)message.length())); + for (const auto& item : segments) { + if (item == "") { + unacceptedItems.insert({ (char)0, (char)message.length()}); + break; + } + + unacceptedItems.insert({ message.find(item), item.length() }); + } } if (user->GetIsMuted()) { diff --git a/dNet/WorldPackets.cpp b/dNet/WorldPackets.cpp index ae8f71a4..94792c91 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -192,19 +192,22 @@ void WorldPackets::SendChatModerationResponse(const SystemAddress& sysAddr, bool CBITSTREAM PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_CHAT_MODERATION_STRING); - bitStream.Write(static_cast(requestAccepted)); - bitStream.Write(static_cast(0)); - bitStream.Write(static_cast(requestID)); - bitStream.Write(static_cast(0)); + bitStream.Write(unacceptedItems.empty()); // Is sentence ok? + bitStream.Write(0x16); // Source ID, unknown - for (uint32_t i = 0; i < 33; ++i) { - bitStream.Write(static_cast(receiver[i])); + bitStream.Write(static_cast(requestID)); // request ID + bitStream.Write(static_cast(0)); // chat mode + + PacketUtils::WritePacketWString(receiver, 42, &bitStream); // receiver name + + for (auto it : unacceptedItems) { + bitStream.Write(it.first); // start index + bitStream.Write(it.second); // length } - for (std::unordered_map::iterator it = unacceptedItems.begin(); it != unacceptedItems.end(); ++it) { - bitStream.Write(it->first); - bitStream.Write(it->second); - } + for (int i = unacceptedItems.size(); 64 > i; i++) { + bitStream.Write(0); + } SEND_PACKET } diff --git a/resources/blacklist.dcf b/resources/blacklist.dcf new file mode 100644 index 0000000000000000000000000000000000000000..cdc64c2f5b245f4526974ab9cb1770fd8355b783 GIT binary patch literal 16160 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