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 1/3] 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 <regex> #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<std::string> 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<std::string> listOfBadSegments = std::vector<std::string>(); + 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<std::string> IsSentenceOkay(const std::string& message, int gmLevel, bool whiteList = true); private: bool m_DontGenerateDCF; - std::vector<size_t> m_Words; + std::vector<size_t> m_NoNoWords; + std::vector<size_t> m_YesYesWords; std::vector<size_t> 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<char, char> unacceptedItems; - bool bAllClean = Game::chatFilter->IsSentenceOkay(message, user->GetLastUsedChar()->GetGMLevel()); + std::vector<std::string> 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<char>(requestAccepted)); - bitStream.Write(static_cast<uint16_t>(0)); - bitStream.Write(static_cast<uint8_t>(requestID)); - bitStream.Write(static_cast<char>(0)); + bitStream.Write<uint8_t>(unacceptedItems.empty()); // Is sentence ok? + bitStream.Write<uint16_t>(0x16); // Source ID, unknown - for (uint32_t i = 0; i < 33; ++i) { - bitStream.Write(static_cast<uint16_t>(receiver[i])); + bitStream.Write(static_cast<uint8_t>(requestID)); // request ID + bitStream.Write(static_cast<char>(0)); // chat mode + + PacketUtils::WritePacketWString(receiver, 42, &bitStream); // receiver name + + for (auto it : unacceptedItems) { + bitStream.Write<uint8_t>(it.first); // start index + bitStream.Write<uint8_t>(it.second); // length } - for (std::unordered_map<char, char>::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<uint16_t>(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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 literal 0 HcmV?d00001 From 945e57249393d8b3586016afd4a795c507caee87 Mon Sep 17 00:00:00 2001 From: Jett <55758076+Jettford@users.noreply.github.com> Date: Sun, 17 Jul 2022 09:40:34 +0100 Subject: [PATCH 2/3] Add best friend check and complete blacklist --- dChatFilter/dChatFilter.cpp | 46 ++++++++++++----------------- dChatFilter/dChatFilter.h | 5 ++-- dGame/User.cpp | 2 ++ dGame/User.h | 5 ++++ dNet/ClientPackets.cpp | 56 ++++++++++++++++++++++++++++++++++-- resources/blacklist.dcf | Bin 16160 -> 16160 bytes 6 files changed, 82 insertions(+), 32 deletions(-) diff --git a/dChatFilter/dChatFilter.cpp b/dChatFilter/dChatFilter.cpp index fe8a0d10..eb6674a4 100644 --- a/dChatFilter/dChatFilter.cpp +++ b/dChatFilter/dChatFilter.cpp @@ -19,12 +19,12 @@ dChatFilter::dChatFilter(const std::string& filepath, bool dontGenerateDCF) { m_DontGenerateDCF = dontGenerateDCF; if (!BinaryIO::DoesFileExist(filepath + ".dcf") || m_DontGenerateDCF) { - ReadWordlistPlaintext(filepath + ".txt"); - if (!m_DontGenerateDCF) ExportWordlistToDCF(filepath + ".dcf"); + ReadWordlistPlaintext(filepath + ".txt", true); + if (!m_DontGenerateDCF) ExportWordlistToDCF(filepath + ".dcf", true); } else if (!ReadWordlistDCF(filepath + ".dcf", true)) { - ReadWordlistPlaintext(filepath + ".txt"); - ExportWordlistToDCF(filepath + ".dcf"); + ReadWordlistPlaintext(filepath + ".txt", true); + ExportWordlistToDCF(filepath + ".dcf", true); } if (BinaryIO::DoesFileExist("blacklist.dcf")) { @@ -48,14 +48,15 @@ dChatFilter::~dChatFilter() { m_NoNoWords.clear(); } -void dChatFilter::ReadWordlistPlaintext(const std::string& filepath) { +void dChatFilter::ReadWordlistPlaintext(const std::string& filepath, bool whiteList) { std::ifstream file(filepath); if (file) { std::string line; 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_YesYesWords.push_back(CalculateHash(line)); + if (whiteList) m_YesYesWords.push_back(CalculateHash(line)); + else m_NoNoWords.push_back(CalculateHash(line)); } } } @@ -94,14 +95,14 @@ bool dChatFilter::ReadWordlistDCF(const std::string& filepath, bool whiteList) { return false; } -void dChatFilter::ExportWordlistToDCF(const std::string& filepath) { +void dChatFilter::ExportWordlistToDCF(const std::string& filepath, bool whiteList) { std::ofstream file(filepath, std::ios::binary | std::ios_base::out); if (file) { BinaryIO::BinaryWrite(file, uint32_t(dChatFilterDCF::header)); BinaryIO::BinaryWrite(file, uint32_t(dChatFilterDCF::formatVersion)); - BinaryIO::BinaryWrite(file, size_t(m_YesYesWords.size())); + BinaryIO::BinaryWrite(file, size_t(whiteList ? m_YesYesWords.size() : m_NoNoWords.size())); - for (size_t word : m_YesYesWords) { + for (size_t word : whiteList ? m_YesYesWords : m_NoNoWords) { BinaryIO::BinaryWrite(file, word); } @@ -128,21 +129,18 @@ std::vector<std::string> dChatFilter::IsSentenceOkay(const std::string& message, size_t hash = CalculateHash(segment); - if (std::find(m_UserUnapprovedWordCache.begin(), m_UserUnapprovedWordCache.end(), hash) != m_UserUnapprovedWordCache.end()) { - listOfBadSegments.push_back(originalSegment); // found word that isn't ok, just deny this code works for both white and black list + if (std::find(m_UserUnapprovedWordCache.begin(), m_UserUnapprovedWordCache.end(), hash) != m_UserUnapprovedWordCache.end() && whiteList) { + listOfBadSegments.push_back(originalSegment); } - if (!IsInWordlist(hash, whiteList)) { - if (whiteList) { - m_UserUnapprovedWordCache.push_back(hash); - listOfBadSegments.push_back(originalSegment); - } + if (std::find(m_YesYesWords.begin(), m_YesYesWords.end(), hash) == m_YesYesWords.end() && whiteList) { + m_UserUnapprovedWordCache.push_back(hash); + listOfBadSegments.push_back(originalSegment); } - else { - if (!whiteList) { - m_UserUnapprovedWordCache.push_back(hash); - listOfBadSegments.push_back(originalSegment); - } + + if (std::find(m_NoNoWords.begin(), m_NoNoWords.end(), hash) != m_NoNoWords.end() && !whiteList) { + m_UserUnapprovedWordCache.push_back(hash); + listOfBadSegments.push_back(originalSegment); } } @@ -155,10 +153,4 @@ size_t dChatFilter::CalculateHash(const std::string& word) { size_t value = hash(word); return value; -} - -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 5acd6063..62a47242 100644 --- a/dChatFilter/dChatFilter.h +++ b/dChatFilter/dChatFilter.h @@ -20,9 +20,9 @@ public: dChatFilter(const std::string& filepath, bool dontGenerateDCF); ~dChatFilter(); - void ReadWordlistPlaintext(const std::string& filepath); + void ReadWordlistPlaintext(const std::string& filepath, bool whiteList); bool ReadWordlistDCF(const std::string& filepath, bool whiteList); - void ExportWordlistToDCF(const std::string& filepath); + void ExportWordlistToDCF(const std::string& filepath, bool whiteList); std::vector<std::string> IsSentenceOkay(const std::string& message, int gmLevel, bool whiteList = true); private: @@ -33,5 +33,4 @@ private: //Private functions: size_t CalculateHash(const std::string& word); - bool IsInWordlist(size_t word, bool whiteList); }; \ No newline at end of file diff --git a/dGame/User.cpp b/dGame/User.cpp index 3efc4d0a..98a37954 100644 --- a/dGame/User.cpp +++ b/dGame/User.cpp @@ -18,6 +18,8 @@ User::User(const SystemAddress& sysAddr, const std::string& username, const std: m_SystemAddress = sysAddr; m_Username = username; m_LoggedInCharID = 0; + + m_IsBestFriendMap = std::unordered_map<std::string, bool>(); //HACK HACK HACK //This needs to be re-enabled / updated whenever the mute stuff is moved to another table. diff --git a/dGame/User.h b/dGame/User.h index 3be9148e..78640b38 100644 --- a/dGame/User.h +++ b/dGame/User.h @@ -42,6 +42,9 @@ public: bool GetLastChatMessageApproved() { return m_LastChatMessageApproved; } void SetLastChatMessageApproved(bool approved) { m_LastChatMessageApproved = approved; } + std::unordered_map<std::string, bool> GetIsBestFriendMap() { return m_IsBestFriendMap; } + void SetIsBestFriendMap(std::unordered_map<std::string, bool> mapToSet) { m_IsBestFriendMap = mapToSet; } + bool GetIsMuted() const; time_t GetMuteExpire() const; @@ -63,6 +66,8 @@ private: std::vector<Character*> m_Characters; LWOOBJID m_LoggedInCharID; + std::unordered_map<std::string, bool> m_IsBestFriendMap; + bool m_LastChatMessageApproved = false; int m_AmountOfTimesOutOfSync = 0; const int m_MaxDesyncAllowed = 12; diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index 187ee12f..abb08688 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -30,6 +30,9 @@ #include "VehiclePhysicsComponent.h" #include "dConfig.h" #include "CharacterComponent.h" +#include "Database.h" + + void ClientPackets::HandleChatMessage(const SystemAddress& sysAddr, Packet* packet) { User* user = UserManager::Instance()->GetUser(sysAddr); @@ -276,7 +279,6 @@ 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) { @@ -285,6 +287,12 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa receiver.push_back(static_cast<uint8_t>(character)); } + if (!receiver.empty()) { + if (std::string(receiver.c_str(), 4) == "[GM]") { + receiver = std::string(receiver.c_str() + 4, receiver.size() - 4); + } + } + stream.Read(messageLength); for (uint32_t i = 0; i < messageLength; ++i) { uint16_t character; @@ -292,8 +300,52 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa message.push_back(static_cast<uint8_t>(character)); } + bool isBestFriend = false; + + if (chatLevel == 1) { + // Private chat + LWOOBJID idOfReceiver = LWOOBJID_EMPTY; + + { + sql::PreparedStatement* stmt = Database::CreatePreppedStmt("SELECT name FROM charinfo WHERE name = ?"); + stmt->setString(1, receiver); + + sql::ResultSet* res = stmt->executeQuery(); + + if (res->next()) { + idOfReceiver = res->getInt("id"); + } + } + + if (user->GetIsBestFriendMap().find(receiver) == user->GetIsBestFriendMap().end() && idOfReceiver != LWOOBJID_EMPTY) { + sql::PreparedStatement* stmt = Database::CreatePreppedStmt("SELECT * FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;"); + stmt->setInt(1, entity->GetObjectID()); + stmt->setInt(2, idOfReceiver); + stmt->setInt(3, idOfReceiver); + stmt->setInt(4, entity->GetObjectID()); + + sql::ResultSet* res = stmt->executeQuery(); + + if (res->next()) { + isBestFriend = res->getInt("best_friend") == 3; + } + + if (isBestFriend) { + auto tmpBestFriendMap = user->GetIsBestFriendMap(); + tmpBestFriendMap[receiver] = true; + user->SetIsBestFriendMap(tmpBestFriendMap); + } + + delete res; + delete stmt; + } + else if (user->GetIsBestFriendMap().find(receiver) != user->GetIsBestFriendMap().end()) { + isBestFriend = true; + } + } + std::unordered_map<char, char> unacceptedItems; - std::vector<std::string> segments = Game::chatFilter->IsSentenceOkay(message, entity->GetGMLevel()); + std::vector<std::string> segments = Game::chatFilter->IsSentenceOkay(message, entity->GetGMLevel(), !(isBestFriend && chatLevel == 1)); bool bAllClean = segments.empty(); diff --git a/resources/blacklist.dcf b/resources/blacklist.dcf index cdc64c2f5b245f4526974ab9cb1770fd8355b783..ca4db242aa20dbc20082100845e658ab7d958bb6 100644 GIT binary patch literal 16160 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 No@;^US>So|{{UYoH%$Nl literal 16160 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 From 69a0d5a50bf4ae47d8d25df18117903d9cc604a5 Mon Sep 17 00:00:00 2001 From: Jett <55758076+Jettford@users.noreply.github.com> Date: Mon, 18 Jul 2022 10:01:43 +0100 Subject: [PATCH 3/3] Efficiency and naming changes. --- CMakeLists.txt | 6 ++--- dChatFilter/dChatFilter.cpp | 44 ++++++++++++++++++++----------------- dChatFilter/dChatFilter.h | 8 +++---- dNet/ClientPackets.cpp | 21 +++++------------- dNet/WorldPackets.cpp | 18 +++++++-------- dNet/WorldPackets.h | 2 +- 6 files changed, 47 insertions(+), 52 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index caa61e4e..a9218bdf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,10 +15,10 @@ foreach(variable ${variables}) # If the string contains a #, skip it if(NOT "${variable}" MATCHES "#") - # Split the variable into name and value + # Split the variable into name and value string(REPLACE "=" ";" variable ${variable}) - # Check that the length of the variable is 2 (name and value) + # Check that the length of the variable is 2 (name and value) list(LENGTH variable length) if(${length} EQUAL 2) @@ -83,7 +83,7 @@ make_directory(${CMAKE_BINARY_DIR}/locale) # Create a /logs directory make_directory(${CMAKE_BINARY_DIR}/logs) -# Copy ini files on first build +# Copy resource files on first build 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}) diff --git a/dChatFilter/dChatFilter.cpp b/dChatFilter/dChatFilter.cpp index eb6674a4..6389623e 100644 --- a/dChatFilter/dChatFilter.cpp +++ b/dChatFilter/dChatFilter.cpp @@ -37,15 +37,15 @@ dChatFilter::dChatFilter(const std::string& filepath, bool dontGenerateDCF) { while (res->next()) { std::string line = res->getString(1).c_str(); std::transform(line.begin(), line.end(), line.begin(), ::tolower); //Transform to lowercase - m_YesYesWords.push_back(CalculateHash(line)); + m_ApprovedWords.push_back(CalculateHash(line)); } delete res; delete stmt; } dChatFilter::~dChatFilter() { - m_YesYesWords.clear(); - m_NoNoWords.clear(); + m_ApprovedWords.clear(); + m_DeniedWords.clear(); } void dChatFilter::ReadWordlistPlaintext(const std::string& filepath, bool whiteList) { @@ -55,8 +55,8 @@ void dChatFilter::ReadWordlistPlaintext(const std::string& filepath, bool whiteL 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 - if (whiteList) m_YesYesWords.push_back(CalculateHash(line)); - else m_NoNoWords.push_back(CalculateHash(line)); + if (whiteList) m_ApprovedWords.push_back(CalculateHash(line)); + else m_DeniedWords.push_back(CalculateHash(line)); } } } @@ -74,14 +74,14 @@ bool dChatFilter::ReadWordlistDCF(const std::string& filepath, bool whiteList) { if (hdr.formatVersion == formatVersion) { size_t wordsToRead = 0; BinaryIO::BinaryRead(file, wordsToRead); - if (whiteList) m_YesYesWords.reserve(wordsToRead); - else m_NoNoWords.reserve(wordsToRead); + if (whiteList) m_ApprovedWords.reserve(wordsToRead); + else m_DeniedWords.reserve(wordsToRead); size_t word = 0; for (size_t i = 0; i < wordsToRead; ++i) { BinaryIO::BinaryRead(file, word); - if (whiteList) m_YesYesWords.push_back(word); - else m_NoNoWords.push_back(word); + if (whiteList) m_ApprovedWords.push_back(word); + else m_DeniedWords.push_back(word); } return true; @@ -100,9 +100,9 @@ void dChatFilter::ExportWordlistToDCF(const std::string& filepath, bool whiteLis if (file) { BinaryIO::BinaryWrite(file, uint32_t(dChatFilterDCF::header)); BinaryIO::BinaryWrite(file, uint32_t(dChatFilterDCF::formatVersion)); - BinaryIO::BinaryWrite(file, size_t(whiteList ? m_YesYesWords.size() : m_NoNoWords.size())); + BinaryIO::BinaryWrite(file, size_t(whiteList ? m_ApprovedWords.size() : m_DeniedWords.size())); - for (size_t word : whiteList ? m_YesYesWords : m_NoNoWords) { + for (size_t word : whiteList ? m_ApprovedWords : m_DeniedWords) { BinaryIO::BinaryWrite(file, word); } @@ -110,16 +110,18 @@ void dChatFilter::ExportWordlistToDCF(const std::string& filepath, bool whiteLis } } -std::vector<std::string> dChatFilter::IsSentenceOkay(const std::string& message, int gmLevel, bool whiteList) { +std::vector<std::pair<uint8_t, uint8_t>> 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 { "" }; + if (!whiteList && m_DeniedWords.empty()) return { { 0, message.length() } }; std::stringstream sMessage(message); std::string segment; std::regex reg("(!*|\\?*|\\;*|\\.*|\\,*)"); - std::vector<std::string> listOfBadSegments = std::vector<std::string>(); + std::vector<std::pair<uint8_t, uint8_t>> listOfBadSegments = std::vector<std::pair<uint8_t, uint8_t>>(); + + uint32_t position = 0; while (std::getline(sMessage, segment, ' ')) { std::string originalSegment = segment; @@ -130,18 +132,20 @@ std::vector<std::string> dChatFilter::IsSentenceOkay(const std::string& message, size_t hash = CalculateHash(segment); if (std::find(m_UserUnapprovedWordCache.begin(), m_UserUnapprovedWordCache.end(), hash) != m_UserUnapprovedWordCache.end() && whiteList) { - listOfBadSegments.push_back(originalSegment); + listOfBadSegments.emplace_back(position, originalSegment.length()); } - if (std::find(m_YesYesWords.begin(), m_YesYesWords.end(), hash) == m_YesYesWords.end() && whiteList) { + if (std::find(m_ApprovedWords.begin(), m_ApprovedWords.end(), hash) == m_ApprovedWords.end() && whiteList) { m_UserUnapprovedWordCache.push_back(hash); - listOfBadSegments.push_back(originalSegment); + listOfBadSegments.emplace_back(position, originalSegment.length()); } - if (std::find(m_NoNoWords.begin(), m_NoNoWords.end(), hash) != m_NoNoWords.end() && !whiteList) { + if (std::find(m_DeniedWords.begin(), m_DeniedWords.end(), hash) != m_DeniedWords.end() && !whiteList) { m_UserUnapprovedWordCache.push_back(hash); - listOfBadSegments.push_back(originalSegment); + listOfBadSegments.emplace_back(position, originalSegment.length()); } + + position += segment.length() + 1; } return listOfBadSegments; @@ -153,4 +157,4 @@ size_t dChatFilter::CalculateHash(const std::string& word) { size_t value = hash(word); return value; -} \ No newline at end of file +} diff --git a/dChatFilter/dChatFilter.h b/dChatFilter/dChatFilter.h index 62a47242..7e7dd859 100644 --- a/dChatFilter/dChatFilter.h +++ b/dChatFilter/dChatFilter.h @@ -23,14 +23,14 @@ public: void ReadWordlistPlaintext(const std::string& filepath, bool whiteList); bool ReadWordlistDCF(const std::string& filepath, bool whiteList); void ExportWordlistToDCF(const std::string& filepath, bool whiteList); - std::vector<std::string> IsSentenceOkay(const std::string& message, int gmLevel, bool whiteList = true); + std::vector<std::pair<uint8_t, uint8_t>> IsSentenceOkay(const std::string& message, int gmLevel, bool whiteList = true); private: bool m_DontGenerateDCF; - std::vector<size_t> m_NoNoWords; - std::vector<size_t> m_YesYesWords; + std::vector<size_t> m_DeniedWords; + std::vector<size_t> m_ApprovedWords; std::vector<size_t> m_UserUnapprovedWordCache; //Private functions: size_t CalculateHash(const std::string& word); -}; \ No newline at end of file +}; diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index abb08688..b9f715ad 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -288,7 +288,7 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa } if (!receiver.empty()) { - if (std::string(receiver.c_str(), 4) == "[GM]") { + if (std::string(receiver.c_str(), 4) == "[GM]") { // Shift the string forward if we are speaking to a GM as the client appends "[GM]" if they are receiver = std::string(receiver.c_str() + 4, receiver.size() - 4); } } @@ -315,6 +315,9 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa if (res->next()) { idOfReceiver = res->getInt("id"); } + + delete stmt; + delete res; } if (user->GetIsBestFriendMap().find(receiver) == user->GetIsBestFriendMap().end() && idOfReceiver != LWOOBJID_EMPTY) { @@ -344,26 +347,14 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa } } - std::unordered_map<char, char> unacceptedItems; - std::vector<std::string> segments = Game::chatFilter->IsSentenceOkay(message, entity->GetGMLevel(), !(isBestFriend && chatLevel == 1)); + std::vector<std::pair<uint8_t, uint8_t>> segments = Game::chatFilter->IsSentenceOkay(message, entity->GetGMLevel(), !(isBestFriend && chatLevel == 1)); bool bAllClean = segments.empty(); - if (!bAllClean) { - 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()) { bAllClean = false; } user->SetLastChatMessageApproved(bAllClean); - WorldPackets::SendChatModerationResponse(sysAddr, bAllClean, requestID, receiver, unacceptedItems); + WorldPackets::SendChatModerationResponse(sysAddr, bAllClean, requestID, receiver, segments); } diff --git a/dNet/WorldPackets.cpp b/dNet/WorldPackets.cpp index 94792c91..54c925d6 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -188,28 +188,28 @@ void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, Entity* ent Game::logger->Log("WorldPackets", "Sent CreateCharacter for ID: %llu\n", entity->GetObjectID()); } -void WorldPackets::SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::unordered_map<char, char> unacceptedItems) { - CBITSTREAM - PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_CHAT_MODERATION_STRING); +void WorldPackets::SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::vector<std::pair<uint8_t, uint8_t>> unacceptedItems) { + CBITSTREAM + PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_CHAT_MODERATION_STRING); bitStream.Write<uint8_t>(unacceptedItems.empty()); // Is sentence ok? bitStream.Write<uint16_t>(0x16); // Source ID, unknown bitStream.Write(static_cast<uint8_t>(requestID)); // request ID - bitStream.Write(static_cast<char>(0)); // chat mode + bitStream.Write(static_cast<char>(0)); // chat mode PacketUtils::WritePacketWString(receiver, 42, &bitStream); // receiver name - for (auto it : unacceptedItems) { - bitStream.Write<uint8_t>(it.first); // start index - bitStream.Write<uint8_t>(it.second); // length - } + for (auto it : unacceptedItems) { + bitStream.Write<uint8_t>(it.first); // start index + bitStream.Write<uint8_t>(it.second); // length + } for (int i = unacceptedItems.size(); 64 > i; i++) { bitStream.Write<uint16_t>(0); } - SEND_PACKET + SEND_PACKET } void WorldPackets::SendGMLevelChange(const SystemAddress& sysAddr, bool success, uint8_t highestLevel, uint8_t prevLevel, uint8_t newLevel) { diff --git a/dNet/WorldPackets.h b/dNet/WorldPackets.h index 3508d6f0..b88602a4 100644 --- a/dNet/WorldPackets.h +++ b/dNet/WorldPackets.h @@ -18,7 +18,7 @@ namespace WorldPackets { void SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift); void SendServerState(const SystemAddress& sysAddr); void SendCreateCharacter(const SystemAddress& sysAddr, Entity* entity, const std::string& xmlData, const std::u16string& username, int32_t gm); - void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::unordered_map<char, char> unacceptedItems); + void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::vector<std::pair<uint8_t, uint8_t>> unacceptedItems); void SendGMLevelChange(const SystemAddress& sysAddr, bool success, uint8_t highestLevel, uint8_t prevLevel, uint8_t newLevel); }