[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

@ -253,3 +253,11 @@ PendingCommand *AbstractClient::prepareAdminCommand(const ::google::protobuf::Me
c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd);
return new PendingCommand(cont);
}
PendingCommand *AbstractClient::prepareDeveloperCommand(const ::google::protobuf::Message &cmd)
{
CommandContainer cont;
DeveloperCommand *c = cont.add_developer_command();
c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd);
return new PendingCommand(cont);
}

View file

@ -173,6 +173,7 @@ public:
static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId);
static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd);
static PendingCommand *prepareAdminCommand(const ::google::protobuf::Message &cmd);
static PendingCommand *prepareDeveloperCommand(const ::google::protobuf::Message &cmd);
QMap<QString, bool> clientFeatures;
};

View file

@ -388,6 +388,33 @@ Response::ResponseCode Server_ProtocolHandler::processAdminCommandContainer(cons
return finalResponseCode;
}
Response::ResponseCode Server_ProtocolHandler::processDeveloperCommandContainer(const CommandContainer &cont,
ResponseContainer &rc)
{
if (!userInfo) {
return Response::RespLoginNeeded;
}
if (!(userInfo->user_level() & ServerInfo_User::IsDeveloper)) {
return Response::RespLoginNeeded;
}
resetIdleTimer();
Response::ResponseCode finalResponseCode = Response::RespOk;
for (int i = cont.developer_command_size() - 1; i >= 0; --i) {
Response::ResponseCode resp = Response::RespInvalidCommand;
const DeveloperCommand &sc = cont.developer_command(i);
const int num = getPbExtension(sc);
logDebugMessage(getSafeDebugString(sc));
resp = processExtendedDeveloperCommand(num, sc, rc);
if (resp != Response::RespOk) {
finalResponseCode = resp;
}
}
return finalResponseCode;
}
void Server_ProtocolHandler::processCommandContainer(const CommandContainer &cont)
{
// Command processing must be disabled after prepareDestroy() has been called.
@ -410,6 +437,8 @@ void Server_ProtocolHandler::processCommandContainer(const CommandContainer &con
finalResponseCode = processModeratorCommandContainer(cont, responseContainer);
} else if (cont.admin_command_size()) {
finalResponseCode = processAdminCommandContainer(cont, responseContainer);
} else if (cont.developer_command_size()) {
finalResponseCode = processDeveloperCommandContainer(cont, responseContainer);
} else {
finalResponseCode = Response::RespInvalidCommand;
}
@ -454,11 +483,12 @@ void Server_ProtocolHandler::pingClockTimeout()
prepareDestroy();
}
// PrivLevel users, Moderators, and Admins are not subject to the server idle timeout policy
// PrivLevel users, Moderators, Admins, and Developers are not subject to the server idle timeout policy
const bool hasPrivLevel = userInfo && QString::fromStdString(userInfo->privlevel()).toLower() != "none";
const bool isModOrAdmin =
userInfo && (userInfo->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin));
if (!hasPrivLevel && !isModOrAdmin) {
const bool isStaff =
userInfo && (userInfo->user_level() &
(ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin | ServerInfo_User::IsDeveloper));
if (!hasPrivLevel && !isStaff) {
if ((server->getIdleClientTimeout() > 0) && (idleClientWarningSent)) {
if (timeRunning - lastActionReceived > server->getIdleClientTimeout()) {
prepareDestroy();

View file

@ -27,6 +27,7 @@ class CommandContainer;
class SessionCommand;
class ModeratorCommand;
class AdminCommand;
class DeveloperCommand;
class Command_Ping;
class Command_Login;
@ -98,6 +99,12 @@ private:
{
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode processDeveloperCommandContainer(const CommandContainer &cont, ResponseContainer &rc);
virtual Response::ResponseCode
processExtendedDeveloperCommand(int /* cmdType */, const DeveloperCommand & /* cmd */, ResponseContainer & /* rc */)
{
return Response::RespFunctionNotAllowed;
}
void resetIdleTimer();
private slots: