mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-10 20:13:24 -08:00
[Server] Let users fetch their own private chat history
Players lose their private conversations on reconnect or on another device. Servatrice can already log private messages, but only moderators can read the logs. Add Command_GetPrivateChatHistory / Response_PrivateChatHistory so a registered user can page through their own logged conversation with one other user. Operators opt in with logging/allow_user_chat_history (default off, and log_user_msg_chat must be on). The caller is matched by account id so reused guest or deleted-account names can't read older messages; only time, sender name and text are returned. Pages hold 1..200 messages and never split a second, since log_time has 1-second resolution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9793a0df4a
commit
d9852e13b4
11 changed files with 189 additions and 0 deletions
|
|
@ -33,6 +33,7 @@ set(PROTO_FILES
|
|||
command_dump_zone.proto
|
||||
command_flip_card.proto
|
||||
command_game_say.proto
|
||||
command_get_private_chat_history.proto
|
||||
command_get_server_stats.proto
|
||||
command_inc_card_counter.proto
|
||||
command_inc_counter.proto
|
||||
|
|
@ -164,6 +165,7 @@ set(PROTO_FILES
|
|||
response_list_users.proto
|
||||
response_login.proto
|
||||
response_password_salt.proto
|
||||
response_private_chat_history.proto
|
||||
response_register.proto
|
||||
response_replay_download.proto
|
||||
response_replay_download_by_game_id.proto
|
||||
|
|
@ -202,6 +204,7 @@ set(PROTO_FILES
|
|||
serverinfo_player.proto
|
||||
serverinfo_playerping.proto
|
||||
serverinfo_playerproperties.proto
|
||||
serverinfo_private_chat_message.proto
|
||||
serverinfo_replay.proto
|
||||
serverinfo_replay_match.proto
|
||||
serverinfo_report.proto
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
syntax = "proto2";
|
||||
import "session_commands.proto";
|
||||
|
||||
/**
|
||||
* Fetch the caller's private conversation with user_name.
|
||||
* user_name must be an active registered account; guest and deleted-account
|
||||
* names have no stable account id, so they are rejected with RespNameNotFound.
|
||||
* before is exclusive Unix seconds; zero or unset applies no upper time bound.
|
||||
* limit is clamped to 1..200; a whole second may exceed it to preserve pagination.
|
||||
*/
|
||||
message Command_GetPrivateChatHistory {
|
||||
extend SessionCommand {
|
||||
optional Command_GetPrivateChatHistory ext = 1035;
|
||||
}
|
||||
optional string user_name = 1;
|
||||
optional uint64 before = 2;
|
||||
optional uint32 limit = 3 [default = 50];
|
||||
}
|
||||
|
|
@ -78,6 +78,7 @@ message Response {
|
|||
PASSWORD_SALT = 1017; // Response containing password salt
|
||||
GET_ADMIN_NOTES = 1018; // Response with admin notes
|
||||
GET_SERVER_STATS = 1019; // Response with server status statistics
|
||||
PRIVATE_CHAT_HISTORY = 1020; // Response with the caller's private conversation
|
||||
REPLAY_LIST = 1100; // Response listing replays
|
||||
REPLAY_DOWNLOAD = 1101; // Response for replay download
|
||||
REPLAY_GET_CODE = 1102; // Response containing replay code
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
import "serverinfo_private_chat_message.proto";
|
||||
|
||||
/**
|
||||
* Private conversation messages, oldest first, without account IDs or IP addresses.
|
||||
* When has_more is true, use the oldest returned time as before on the next request.
|
||||
* Seconds are never split across pages; ordering within a second is unspecified.
|
||||
*/
|
||||
message Response_PrivateChatHistory {
|
||||
extend Response {
|
||||
optional Response_PrivateChatHistory ext = 1020;
|
||||
}
|
||||
repeated ServerInfo_PrivateChatMessage messages = 1;
|
||||
optional bool has_more = 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
syntax = "proto2";
|
||||
|
||||
/**
|
||||
* A private conversation message with its time in Unix seconds.
|
||||
*/
|
||||
message ServerInfo_PrivateChatMessage {
|
||||
optional uint64 time = 1;
|
||||
optional string sender_name = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ message SessionCommand {
|
|||
DECK_SHARE_LIST_MINE = 1032;
|
||||
DECK_SHARE_REMOVE = 1033;
|
||||
SUBMIT_PASSWORD_VERIFIER = 1034;
|
||||
GET_PRIVATE_CHAT_HISTORY = 1035;
|
||||
REPLAY_LIST = 1100;
|
||||
REPLAY_DOWNLOAD = 1101;
|
||||
REPLAY_MODIFY_MATCH = 1102;
|
||||
|
|
|
|||
|
|
@ -534,6 +534,10 @@ log_user_msg_chat=false
|
|||
; Log user messages coming from other servers in the network
|
||||
log_user_msg_isl=false
|
||||
|
||||
; Allow registered users to read their retained private messages; requires log_user_msg_chat=true
|
||||
; Privacy: exposes previously logged conversations to their participants on any device
|
||||
allow_user_chat_history=false
|
||||
|
||||
[audit]
|
||||
|
||||
; Servatrice can record certain actions being performed in the database for server operators to better understand
|
||||
|
|
|
|||
|
|
@ -2638,6 +2638,87 @@ int Servatrice_DatabaseInterface::clearBlacklistedBanners()
|
|||
return cleared;
|
||||
}
|
||||
|
||||
bool Servatrice_DatabaseInterface::getPrivateChatHistory(int callerId,
|
||||
int peerId,
|
||||
quint64 before,
|
||||
int limit,
|
||||
QList<ServerInfo_PrivateChatMessage> &messages,
|
||||
bool &hasMore)
|
||||
{
|
||||
messages.clear();
|
||||
hasMore = false;
|
||||
if (callerId < 1 || peerId < 1 || !checkSql()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Both participants are matched by account id, never by name: names can be
|
||||
// reused by guests or re-registered after deletion, and matching by name
|
||||
// would merge conversations from different people.
|
||||
const QString conversationFilter = " FROM {prefix}_log WHERE target_type = 'chat'"
|
||||
" AND ((sender_id = :caller_sender_id AND target_id = :peer_target_id)"
|
||||
" OR (target_id = :caller_target_id AND sender_id = :peer_sender_id))";
|
||||
const QString selectMessages = "SELECT UNIX_TIMESTAMP(log_time), sender_name, log_message";
|
||||
const auto bindParticipants = [&](QSqlQuery *query) {
|
||||
query->bindValue(":caller_sender_id", callerId);
|
||||
query->bindValue(":peer_target_id", peerId);
|
||||
query->bindValue(":peer_sender_id", peerId);
|
||||
query->bindValue(":caller_target_id", callerId);
|
||||
};
|
||||
const auto readMessages = [&](QSqlQuery *query) {
|
||||
while (query->next()) {
|
||||
ServerInfo_PrivateChatMessage message;
|
||||
message.set_time(query->value(0).toULongLong());
|
||||
message.set_sender_name(query->value(1).toString().toStdString());
|
||||
message.set_message(query->value(2).toString().toStdString());
|
||||
messages.append(message);
|
||||
}
|
||||
};
|
||||
|
||||
QString queryString = selectMessages + conversationFilter;
|
||||
if (before) {
|
||||
queryString.append(" AND log_time < FROM_UNIXTIME(:before)");
|
||||
}
|
||||
queryString.append(" ORDER BY log_time DESC LIMIT :limit_size");
|
||||
QSqlQuery *query = prepareQuery(queryString);
|
||||
bindParticipants(query);
|
||||
if (before) {
|
||||
query->bindValue(":before", before);
|
||||
}
|
||||
query->bindValue(":limit_size", limit + 1);
|
||||
if (!execSqlQuery(query)) {
|
||||
return false;
|
||||
}
|
||||
readMessages(query);
|
||||
|
||||
hasMore = messages.size() > limit;
|
||||
if (hasMore) {
|
||||
const quint64 boundary = messages.at(limit).time();
|
||||
while (!messages.isEmpty() && messages.last().time() == boundary) {
|
||||
messages.removeLast();
|
||||
}
|
||||
if (messages.isEmpty()) {
|
||||
query = prepareQuery(selectMessages + conversationFilter + " AND log_time = FROM_UNIXTIME(:second)");
|
||||
bindParticipants(query);
|
||||
query->bindValue(":second", boundary);
|
||||
if (!execSqlQuery(query)) {
|
||||
return false;
|
||||
}
|
||||
readMessages(query);
|
||||
|
||||
query = prepareQuery("SELECT 1" + conversationFilter + " AND log_time < FROM_UNIXTIME(:before) LIMIT 1");
|
||||
bindParticipants(query);
|
||||
query->bindValue(":before", boundary);
|
||||
if (!execSqlQuery(query)) {
|
||||
return false;
|
||||
}
|
||||
hasMore = query->next();
|
||||
}
|
||||
}
|
||||
|
||||
std::reverse(messages.begin(), messages.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory(const QString &user,
|
||||
const QString &ipaddress,
|
||||
const QString &gamename,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <libcockatrice/protocol/pb/serverinfo_ban.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_chat_message.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_moderator_login.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_private_chat_message.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user_alt.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user_session.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_warning.pb.h>
|
||||
|
|
@ -224,6 +225,12 @@ public:
|
|||
const QString &beforeTime = QString(),
|
||||
bool *hasMore = nullptr);
|
||||
QList<ServerInfo_UserSession> getUserSessions(const QString &userName, int limit);
|
||||
bool getPrivateChatHistory(int callerId,
|
||||
int peerId,
|
||||
quint64 before,
|
||||
int limit,
|
||||
QList<ServerInfo_PrivateChatMessage> &messages,
|
||||
bool &hasMore);
|
||||
QList<ServerInfo_UserAlt> getUserAlts(const QString &userName,
|
||||
int depth = 1,
|
||||
bool useEmail = true,
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_deck_share_list_mine.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_share_remove.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_get_private_chat_history.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_get_server_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_replay_delete_match.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_replay_download.pb.h>
|
||||
|
|
@ -139,6 +140,7 @@
|
|||
#include <libcockatrice/protocol/pb/response_get_server_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_moderator_last_logins.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_password_salt.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_private_chat_history.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_register.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_remove_user_avatar.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_remove_user_banner.pb.h>
|
||||
|
|
@ -414,6 +416,8 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm
|
|||
return cmdDeckShareRemove(cmd.GetExtension(Command_DeckShareRemove::ext), rc);
|
||||
case SessionCommand::DECK_SHARE_DOWNLOAD:
|
||||
return cmdDeckShareDownload(cmd.GetExtension(Command_DeckShareDownload::ext), rc);
|
||||
case SessionCommand::GET_PRIVATE_CHAT_HISTORY:
|
||||
return cmdGetPrivateChatHistory(cmd.GetExtension(Command_GetPrivateChatHistory::ext), rc);
|
||||
case SessionCommand::ACCOUNT_PASSWORD:
|
||||
return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc);
|
||||
case SessionCommand::REQUEST_PASSWORD_SALT:
|
||||
|
|
@ -1702,6 +1706,48 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const
|
|||
return Response::RespOk;
|
||||
}
|
||||
|
||||
Response::ResponseCode AbstractServerSocketInterface::cmdGetPrivateChatHistory(const Command_GetPrivateChatHistory &cmd,
|
||||
ResponseContainer &rc)
|
||||
{
|
||||
if (authState != PasswordRight) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
if (!userInfo || !(userInfo->user_level() & ServerInfo_User::IsRegistered)) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
if (!settingsCache->value("logging/log_user_msg_chat", false).toBool() ||
|
||||
!settingsCache->value("logging/allow_user_chat_history", false).toBool()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
const QString peer = nameFromStdString(cmd.user_name());
|
||||
if (peer.isEmpty()) {
|
||||
return Response::RespInvalidData;
|
||||
}
|
||||
|
||||
// Resolve the peer to an account id. Matching by name alone would merge the
|
||||
// conversations of everyone who has ever used that name.
|
||||
const int peerId = sqlInterface->getUserIdInDB(peer);
|
||||
if (peerId < 1) {
|
||||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
const int limit = qBound<quint32>(1, cmd.limit(), 200);
|
||||
QList<ServerInfo_PrivateChatMessage> messages;
|
||||
bool hasMore = false;
|
||||
if (!sqlInterface->getPrivateChatHistory(userInfo->id(), peerId, cmd.before(), limit, messages, hasMore)) {
|
||||
return Response::RespInternalError;
|
||||
}
|
||||
|
||||
Response_PrivateChatHistory *re = new Response_PrivateChatHistory;
|
||||
for (const auto &message : messages) {
|
||||
re->add_messages()->CopyFrom(message);
|
||||
}
|
||||
re->set_has_more(hasMore);
|
||||
rc.setResponseExtension(re);
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
||||
// MODERATOR FUNCTIONS.
|
||||
// May be called by admins and moderators. Permission is checked by the calling function.
|
||||
Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Command_ViewLogHistory &cmd,
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class Command_ForgotPasswordReset;
|
|||
class Command_GetAdminNotes;
|
||||
class Command_GetBanHistory;
|
||||
class Command_GetModeratorLastLogins;
|
||||
class Command_GetPrivateChatHistory;
|
||||
class Command_GetServerStats;
|
||||
class Command_GetUserAlts;
|
||||
class Command_GetUserSessions;
|
||||
|
|
@ -142,6 +143,7 @@ private:
|
|||
|
||||
Response::ResponseCode cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc);
|
||||
Response::ResponseCode cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc);
|
||||
Response::ResponseCode cmdGetPrivateChatHistory(const Command_GetPrivateChatHistory &cmd, ResponseContainer &rc);
|
||||
int getDeckPathId(int basePathId, QStringList path);
|
||||
int getDeckPathId(const QString &path);
|
||||
bool deckListHelper(int folderId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue