diff --git a/CMakeLists.txt b/CMakeLists.txt index 0817412b..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,14 +83,15 @@ make_directory(${CMAKE_BINARY_DIR}/locale) # Create a /logs directory 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}) +# 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}) 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..6389623e 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; @@ -18,12 +19,16 @@ 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")) { - ReadWordlistPlaintext(filepath + ".txt"); - ExportWordlistToDCF(filepath + ".dcf"); + else if (!ReadWordlistDCF(filepath + ".dcf", true)) { + ReadWordlistPlaintext(filepath + ".txt", true); + ExportWordlistToDCF(filepath + ".dcf", true); + } + + if (BinaryIO::DoesFileExist("blacklist.dcf")) { + ReadWordlistDCF("blacklist.dcf", false); } //Read player names that are ok as well: @@ -32,29 +37,31 @@ 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_Words.push_back(CalculateHash(line)); + m_ApprovedWords.push_back(CalculateHash(line)); } delete res; delete stmt; } dChatFilter::~dChatFilter() { - m_Words.clear(); + m_ApprovedWords.clear(); + m_DeniedWords.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_Words.push_back(CalculateHash(line)); + if (whiteList) m_ApprovedWords.push_back(CalculateHash(line)); + else m_DeniedWords.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 +74,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_ApprovedWords.reserve(wordsToRead); + else m_DeniedWords.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_ApprovedWords.push_back(word); + else m_DeniedWords.push_back(word); } return true; @@ -86,14 +95,14 @@ bool dChatFilter::ReadWordlistDCF(const std::string& filepath) { 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_Words.size())); + BinaryIO::BinaryWrite(file, size_t(whiteList ? m_ApprovedWords.size() : m_DeniedWords.size())); - for (size_t word : m_Words) { + for (size_t word : whiteList ? m_ApprovedWords : m_DeniedWords) { BinaryIO::BinaryWrite(file, word); } @@ -101,31 +110,45 @@ 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_DeniedWords.empty()) return { { 0, message.length() } }; std::stringstream sMessage(message); std::string segment; std::regex reg("(!*|\\?*|\\;*|\\.*|\\,*)"); + std::vector> listOfBadSegments = std::vector>(); + + uint32_t position = 0; + 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; + if (std::find(m_UserUnapprovedWordCache.begin(), m_UserUnapprovedWordCache.end(), hash) != m_UserUnapprovedWordCache.end() && whiteList) { + listOfBadSegments.emplace_back(position, originalSegment.length()); } - if (!IsInWordlist(hash)) { + if (std::find(m_ApprovedWords.begin(), m_ApprovedWords.end(), hash) == m_ApprovedWords.end() && whiteList) { m_UserUnapprovedWordCache.push_back(hash); - return false; + listOfBadSegments.emplace_back(position, originalSegment.length()); } + + if (std::find(m_DeniedWords.begin(), m_DeniedWords.end(), hash) != m_DeniedWords.end() && !whiteList) { + m_UserUnapprovedWordCache.push_back(hash); + listOfBadSegments.emplace_back(position, originalSegment.length()); + } + + position += segment.length() + 1; } - return true; + return listOfBadSegments; } size_t dChatFilter::CalculateHash(const std::string& word) { @@ -135,7 +158,3 @@ 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(); -} diff --git a/dChatFilter/dChatFilter.h b/dChatFilter/dChatFilter.h index e8ae67d0..7e7dd859 100644 --- a/dChatFilter/dChatFilter.h +++ b/dChatFilter/dChatFilter.h @@ -20,17 +20,17 @@ 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 whiteList); + bool ReadWordlistDCF(const std::string& filepath, bool whiteList); + void ExportWordlistToDCF(const std::string& filepath, bool whiteList); + std::vector> IsSentenceOkay(const std::string& message, int gmLevel, bool whiteList = true); private: bool m_DontGenerateDCF; - std::vector m_Words; + std::vector m_DeniedWords; + std::vector m_ApprovedWords; std::vector m_UserUnapprovedWordCache; //Private functions: size_t CalculateHash(const std::string& word); - bool IsInWordlist(size_t word); }; 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(); //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 GetIsBestFriendMap() { return m_IsBestFriendMap; } + void SetIsBestFriendMap(std::unordered_map mapToSet) { m_IsBestFriendMap = mapToSet; } + bool GetIsMuted() const; time_t GetMuteExpire() const; @@ -63,6 +66,8 @@ private: std::vector m_Characters; LWOOBJID m_LoggedInCharID; + std::unordered_map m_IsBestFriendMap; + bool m_LastChatMessageApproved = false; int m_AmountOfTimesOutOfSync = 0; const int m_MaxDesyncAllowed = 12; 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..b9f715ad 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); @@ -284,6 +287,12 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa receiver.push_back(static_cast(character)); } + if (!receiver.empty()) { + 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); + } + } + stream.Read(messageLength); for (uint32_t i = 0; i < messageLength; ++i) { uint16_t character; @@ -291,16 +300,61 @@ void ClientPackets::HandleChatModerationRequest(const SystemAddress& sysAddr, Pa message.push_back(static_cast(character)); } - std::unordered_map unacceptedItems; - bool bAllClean = Game::chatFilter->IsSentenceOkay(message, user->GetLastUsedChar()->GetGMLevel()); - if (!bAllClean) { - unacceptedItems.insert(std::make_pair((char)0, (char)message.length())); + 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"); + } + + delete stmt; + delete res; + } + + 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::vector> segments = Game::chatFilter->IsSentenceOkay(message, entity->GetGMLevel(), !(isBestFriend && chatLevel == 1)); + + bool bAllClean = segments.empty(); + 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 ae8f71a4..54c925d6 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -188,25 +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 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> unacceptedItems) { + 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 - for (std::unordered_map::iterator it = unacceptedItems.begin(); it != unacceptedItems.end(); ++it) { - bitStream.Write(it->first); - bitStream.Write(it->second); - } + PacketUtils::WritePacketWString(receiver, 42, &bitStream); // receiver name - SEND_PACKET + for (auto it : unacceptedItems) { + bitStream.Write(it.first); // start index + bitStream.Write(it.second); // length + } + + for (int i = unacceptedItems.size(); 64 > i; i++) { + bitStream.Write(0); + } + + 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 unacceptedItems); + void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::vector> unacceptedItems); void SendGMLevelChange(const SystemAddress& sysAddr, bool success, uint8_t highestLevel, uint8_t prevLevel, uint8_t newLevel); } diff --git a/resources/blacklist.dcf b/resources/blacklist.dcf new file mode 100644 index 00000000..ca4db242 Binary files /dev/null and b/resources/blacklist.dcf differ