mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Guild chat per guild (/chat_log/guild/<id>, from the guild's card), team chat by team (/chat_log/teams) and a character's whispers by conversation (/characters/<id>/whispers, chat_dms; opening one is audited), paged on the server. The Chat Log filters by account and time range, shows the conversation around a message, links to its history and marks flagged messages; messages can be picked (shift-click for a range) to flag. The character page links to all of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
4.8 KiB
C++
122 lines
4.8 KiB
C++
#include "ChatHistory.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace ChatHistory {
|
|
bool CanRead(const std::string& channel, const Access& access) {
|
|
if (channel == "zone" || channel == "web") return true;
|
|
if (channel == "team" || channel == "guild") return access.group;
|
|
if (channel == "whisper") return access.whispers;
|
|
return false;
|
|
}
|
|
|
|
nlohmann::json MessageJson(const IChatLog::ChatMessage& m, const Access& access) {
|
|
const bool readable = CanRead(m.channel, access);
|
|
return { {"id", m.id}, {"time", m.time}, {"channel", m.channel}, {"sender_id", std::to_string(m.senderId)}, {"sender_name", m.senderName},
|
|
{"account_id", m.accountId}, {"recipient_id", std::to_string(m.recipientId)}, {"recipient_name", m.recipientName},
|
|
{"zone_id", m.zoneId}, {"instance_id", m.instanceId}, {"clone_id", m.cloneId}, {"message", readable ? m.message : ""},
|
|
{"blocked", m.blocked}, {"filtered", m.filtered}, {"guild_id", std::to_string(m.guildId)}, {"team_id", std::to_string(m.teamId)},
|
|
{"redacted", !readable} };
|
|
}
|
|
|
|
IChatLog::ChatQuery ConversationQuery(const IChatLog::ChatMessage& m, const Access& access) {
|
|
IChatLog::ChatQuery q;
|
|
q.includePrivate = access.group;
|
|
q.includeWhispers = access.whispers;
|
|
q.channel = m.channel;
|
|
if (m.channel == "zone") {
|
|
q.zoneId = m.zoneId;
|
|
q.instanceId = m.instanceId;
|
|
// Zone 0 would mean any zone
|
|
if (m.zoneId == 0) q.channel = "none";
|
|
} else if (m.channel == "whisper") {
|
|
q.characterId = m.senderId;
|
|
q.otherCharacterId = m.recipientId;
|
|
if (m.senderId == 0 || m.recipientId == 0) q.channel = "none";
|
|
} else if (m.channel == "team") {
|
|
q.teamId = m.teamId;
|
|
if (m.teamId == 0) q.channel = "none"; // logged before teams were recorded: no way to tell them apart
|
|
} else if (m.channel == "guild") {
|
|
q.guildId = m.guildId;
|
|
if (m.guildId == 0) q.channel = "none";
|
|
}
|
|
return q;
|
|
}
|
|
|
|
bool SameConversation(const IChatLog::ChatMessage& a, const IChatLog::ChatMessage& b) {
|
|
if (a.channel != b.channel) return false;
|
|
if (a.channel == "zone") return a.zoneId == b.zoneId && a.instanceId == b.instanceId;
|
|
if (a.channel == "whisper") {
|
|
return (a.senderId == b.senderId && a.recipientId == b.recipientId) || (a.senderId == b.recipientId && a.recipientId == b.senderId);
|
|
}
|
|
if (a.channel == "team") return a.teamId == b.teamId;
|
|
if (a.channel == "guild") return a.guildId == b.guildId;
|
|
return true; // web
|
|
}
|
|
|
|
std::optional<std::string> ParseStatus(const std::string& status) {
|
|
if (status == "open" || status == "actioned" || status == "dismissed") return status;
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::string StatusAction(const std::string& from, const std::string& to) {
|
|
if (from == to) return "";
|
|
if (to == "open") return "reopened";
|
|
return to;
|
|
}
|
|
|
|
std::string Excerpt(const std::vector<IChatLog::ChatMessage>& messages, const size_t maxBytes) {
|
|
std::string text;
|
|
for (const auto& m : messages) {
|
|
if (!text.empty()) text += " / ";
|
|
text += m.senderName + ": " + m.message;
|
|
}
|
|
if (text.size() <= maxBytes) return text;
|
|
const std::string ellipsis = "\xE2\x80\xA6"; // …
|
|
size_t cut = maxBytes >= ellipsis.size() ? maxBytes - ellipsis.size() : 0;
|
|
// Back up to the start of a UTF-8 character
|
|
while (cut > 0 && (static_cast<unsigned char>(text[cut]) & 0xC0) == 0x80) cut--;
|
|
return text.substr(0, cut) + ellipsis;
|
|
}
|
|
|
|
nlohmann::json Snapshot(const std::vector<IChatLog::ChatMessage>& before, const std::vector<IChatLog::ChatMessage>& flagged,
|
|
const std::vector<IChatLog::ChatMessage>& after) {
|
|
const Access all{ true, true };
|
|
nlohmann::json out = nlohmann::json::array();
|
|
const auto add = [&](const std::vector<IChatLog::ChatMessage>& list, bool isFlagged) {
|
|
for (const auto& m : list) {
|
|
auto json = MessageJson(m, all);
|
|
json.erase("redacted");
|
|
json["flagged"] = isFlagged;
|
|
out.push_back(std::move(json));
|
|
}
|
|
};
|
|
add(before, false);
|
|
add(flagged, true);
|
|
add(after, false);
|
|
std::stable_sort(out.begin(), out.end(), [](const nlohmann::json& a, const nlohmann::json& b) { return a.value("id", uint64_t{}) < b.value("id", uint64_t{}); });
|
|
return out;
|
|
}
|
|
|
|
nlohmann::json RedactSnapshot(const nlohmann::json& snapshot, const Access& access) {
|
|
nlohmann::json out = nlohmann::json::array();
|
|
if (!snapshot.is_array()) return out;
|
|
for (auto message : snapshot) {
|
|
if (!message.is_object()) continue;
|
|
const bool readable = CanRead(message.value("channel", ""), access);
|
|
if (!readable) message["message"] = "";
|
|
message["redacted"] = !readable;
|
|
out.push_back(std::move(message));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::optional<IChatLog::ChatMessage> Subject(const std::vector<IChatLog::ChatMessage>& flagged, const LWOOBJID chosen) {
|
|
if (chosen != 0) {
|
|
for (const auto& m : flagged) if (m.senderId == chosen) return m;
|
|
}
|
|
for (const auto& m : flagged) if (m.senderId != 0) return m;
|
|
return std::nullopt;
|
|
}
|
|
}
|