[Server/Client/Protocol] Address developer role review feedback

Address ZeizaZach's review of the developer staff role:

- Nudge the developer log query to exclude private chat and sender IPs
  (the ModeratorCommand path still sees everything).
- Deduplicate Command_GetLogHistory into Command_ViewLogHistory, which now
  extends both ModeratorCommand (ext) and DeveloperCommand (dev_ext); the
  client picks the DeveloperCommand-scoped extension by extendee, and the
  server reads it via the extension number.
- Pull the uptime snapshot SQL into Servatrice_DatabaseInterface as
  getLatestUptimeSnapshot() and widen the reported counters to 64-bit.
- Document the admin bitfield (1 admin, 2 moderator, 4 judge, 8 developer)
  and add a server-side test for the developer command path.
This commit is contained in:
Lukas Brübach 2026-08-30 21:06:31 +02:00
parent 1ff7ffcaa4
commit 1dc20176b3
14 changed files with 251 additions and 76 deletions

View file

@ -1487,6 +1487,38 @@ QList<ServerInfo_ModeratorLogin> Servatrice_DatabaseInterface::getModeratorLastL
return results;
}
Servatrice_DatabaseInterface::UptimeSnapshot Servatrice_DatabaseInterface::getLatestUptimeSnapshot(int serverId)
{
UptimeSnapshot snapshot;
if (!checkSql()) {
return snapshot;
}
QSqlQuery *query = prepareQuery("SELECT users_count, mods_count, games_count, tx_bytes, rx_bytes, uptime, "
"UNIX_TIMESTAMP(timest) FROM {prefix}_uptime "
"WHERE id_server = :id_server ORDER BY timest DESC LIMIT 1");
query->bindValue(":id_server", serverId);
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog) << "Failed to collect server stats snapshot: SQL Error";
return snapshot;
}
if (query->next()) {
snapshot.valid = true;
snapshot.usersCount = query->value(0).toULongLong();
snapshot.modsCount = query->value(1).toULongLong();
snapshot.gamesCount = query->value(2).toULongLong();
snapshot.txBytes = query->value(3).toULongLong();
snapshot.rxBytes = query->value(4).toULongLong();
snapshot.uptimeSecs = query->value(5).toULongLong();
snapshot.timest = query->value(6).toULongLong();
}
return snapshot;
}
bool Servatrice_DatabaseInterface::removeUserAvatar(const QString &userName)
{
if (!checkSql()) {

View file

@ -140,6 +140,21 @@ public:
QList<ServerInfo_UserSession> getUserSessions(const QString &userName, int limit);
QList<ServerInfo_UserAlt> getUserAlts(const QString &userName);
QList<ServerInfo_ModeratorLogin> getModeratorLastLogins();
// Uptime snapshot as recorded by Servatrice::statusUpdate() into the
// {prefix}_uptime table. valid is false when no snapshot exists yet.
struct UptimeSnapshot
{
bool valid = false;
quint64 usersCount = 0;
quint64 modsCount = 0;
quint64 gamesCount = 0;
quint64 txBytes = 0;
quint64 rxBytes = 0;
quint64 uptimeSecs = 0;
quint64 timest = 0;
};
UptimeSnapshot getLatestUptimeSnapshot(int serverId);
bool removeUserAvatar(const QString &userName);
bool addForgotPassword(const QString &user);
bool removeForgotPassword(const QString &user) override;

View file

@ -287,7 +287,7 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo
case ModeratorCommand::REPORT_RESOLVE:
return cmdReportResolve(cmd.GetExtension(Command_ReportResolve::ext), rc);
case ModeratorCommand::VIEWLOG_HISTORY:
return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc);
return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc, true);
case ModeratorCommand::GRANT_REPLAY_ACCESS:
return cmdGrantReplayAccess(cmd.GetExtension(Command_GrantReplayAccess::ext), rc);
case ModeratorCommand::REPLAY_DOWNLOAD_BY_GAME_ID:
@ -351,21 +351,9 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedDeveloperCo
case DeveloperCommand::GET_SERVER_STATS:
return cmdGetServerStats(cmd.GetExtension(Command_GetServerStats::ext), rc);
case DeveloperCommand::VIEWLOG_HISTORY: {
// Same query as the moderator log view, just carried by the
// developer command family.
const Command_GetLogHistory &devCmd = cmd.GetExtension(Command_GetLogHistory::ext);
Command_ViewLogHistory modCmd;
modCmd.set_user_name(devCmd.user_name());
modCmd.set_ip_address(devCmd.ip_address());
modCmd.set_game_name(devCmd.game_name());
modCmd.set_game_id(devCmd.game_id());
modCmd.set_message(devCmd.message());
for (int i = 0; i < devCmd.log_location_size(); ++i) {
modCmd.add_log_location(devCmd.log_location(i));
}
modCmd.set_date_range(devCmd.date_range());
modCmd.set_maximum_results(devCmd.maximum_results());
return cmdGetLogHistory(modCmd, rc);
// Same query as the moderator log view, carried by the developer
// command family, but narrows out private chats and sender IPs.
return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::dev_ext), rc, false);
}
default:
return Response::RespFunctionNotAllowed;
@ -1055,12 +1043,13 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const
// MODERATOR FUNCTIONS.
// May be called by admins and moderators. Permission is checked by the calling function.
Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Command_ViewLogHistory &cmd,
ResponseContainer &rc)
ResponseContainer &rc,
bool allowPrivateChat)
{
QList<ServerInfo_ChatMessage> messageList;
QString userName = nameFromStdString(cmd.user_name());
QString ipAddress = nameFromStdString(cmd.ip_address());
QString ipAddress = allowPrivateChat ? nameFromStdString(cmd.ip_address()) : QString();
QString gameName = nameFromStdString(cmd.game_name());
QString gameID = nameFromStdString(cmd.game_id());
QString message = textFromStdString(cmd.message());
@ -1075,7 +1064,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com
if (nameFromStdString(cmd.log_location(i)).simplified() == "game") {
gameType = true;
}
if (nameFromStdString(cmd.log_location(i)).simplified() == "chat") {
if (nameFromStdString(cmd.log_location(i)).simplified() == "chat" && allowPrivateChat) {
chatType = true;
}
}
@ -1089,7 +1078,11 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com
QListIterator<ServerInfo_ChatMessage> messageIterator(sqlInterface->getMessageLogHistory(
userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults));
while (messageIterator.hasNext()) {
re->add_log_message()->CopyFrom(messageIterator.next());
ServerInfo_ChatMessage chatMessage = messageIterator.next();
if (!allowPrivateChat) {
chatMessage.clear_sender_ip();
}
re->add_log_message()->CopyFrom(chatMessage);
}
} else {
ServerInfo_ChatMessage chatMessage;
@ -1687,24 +1680,20 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetServerStats(const Co
// Servatrice::statusUpdate() periodically snapshots server health into the
// uptime table. Serve the freshest snapshot for this server.
QSqlQuery *query = sqlInterface->prepareQuery(
"SELECT users_count, mods_count, games_count, tx_bytes, rx_bytes, uptime, UNIX_TIMESTAMP(timest) "
"FROM {prefix}_uptime WHERE id_server = :id_server ORDER BY timest DESC LIMIT 1");
query->bindValue(":id_server", servatrice->getServerID());
if (!sqlInterface->execSqlQuery(query)) {
const auto snapshot = sqlInterface->getLatestUptimeSnapshot(servatrice->getServerID());
if (!snapshot.valid) {
// No snapshot yet (fresh server, or statusUpdate() has not ticked).
return Response::RespInternalError;
}
auto *re = new Response_GetServerStats;
if (query->next()) {
re->set_users_count(query->value(0).toUInt());
re->set_mods_count(query->value(1).toUInt());
re->set_games_count(query->value(2).toUInt());
re->set_tx_bytes(query->value(3).toUInt());
re->set_rx_bytes(query->value(4).toUInt());
re->set_uptime_secs(query->value(5).toUInt());
re->set_timest(query->value(6).toUInt());
}
re->set_users_count(snapshot.usersCount);
re->set_mods_count(snapshot.modsCount);
re->set_games_count(snapshot.gamesCount);
re->set_tx_bytes(snapshot.txBytes);
re->set_rx_bytes(snapshot.rxBytes);
re->set_uptime_secs(snapshot.uptimeSecs);
re->set_timest(snapshot.timest);
rc.setResponseExtension(re);
return Response::RespOk;

View file

@ -117,7 +117,8 @@ private:
Response::ResponseCode cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer &rc);
Response::ResponseCode cmdReportList(const Command_ReportList &cmd, ResponseContainer &rc);
Response::ResponseCode cmdWarnUser(const Command_WarnUser &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc);
Response::ResponseCode
cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc, bool allowPrivateChat);
Response::ResponseCode cmdGetBanHistory(const Command_GetBanHistory &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetWarnList(const Command_GetWarnList &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetWarnHistory(const Command_GetWarnHistory &cmd, ResponseContainer &rc);