[Server/Client/Protocol] Add developer staff role

Introduce a Developer staff level (proto flag 32, DB admin bit 8) that
sits between admin and moderator: no kick/ban/warn/report/admin powers,
but gets server log access via a new developer command container family
(GET_SERVER_STATS, VIEWLOG_HISTORY) and an idle-timeout exemption.

- Protocol: IsDeveloper flag, developer_commands.proto envelope,
  Command_GetServerStats/Command_GetLogHistory, Response_GetServerStats,
  Command_AdjustMod.should_be_developer
- Servatrice: fail-closed developer dispatcher, uptime snapshot handler,
  shared log history handler reuse, bit-8 DB mapping
- Client: burgundy pawn/badge/labels/sort order, prepareDeveloperCommand,
  minimal Developer stats tab, log tab access, promote/demote actions

Took 24 minutes

Took 18 seconds
This commit is contained in:
Lukas Brübach 2026-08-23 19:40:16 +02:00
parent 7d867b9745
commit 1ff7ffcaa4
31 changed files with 515 additions and 23 deletions

View file

@ -47,6 +47,8 @@
#include <libcockatrice/protocol/pb/command_deck_list.pb.h>
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
#include <libcockatrice/protocol/pb/command_get_log_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>
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
@ -80,6 +82,7 @@
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
#include <libcockatrice/protocol/pb/response_forgotpasswordrequest.pb.h>
#include <libcockatrice/protocol/pb/response_get_admin_notes.pb.h>
#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_register.pb.h>
@ -337,6 +340,38 @@ AbstractServerSocketInterface::processExtendedAdminCommand(int cmdType, const Ad
}
}
// DEVELOPER FUNCTIONS.
// Permission is checked by processDeveloperCommandContainer. Only stats-style
// queries live here, never community moderation or server administration.
Response::ResponseCode AbstractServerSocketInterface::processExtendedDeveloperCommand(int cmdType,
const DeveloperCommand &cmd,
ResponseContainer &rc)
{
switch ((DeveloperCommand::DeveloperCommandType)cmdType) {
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);
}
default:
return Response::RespFunctionNotAllowed;
}
}
Response::ResponseCode AbstractServerSocketInterface::cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc)
{
if (authState != PasswordRight) {
@ -1643,6 +1678,38 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportUserInfo(const Co
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdGetServerStats(const Command_GetServerStats & /*cmd */,
ResponseContainer &rc)
{
if (!sqlInterface->checkSql()) {
return Response::RespInternalError;
}
// 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)) {
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());
}
rc.setResponseExtension(re);
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdReportStats(const Command_ReportStats & /*cmd */,
ResponseContainer &rc)
{
@ -3215,7 +3282,7 @@ bool AbstractServerSocketInterface::removeAdminFlagFromUser(const QString &userN
if (user) {
Event_ConnectionClosed event;
event.set_reason(Event_ConnectionClosed::DEMOTED);
event.set_reason_str("Your moderator and/or judge status has been revoked.");
event.set_reason_str("Your moderator, judge, and/or developer status has been revoked.");
event.set_end_time(QDateTime::currentDateTime().toSecsSinceEpoch());
SessionEvent *se = user->prepareSessionEvent(event);
@ -3257,6 +3324,18 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAdjustMod(const Command
}
}
if (cmd.has_should_be_developer()) {
if (cmd.should_be_developer()) {
if (!addAdminFlagToUser(userName, 8)) {
return Response::RespInternalError;
}
} else {
if (!removeAdminFlagFromUser(userName, 8)) {
return Response::RespInternalError;
}
}
}
return Response::RespOk;
}