feat(chat): log each message's guild, team and filter result as it is sent

The chat server records the guild of guild chat, the team of team chat and what
the chat filter thinks of whispers, team and guild chat (delivered either way).
World servers mark filtered zone chat. Team IDs start from the chat server's
start time so a restart doesn't hand out the same team ID again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 18:22:32 -05:00
parent af54ca282f
commit ffeb742c82
3 changed files with 27 additions and 8 deletions

View File

@@ -24,6 +24,7 @@
#include "ChatPackets.h" #include "ChatPackets.h"
#include "TeamContainer.h" #include "TeamContainer.h"
#include "MinimumChatMode.h" #include "MinimumChatMode.h"
#include "dChatFilter.h"
void ChatPacketHandler::SendRouted(const LWOOBJID target, const SystemAddress& world, const LUBitStream& msg, const bool broadcast) { void ChatPacketHandler::SendRouted(const LWOOBJID target, const SystemAddress& world, const LUBitStream& msg, const bool broadcast) {
ChatPackets::WorldRoutePacket route; ChatPackets::WorldRoutePacket route;
@@ -424,20 +425,32 @@ void ChatPacketHandler::HandleShowAll(const ChatPackets::ShowAllRequest& request
// the structure the client uses to send this packet is shared in many chat messages // the structure the client uses to send this packet is shared in many chat messages
// that are sent to the server. Because of this, there are large gaps of unused data in chat messages // that are sent to the server. Because of this, there are large gaps of unused data in chat messages
namespace { namespace {
// Whispers and team chat, for moderation (log_chat=0 turns chat logging off, log_private_chat=0 just these) // Where a private message went, besides its channel
void LogChat(const PlayerData& sender, const std::string& channel, const std::string& message, LWOOBJID recipientId = LWOOBJID_EMPTY, const std::string& recipientName = "") { struct ChatScope {
LWOOBJID recipientId = LWOOBJID_EMPTY; // whispers
std::string recipientName;
int64_t guildId{}; // guild chat
LWOOBJID teamId{}; // team chat
};
// Whispers, team and guild chat, for moderation (log_chat=0 turns chat logging off, log_private_chat=0 just these).
// Written as the message is sent, with what the chat filter thinks of it: these are delivered either way.
void LogChat(const PlayerData& sender, const std::string& channel, const std::string& message, const ChatScope& scope = {}) {
if (Game::config->GetValue("log_chat") == "0" || Game::config->GetValue("log_private_chat") == "0") return; if (Game::config->GetValue("log_chat") == "0" || Game::config->GetValue("log_private_chat") == "0") return;
IChatLog::ChatMessage entry; IChatLog::ChatMessage entry;
entry.time = static_cast<int64_t>(std::time(nullptr)); entry.time = static_cast<int64_t>(std::time(nullptr));
entry.channel = channel; entry.channel = channel;
entry.senderId = sender.playerID; entry.senderId = sender.playerID;
entry.senderName = sender.playerName; entry.senderName = sender.playerName;
entry.recipientId = recipientId == LWOOBJID_EMPTY ? 0 : recipientId; entry.recipientId = scope.recipientId == LWOOBJID_EMPTY ? 0 : scope.recipientId;
entry.recipientName = recipientName; entry.recipientName = scope.recipientName;
entry.guildId = scope.guildId;
entry.teamId = scope.teamId == LWOOBJID_EMPTY ? 0 : scope.teamId;
entry.zoneId = sender.zoneID.GetMapID(); entry.zoneId = sender.zoneID.GetMapID();
entry.instanceId = sender.zoneID.GetInstanceID(); entry.instanceId = sender.zoneID.GetInstanceID();
entry.cloneId = sender.zoneID.GetCloneID(); entry.cloneId = sender.zoneID.GetCloneID();
entry.message = message; entry.message = message;
entry.filtered = Game::chatFilter && !Game::chatFilter->IsSentenceOkay(message, sender.gmLevel).empty();
try { try {
if (const auto info = Database::Get()->GetCharacterInfo(sender.playerID)) entry.accountId = info->accountId; if (const auto info = Database::Get()->GetCharacterInfo(sender.playerID)) entry.accountId = info->accountId;
Database::Get()->InsertChatMessage(entry); Database::Get()->InsertChatMessage(entry);
@@ -463,7 +476,7 @@ void ChatPacketHandler::HandleChatMessage(const ChatPackets::GeneralChatMessage&
case eChatChannel::TEAM: { case eChatChannel::TEAM: {
auto* team = TeamContainer::GetTeam(playerID); auto* team = TeamContainer::GetTeam(playerID);
if (team == nullptr) return; if (team == nullptr) return;
LogChat(sender, "team", message.GetAsString()); LogChat(sender, "team", message.GetAsString(), { .teamId = team->teamID });
for (const auto memberId : team->memberIDs) { for (const auto memberId : team->memberIDs) {
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
@@ -476,7 +489,8 @@ void ChatPacketHandler::HandleChatMessage(const ChatPackets::GeneralChatMessage&
// Sent by the world for /g, which is what the client's guild chat tab sends // Sent by the world for /g, which is what the client's guild chat tab sends
const auto members = ChatGuilds::Get().OnlineGuildmates(playerID); const auto members = ChatGuilds::Get().OnlineGuildmates(playerID);
if (members.empty()) return; if (members.empty()) return;
LogChat(sender, "guild", message.GetAsString()); const auto guildMember = Database::Get()->GetGuildMember(playerID);
LogChat(sender, "guild", message.GetAsString(), { .guildId = guildMember ? guildMember->guildId : 0 });
for (const auto memberId : members) { for (const auto memberId : members) {
const auto& member = Game::playerContainer.GetPlayerData(memberId); const auto& member = Game::playerContainer.GetPlayerData(memberId);
if (member) SendPrivateChatMessage(sender, member, member, message, eChatChannel::GUILD, eChatMessageResponseCode::SENT); if (member) SendPrivateChatMessage(sender, member, member, message, eChatChannel::GUILD, eChatMessageResponseCode::SENT);
@@ -522,7 +536,7 @@ void ChatPacketHandler::HandlePrivateChatMessage(const ChatPackets::PrivateChatM
// only freinds can whispr each other // only freinds can whispr each other
for (const auto& fr : receiver.friends) { for (const auto& fr : receiver.friends) {
if (fr.friendID == sender.playerID) { if (fr.friendID == sender.playerID) {
LogChat(sender, "whisper", message.GetAsString(), receiver.playerID, receiver.playerName); LogChat(sender, "whisper", message.GetAsString(), { .recipientId = receiver.playerID, .recipientName = receiver.playerName });
//To the sender: //To the sender:
SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::PRIVATE_CHAT, eChatMessageResponseCode::SENT); SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::PRIVATE_CHAT, eChatMessageResponseCode::SENT);
//To the receiver: //To the receiver:

View File

@@ -1,5 +1,7 @@
#include "TeamContainer.h" #include "TeamContainer.h"
#include <ctime>
#include "ChatPackets.h" #include "ChatPackets.h"
#include "MessageType/Chat.h" #include "MessageType/Chat.h"
@@ -10,7 +12,9 @@
namespace { namespace {
TeamContainer::Data g_TeamContainer{}; TeamContainer::Data g_TeamContainer{};
LWOOBJID g_TeamIDCounter = 0; // Starts from the time the chat server started (in millionths of a second), so a team's ID is not given out again
// after a restart: the chat log tells teams apart by it
LWOOBJID g_TeamIDCounter = static_cast<LWOOBJID>(std::time(nullptr)) * 1000000;
} }
ChatHandoff TeamContainer::MakeHandoff() { ChatHandoff TeamContainer::MakeHandoff() {

View File

@@ -1575,6 +1575,7 @@ namespace {
entry.cloneId = Game::zoneManager->GetZoneID().GetCloneID(); entry.cloneId = Game::zoneManager->GetZoneID().GetCloneID();
entry.message = GeneralUtils::UTF16ToWTF8(message); entry.message = GeneralUtils::UTF16ToWTF8(message);
entry.blocked = !isOk; entry.blocked = !isOk;
entry.filtered = !isOk;
try { try {
Database::Get()->InsertChatMessage(entry); Database::Get()->InsertChatMessage(entry);
} catch (const std::exception& ex) { } catch (const std::exception& ex) {