[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

@ -382,6 +382,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/tab_card_art_rules.cpp src/interface/widgets/tabs/tab_card_art_rules.cpp
src/interface/widgets/tabs/tab_deck_editor.cpp src/interface/widgets/tabs/tab_deck_editor.cpp
src/interface/widgets/tabs/tab_deck_storage.cpp src/interface/widgets/tabs/tab_deck_storage.cpp
src/interface/widgets/tabs/tab_developer.cpp
src/interface/widgets/tabs/tab_game.cpp src/interface/widgets/tabs/tab_game.cpp
src/interface/widgets/tabs/tab_home.cpp src/interface/widgets/tabs/tab_home.cpp
src/interface/widgets/tabs/tab_logs.cpp src/interface/widgets/tabs/tab_logs.cpp

View file

@ -14,6 +14,7 @@
#define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff"; #define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff";
#define DEFAULT_COLOR_MODERATOR_RIGHT "#000000"; #define DEFAULT_COLOR_MODERATOR_RIGHT "#000000";
#define DEFAULT_COLOR_ADMIN "#ff2701"; #define DEFAULT_COLOR_ADMIN "#ff2701";
#define DEFAULT_COLOR_DEVELOPER "#800020"
/** /**
* Clamps an svg render size so that rendering does not exceed a multiple of the requested size. * Clamps an svg render size so that rendering does not exceed a multiple of the requested size.
@ -359,6 +360,8 @@ QIcon UserLevelPixmapGenerator::generateIconDefault(int height,
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
colorLeft = DEFAULT_COLOR_ADMIN; colorLeft = DEFAULT_COLOR_ADMIN;
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
colorLeft = DEFAULT_COLOR_DEVELOPER;
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
colorLeft = DEFAULT_COLOR_MODERATOR_LEFT; colorLeft = DEFAULT_COLOR_MODERATOR_LEFT;
colorRight = DEFAULT_COLOR_MODERATOR_RIGHT; colorRight = DEFAULT_COLOR_MODERATOR_RIGHT;

View file

@ -51,6 +51,8 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
aDemoteFromMod = new QAction(QString(), this); aDemoteFromMod = new QAction(QString(), this);
aPromoteToJudge = new QAction(QString(), this); aPromoteToJudge = new QAction(QString(), this);
aDemoteFromJudge = new QAction(QString(), this); aDemoteFromJudge = new QAction(QString(), this);
aPromoteToDeveloper = new QAction(QString(), this);
aDemoteFromDeveloper = new QAction(QString(), this);
aGetAdminNotes = new QAction(QString(), this); aGetAdminNotes = new QAction(QString(), this);
aInvestigateUser = new QAction(QString(), this); aInvestigateUser = new QAction(QString(), this);
@ -76,6 +78,8 @@ void UserContextMenu::retranslateUi()
aDemoteFromMod->setText(tr("Dem&ote user from moderator")); aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
aPromoteToJudge->setText(tr("Promote user to &judge")); aPromoteToJudge->setText(tr("Promote user to &judge"));
aDemoteFromJudge->setText(tr("Demote user from judge")); aDemoteFromJudge->setText(tr("Demote user from judge"));
aPromoteToDeveloper->setText(tr("Promote user to &developer"));
aDemoteFromDeveloper->setText(tr("Demote user from de&veloper"));
aGetAdminNotes->setText(tr("View admin notes")); aGetAdminNotes->setText(tr("View admin notes"));
aInvestigateUser->setText(tr("Investigate user")); aInvestigateUser->setText(tr("Investigate user"));
} }
@ -268,7 +272,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext); const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
if (resp.response_code() == Response::RespOk) { if (resp.response_code() == Response::RespOk) {
if (cmd.should_be_mod() || cmd.should_be_judge()) { if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"), QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"),
tr("Successfully promoted user.")); tr("Successfully promoted user."));
} else { } else {
@ -276,7 +280,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
} }
} else { } else {
if (cmd.should_be_mod() || cmd.should_be_judge()) { if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user.")); QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user."));
} else { } else {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user.")); QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user."));
@ -437,6 +441,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) { (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToJudge); menu->addAction(aPromoteToJudge);
} }
if (userLevel.testFlag(ServerInfo_User::IsDeveloper) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aDemoteFromDeveloper);
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToDeveloper);
}
} }
aDetails->setEnabled(true); aDetails->setEnabled(true);
aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName)); aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName));
@ -455,6 +468,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
aInvestigateUser->setEnabled(anotherUser); aInvestigateUser->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser);
aPromoteToJudge->setEnabled(anotherUser);
aDemoteFromJudge->setEnabled(anotherUser);
aPromoteToDeveloper->setEnabled(anotherUser);
aDemoteFromDeveloper->setEnabled(anotherUser);
QAction *actionClicked = menu->exec(pos); QAction *actionClicked = menu->exec(pos);
if (actionClicked == nullptr) { if (actionClicked == nullptr) {
@ -489,6 +506,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
execAdjustMod(userName, actionClicked == aPromoteToMod); execAdjustMod(userName, actionClicked == aPromoteToMod);
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) { } else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
execAdjustJudge(userName, actionClicked == aPromoteToJudge); execAdjustJudge(userName, actionClicked == aPromoteToJudge);
} else if (actionClicked == aPromoteToDeveloper || actionClicked == aDemoteFromDeveloper) {
execAdjustDeveloper(userName, actionClicked == aPromoteToDeveloper);
} else if (actionClicked == aBanHistory) { } else if (actionClicked == aBanHistory) {
execBanHistory(userName); execBanHistory(userName);
} else if (actionClicked == aWarnUser) { } else if (actionClicked == aWarnUser) {
@ -707,3 +726,13 @@ void UserContextMenu::execAdjustJudge(const QString &userName, bool shouldBeJudg
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse); connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend); client->sendCommand(pend);
} }
void UserContextMenu::execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper)
{
Command_AdjustMod cmd;
cmd.set_user_name(userName.toStdString());
cmd.set_should_be_developer(shouldBeDeveloper);
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend);
}

View file

@ -45,6 +45,7 @@ private:
QAction *aBan, *aBanHistory; QAction *aBan, *aBanHistory;
QAction *aPromoteToMod, *aDemoteFromMod; QAction *aPromoteToMod, *aDemoteFromMod;
QAction *aPromoteToJudge, *aDemoteFromJudge; QAction *aPromoteToJudge, *aDemoteFromJudge;
QAction *aPromoteToDeveloper, *aDemoteFromDeveloper;
QAction *aWarnUser, *aWarnHistory; QAction *aWarnUser, *aWarnHistory;
QAction *aGetAdminNotes; QAction *aGetAdminNotes;
std::function<QList<GameInviteOption>()> gameInviteLinkProvider; std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
@ -123,6 +124,7 @@ public:
void execInvestigateUser(const QString &userName); void execInvestigateUser(const QString &userName);
void execAdjustMod(const QString &userName, bool shouldBeMod); void execAdjustMod(const QString &userName, bool shouldBeMod);
void execAdjustJudge(const QString &userName, bool shouldBeJudge); void execAdjustJudge(const QString &userName, bool shouldBeJudge);
void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper);
private: private:
void execInvite(const QString &userName, const GameInviteOption &option); void execInvite(const QString &userName, const GameInviteOption &option);

View file

@ -122,6 +122,8 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user)
QString userLevelText; QString userLevelText;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
userLevelText = tr("Administrator"); userLevelText = tr("Administrator");
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
userLevelText = tr("Developer");
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
userLevelText = tr("Moderator"); userLevelText = tr("Moderator");
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) { } else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {

View file

@ -245,6 +245,9 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
if (level.testFlag(ServerInfo_User::IsAdmin)) { if (level.testFlag(ServerInfo_User::IsAdmin)) {
return QColor(245, 158, 11); return QColor(245, 158, 11);
} }
if (level.testFlag(ServerInfo_User::IsDeveloper)) {
return QColor(185, 28, 28);
}
if (level.testFlag(ServerInfo_User::IsModerator)) { if (level.testFlag(ServerInfo_User::IsModerator)) {
return QColor(59, 130, 246); return QColor(59, 130, 246);
} }
@ -300,6 +303,8 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
} badge; } badge;
if (level.testFlag(ServerInfo_User::IsAdmin)) { if (level.testFlag(ServerInfo_User::IsAdmin)) {
badge = {"ADMIN", QColor(245, 158, 11)}; badge = {"ADMIN", QColor(245, 158, 11)};
} else if (level.testFlag(ServerInfo_User::IsDeveloper)) {
badge = {"DEV", QColor(185, 28, 28)};
} else if (level.testFlag(ServerInfo_User::IsModerator)) { } else if (level.testFlag(ServerInfo_User::IsModerator)) {
badge = {"MOD", QColor(59, 130, 246)}; badge = {"MOD", QColor(59, 130, 246)};
} else if (level.testFlag(ServerInfo_User::IsJudge)) { } else if (level.testFlag(ServerInfo_User::IsJudge)) {

View file

@ -49,6 +49,8 @@ QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool onl
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
accentColor = QColor(245, 158, 11); accentColor = QColor(245, 158, 11);
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
accentColor = QColor(185, 28, 28);
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
accentColor = QColor(59, 130, 246); accentColor = QColor(59, 130, 246);
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@ -299,6 +301,8 @@ QList<UserListPainter::Badge> UserListPainter::buildBadges(const UserLevelFlags
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
badges << Badge{"ADMIN", QColor(245, 158, 11)}; badges << Badge{"ADMIN", QColor(245, 158, 11)};
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
badges << Badge{"DEV", QColor(185, 28, 28)};
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
badges << Badge{"MOD", QColor(59, 130, 246)}; badges << Badge{"MOD", QColor(59, 130, 246)};
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@ -385,9 +389,9 @@ void UserListPainter::paint(QPainter *painter,
const QString userName = QString::fromStdString(userInfo.name()); const QString userName = QString::fromStdString(userInfo.name());
const QString privLevel = QString::fromStdString(userInfo.privlevel()); const QString privLevel = QString::fromStdString(userInfo.privlevel());
const QColor accentColor = getAccentColor(userLevel, online); const QColor accentColor = getAccentColor(userLevel, online);
const bool hasRole = userLevel.testFlag(ServerInfo_User::IsAdmin) || const bool hasRole =
userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsAdmin) || userLevel.testFlag(ServerInfo_User::IsDeveloper) ||
userLevel.testFlag(ServerInfo_User::IsJudge); userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsJudge);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
const int cardRight = getCardRight(option, rect); const int cardRight = getCardRight(option, rect);

View file

@ -234,9 +234,11 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt()); const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt()); const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt());
// Admins & Mods need no additional comparison checks, just to see if they're an admin or a moderator // Admins, Developers & Mods need no additional comparison checks, just to see if they're an admin, a developer
// or a moderator
static const QList<ServerInfo_User_UserLevelFlag> userLevelWithNoOtherPrefOrder = { static const QList<ServerInfo_User_UserLevelFlag> userLevelWithNoOtherPrefOrder = {
ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsModerator}; ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsDeveloper,
ServerInfo_User_UserLevelFlag_IsModerator};
for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) { for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) && if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) { lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {

View file

@ -0,0 +1,122 @@
/**
* @file tab_developer.cpp
* @ingroup ServerTabs
*/
//! \todo Document this file.
#include "tab_developer.h"
#include <QDateTime>
#include <QHeaderView>
#include <QLabel>
#include <QPushButton>
#include <QTableWidget>
#include <QVBoxLayout>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_get_server_stats.pb.h>
#include <libcockatrice/protocol/pb/response_get_server_stats.pb.h>
#include <libcockatrice/protocol/pending_command.h>
TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client)
: Tab(_tabSupervisor), client(_client)
{
statsTable = new QTableWidget(0, 2);
statsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
statsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
statsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
statsTable->setSelectionMode(QAbstractItemView::SingleSelection);
statsTable->horizontalHeader()->setStretchLastSection(true);
statsTable->verticalHeader()->setVisible(false);
statusLabel = new QLabel;
refreshButton = new QPushButton;
refreshButton->setAutoDefault(true);
connect(refreshButton, &QPushButton::clicked, this, &TabDeveloper::refreshClicked);
auto *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(statusLabel, 1, Qt::AlignLeft);
buttonLayout->addWidget(refreshButton, 0, Qt::AlignRight);
auto *mainLayout = new QVBoxLayout;
mainLayout->addWidget(statsTable);
mainLayout->addLayout(buttonLayout);
auto *central = new QWidget;
central->setLayout(mainLayout);
setCentralWidget(central);
retranslateUi();
}
void TabDeveloper::retranslateUi()
{
refreshButton->setText(tr("Refresh server stats"));
statsTable->setHorizontalHeaderLabels(QString(tr("Statistic;Value")).split(";"));
if (statsTable->rowCount() == 0) {
statusLabel->clear();
}
}
QString TabDeveloper::formatBytes(quint64 bytes)
{
const quint64 kib = 1024;
const quint64 mib = 1024 * kib;
const quint64 gib = 1024 * mib;
if (bytes >= gib) {
return tr("%1 GiB").arg(QString::number(bytes / static_cast<double>(gib), 'f', 2));
}
if (bytes >= mib) {
return tr("%1 MiB").arg(QString::number(bytes / static_cast<double>(mib), 'f', 2));
}
if (bytes >= kib) {
return tr("%1 KiB").arg(QString::number(bytes / static_cast<double>(kib), 'f', 2));
}
return tr("%1 bytes").arg(bytes);
}
void TabDeveloper::appendStatRow(const QString &name, const QString &value)
{
const int row = statsTable->rowCount();
statsTable->insertRow(row);
statsTable->setItem(row, 0, new QTableWidgetItem(name));
statsTable->setItem(row, 1, new QTableWidgetItem(value));
}
void TabDeveloper::refreshClicked()
{
Command_GetServerStats cmd;
PendingCommand *pend = client->prepareDeveloperCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabDeveloper::serverStatsResponse);
client->sendCommand(pend);
}
void TabDeveloper::serverStatsResponse(const Response &resp)
{
if (resp.response_code() != Response::RespOk) {
statusLabel->setText(tr("Failed to collect server statistics."));
return;
}
const Response_GetServerStats &response = resp.GetExtension(Response_GetServerStats::ext);
statsTable->setRowCount(0);
appendStatRow(tr("Registered users online"), QString::number(response.users_count()));
appendStatRow(tr("Moderators online"), QString::number(response.mods_count()));
appendStatRow(tr("Games running"), QString::number(response.games_count()));
appendStatRow(tr("Traffic sent (last tick)"), formatBytes(response.tx_bytes()));
appendStatRow(tr("Traffic received (last tick)"), formatBytes(response.rx_bytes()));
const qint64 uptime = static_cast<qint64>(response.uptime_secs());
const int days = static_cast<int>(uptime / 86400);
const int hours = static_cast<int>((uptime % 86400) / 3600);
const int minutes = static_cast<int>((uptime % 3600) / 60);
appendStatRow(tr("Server uptime"), days > 0 ? tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes)
: tr("%1h %2m").arg(hours).arg(minutes));
const QDateTime snapshotTime = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(response.timest()));
appendStatRow(tr("Snapshot taken"), snapshotTime.toLocalTime().toString("yyyy-MM-dd HH:mm"));
statsTable->resizeColumnsToContents();
statusLabel->setText(tr("Updated %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm")));
}

View file

@ -0,0 +1,43 @@
/**
* @file tab_developer.h
* @ingroup ServerTabs
*/
//! \todo Document this file.
#ifndef TAB_DEVELOPER_H
#define TAB_DEVELOPER_H
#include "tab.h"
class AbstractClient;
class QLabel;
class QPushButton;
class QTableWidget;
class Response;
class TabDeveloper : public Tab
{
Q_OBJECT
private:
AbstractClient *client;
QTableWidget *statsTable;
QPushButton *refreshButton;
QLabel *statusLabel;
void appendStatRow(const QString &name, const QString &value);
static QString formatBytes(quint64 bytes);
private slots:
void refreshClicked();
void serverStatsResponse(const Response &resp);
public:
explicit TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client);
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
return tr("Developer");
}
};
#endif

View file

@ -14,12 +14,14 @@
#include <QTabWidget> #include <QTabWidget>
#include <QTableWidget> #include <QTableWidget>
#include <libcockatrice/network/client/abstract/abstract_client.h> #include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_get_log_history.pb.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h> #include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h> #include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands)
: Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands)
{ {
roomTable = new QTableWidget(); roomTable = new QTableWidget();
roomTable->setColumnCount(6); roomTable->setColumnCount(6);
@ -117,7 +119,26 @@ void TabLog::getClicked()
}; };
cmd.set_date_range(dateRange); cmd.set_date_range(dateRange);
cmd.set_maximum_results(maximumResults->value()); cmd.set_maximum_results(maximumResults->value());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
PendingCommand *pend;
if (canUseDeveloperCommands) {
// Developers query logs through the developer command family.
Command_GetLogHistory devCmd;
devCmd.set_user_name(cmd.user_name());
devCmd.set_ip_address(cmd.ip_address());
devCmd.set_game_name(cmd.game_name());
devCmd.set_game_id(cmd.game_id());
devCmd.set_message(cmd.message());
for (int i = 0; i < cmd.log_location_size(); ++i) {
devCmd.add_log_location(cmd.log_location(i));
}
devCmd.set_date_range(cmd.date_range());
devCmd.set_maximum_results(cmd.maximum_results());
pend = client->prepareDeveloperCommand(devCmd);
} else {
pend = client->prepareModeratorCommand(cmd);
}
connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse); connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse);
client->sendCommand(pend); client->sendCommand(pend);
} }

View file

@ -33,6 +33,7 @@ class TabLog : public Tab
Q_OBJECT Q_OBJECT
private: private:
AbstractClient *client; AbstractClient *client;
bool canUseDeveloperCommands;
QLabel *labelFindUserName, *labelFindIPAddress, *labelFindGameName, *labelFindGameID, *labelMessage, *labelMaximum, QLabel *labelFindUserName, *labelFindIPAddress, *labelFindGameName, *labelFindGameID, *labelMessage, *labelMaximum,
*labelDescription; *labelDescription;
LineEditUnfocusable *findUsername, *findIPAddress, *findGameName, *findGameID, *findMessage; LineEditUnfocusable *findUsername, *findIPAddress, *findGameName, *findGameID, *findMessage;
@ -58,7 +59,7 @@ private slots:
void restartLayout(); void restartLayout();
public: public:
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client); TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands = false);
~TabLog() override; ~TabLog() override;
void retranslateUi() override; void retranslateUi() override;
[[nodiscard]] QString getTabText() const override [[nodiscard]] QString getTabText() const override

View file

@ -381,6 +381,9 @@ void TabModeration::moderatorLoginsResponse(const Response &response)
if (login.user_level() & ServerInfo_User::IsAdmin) { if (login.user_level() & ServerInfo_User::IsAdmin) {
levels << tr("Admin"); levels << tr("Admin");
} }
if (login.user_level() & ServerInfo_User::IsDeveloper) {
levels << tr("Developer");
}
if (login.user_level() & ServerInfo_User::IsModerator) { if (login.user_level() & ServerInfo_User::IsModerator) {
levels << tr("Moderator"); levels << tr("Moderator");
} }

View file

@ -15,6 +15,7 @@
#include "tab_card_art_rules.h" #include "tab_card_art_rules.h"
#include "tab_deck_editor.h" #include "tab_deck_editor.h"
#include "tab_deck_storage.h" #include "tab_deck_storage.h"
#include "tab_developer.h"
#include "tab_game.h" #include "tab_game.h"
#include "tab_home.h" #include "tab_home.h"
#include "tab_logs.h" #include "tab_logs.h"
@ -119,7 +120,7 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr), : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr),
tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr),
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr), tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
tabModeration(nullptr), isLocalGame(false) tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false)
{ {
setElideMode(Qt::ElideRight); setElideMode(Qt::ElideRight);
setMovable(true); setMovable(true);
@ -205,6 +206,10 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
aTabModeration->setCheckable(true); aTabModeration->setCheckable(true);
connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration); connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration);
aTabDeveloper = new QAction(this);
aTabDeveloper->setCheckable(true);
connect(aTabDeveloper, &QAction::triggered, this, &TabSupervisor::actTabDeveloper);
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&TabSupervisor::refreshShortcuts); &TabSupervisor::refreshShortcuts);
refreshShortcuts(); refreshShortcuts();
@ -247,6 +252,7 @@ void TabSupervisor::retranslateUi()
aTabReport->setText(tr("Report Queue")); aTabReport->setText(tr("Report Queue"));
aTabModeration->setText(tr("Moderation")); aTabModeration->setText(tr("Moderation"));
aTabCardArtRules->setText(tr("Card Art Rules")); aTabCardArtRules->setText(tr("Card Art Rules"));
aTabDeveloper->setText(tr("Developer"));
// tabs // tabs
QList<Tab *> tabs; QList<Tab *> tabs;
@ -259,6 +265,7 @@ void TabSupervisor::retranslateUi()
tabs.append(tabReport); tabs.append(tabReport);
tabs.append(tabModeration); tabs.append(tabModeration);
tabs.append(tabCardArtRules); tabs.append(tabCardArtRules);
tabs.append(tabDeveloper);
QMapIterator<int, TabRoom *> roomIterator(roomTabs); QMapIterator<int, TabRoom *> roomIterator(roomTabs);
while (roomIterator.hasNext()) { while (roomIterator.hasNext()) {
tabs.append(roomIterator.next().value()); tabs.append(roomIterator.next().value());
@ -528,6 +535,19 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
} }
} }
if (userInfo->user_level() & ServerInfo_User::IsDeveloper) {
tabsMenu->addSeparator();
tabsMenu->addAction(aTabDeveloper);
// Developers without moderation rights get log access through their
// own role. Moderators already have the Logs entry from above.
if (!(userInfo->user_level() & ServerInfo_User::IsModerator)) {
tabsMenu->addAction(aTabLog);
if (SettingsCache::instance().tabs().getTabLogOpen()) {
openTabLog();
}
}
}
retranslateUi(); retranslateUi();
} }
@ -540,6 +560,7 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
tabLog = nullptr; tabLog = nullptr;
tabReport = nullptr; tabReport = nullptr;
tabModeration = nullptr; tabModeration = nullptr;
tabDeveloper = nullptr;
isLocalGame = true; isLocalGame = true;
userInfo = new ServerInfo_User; userInfo = new ServerInfo_User;
localClients = _clients; localClients = _clients;
@ -590,6 +611,10 @@ void TabSupervisor::stop()
if (tabCardArtRules) { if (tabCardArtRules) {
tabCardArtRules->close(); tabCardArtRules->close();
} }
if (tabDeveloper) {
tabDeveloper->close();
}
}
} }
QList<Tab *> tabsToDelete; QList<Tab *> tabsToDelete;
@ -819,7 +844,10 @@ void TabSupervisor::actTabLog(bool checked)
void TabSupervisor::openTabLog() void TabSupervisor::openTabLog()
{ {
tabLog = new TabLog(this, client); // Developers without moderation rights query logs through the developer
// command family, so tell the tab which family to use.
const bool isDeveloper = (userInfo->user_level() & ServerInfo_User::IsDeveloper) != 0;
tabLog = new TabLog(this, client, isDeveloper);
myAddTab(tabLog, aTabLog); myAddTab(tabLog, aTabLog);
connect(tabLog, &QObject::destroyed, this, [this] { connect(tabLog, &QObject::destroyed, this, [this] {
tabLog = nullptr; tabLog = nullptr;
@ -881,6 +909,27 @@ void TabSupervisor::openTabModeration(const QString &userName)
aTabModeration->setChecked(true); aTabModeration->setChecked(true);
} }
void TabSupervisor::actTabDeveloper(bool checked)
{
if (checked && !tabDeveloper) {
openTabDeveloper();
setCurrentWidget(tabDeveloper);
} else if (!checked && tabDeveloper) {
tabDeveloper->closeRequest();
}
}
void TabSupervisor::openTabDeveloper()
{
tabDeveloper = new TabDeveloper(this, client);
myAddTab(tabDeveloper, aTabDeveloper);
connect(tabDeveloper, &QObject::destroyed, this, [this] {
tabDeveloper = nullptr;
aTabDeveloper->setChecked(false);
});
aTabDeveloper->setChecked(true);
}
void TabSupervisor::updatePingTime(int value, int max) void TabSupervisor::updatePingTime(int value, int max)
{ {
if (!tabServer) { if (!tabServer) {

View file

@ -45,6 +45,7 @@ class TabReport;
class TabModeration; class TabModeration;
class TabAccount; class TabAccount;
class TabDeckEditor; class TabDeckEditor;
class TabDeveloper;
class TabLog; class TabLog;
class RoomEvent; class RoomEvent;
class GameEventContainer; class GameEventContainer;
@ -108,6 +109,7 @@ private:
TabLog *tabLog; TabLog *tabLog;
TabReport *tabReport; TabReport *tabReport;
TabModeration *tabModeration; TabModeration *tabModeration;
TabDeveloper *tabDeveloper;
QMap<int, TabRoom *> roomTabs; QMap<int, TabRoom *> roomTabs;
QMap<int, TabGame *> gameTabs; QMap<int, TabGame *> gameTabs;
QList<TabGame *> replayTabs; QList<TabGame *> replayTabs;
@ -117,7 +119,7 @@ private:
QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage, QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage,
*aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin,
*aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration; *aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration, *aTabDeveloper;
int myAddTab(Tab *tab, QAction *manager = nullptr); int myAddTab(Tab *tab, QAction *manager = nullptr);
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
@ -207,6 +209,7 @@ private slots:
void actTabLog(bool checked); void actTabLog(bool checked);
void actTabReport(bool checked); void actTabReport(bool checked);
void actTabModeration(bool checked); void actTabModeration(bool checked);
void actTabDeveloper(bool checked);
void openTabVisualDeckStorage(); void openTabVisualDeckStorage();
void openTabHome(); void openTabHome();
@ -218,6 +221,7 @@ private slots:
void openTabCardArtRules(); void openTabCardArtRules();
void openTabLog(); void openTabLog();
void openTabReport(); void openTabReport();
void openTabDeveloper();
void updateCurrent(int index); void updateCurrent(int index);
void updatePingTime(int value, int max); void updatePingTime(int value, int max);

View file

@ -253,3 +253,11 @@ PendingCommand *AbstractClient::prepareAdminCommand(const ::google::protobuf::Me
c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd); c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd);
return new PendingCommand(cont); 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 *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId);
static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd);
static PendingCommand *prepareAdminCommand(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; QMap<QString, bool> clientFeatures;
}; };

View file

@ -388,6 +388,33 @@ Response::ResponseCode Server_ProtocolHandler::processAdminCommandContainer(cons
return finalResponseCode; 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) void Server_ProtocolHandler::processCommandContainer(const CommandContainer &cont)
{ {
// Command processing must be disabled after prepareDestroy() has been called. // 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); finalResponseCode = processModeratorCommandContainer(cont, responseContainer);
} else if (cont.admin_command_size()) { } else if (cont.admin_command_size()) {
finalResponseCode = processAdminCommandContainer(cont, responseContainer); finalResponseCode = processAdminCommandContainer(cont, responseContainer);
} else if (cont.developer_command_size()) {
finalResponseCode = processDeveloperCommandContainer(cont, responseContainer);
} else { } else {
finalResponseCode = Response::RespInvalidCommand; finalResponseCode = Response::RespInvalidCommand;
} }
@ -454,11 +483,12 @@ void Server_ProtocolHandler::pingClockTimeout()
prepareDestroy(); 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 hasPrivLevel = userInfo && QString::fromStdString(userInfo->privlevel()).toLower() != "none";
const bool isModOrAdmin = const bool isStaff =
userInfo && (userInfo->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin)); userInfo && (userInfo->user_level() &
if (!hasPrivLevel && !isModOrAdmin) { (ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin | ServerInfo_User::IsDeveloper));
if (!hasPrivLevel && !isStaff) {
if ((server->getIdleClientTimeout() > 0) && (idleClientWarningSent)) { if ((server->getIdleClientTimeout() > 0) && (idleClientWarningSent)) {
if (timeRunning - lastActionReceived > server->getIdleClientTimeout()) { if (timeRunning - lastActionReceived > server->getIdleClientTimeout()) {
prepareDestroy(); prepareDestroy();

View file

@ -27,6 +27,7 @@ class CommandContainer;
class SessionCommand; class SessionCommand;
class ModeratorCommand; class ModeratorCommand;
class AdminCommand; class AdminCommand;
class DeveloperCommand;
class Command_Ping; class Command_Ping;
class Command_Login; class Command_Login;
@ -98,6 +99,12 @@ private:
{ {
return Response::RespFunctionNotAllowed; 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(); void resetIdleTimer();
private slots: private slots:

View file

@ -22,6 +22,8 @@ set(PROTO_FILES
command_del_counter.proto command_del_counter.proto
command_delete_arrow.proto command_delete_arrow.proto
command_draw_cards.proto command_draw_cards.proto
command_get_log_history.proto
command_get_server_stats.proto
command_dump_zone.proto command_dump_zone.proto
command_flip_card.proto command_flip_card.proto
command_game_say.proto command_game_say.proto
@ -71,6 +73,7 @@ set(PROTO_FILES
context_ready_start.proto context_ready_start.proto
context_set_sideboard_lock.proto context_set_sideboard_lock.proto
context_undo_draw.proto context_undo_draw.proto
developer_commands.proto
event_add_to_list.proto event_add_to_list.proto
event_attach_card.proto event_attach_card.proto
event_change_zone_properties.proto event_change_zone_properties.proto
@ -140,6 +143,7 @@ set(PROTO_FILES
response_forgotpasswordrequest.proto response_forgotpasswordrequest.proto
response_get_admin_notes.proto response_get_admin_notes.proto
response_get_games_of_user.proto response_get_games_of_user.proto
response_get_server_stats.proto
response_get_user_info.proto response_get_user_info.proto
response_join_room.proto response_join_room.proto
response_list_users.proto response_list_users.proto

View file

@ -37,6 +37,7 @@ message Command_AdjustMod {
required string user_name = 1; required string user_name = 1;
optional bool should_be_mod = 2; optional bool should_be_mod = 2;
optional bool should_be_judge = 3; optional bool should_be_judge = 3;
optional bool should_be_developer = 4;
} }
message Command_ResetUserPassword { message Command_ResetUserPassword {

View file

@ -0,0 +1,19 @@
syntax = "proto2";
import "developer_commands.proto";
// Developer counterpart of Command_ViewLogHistory: identical query fields, but
// routed through the developer command family so developers never need the
// moderator command container.
message Command_GetLogHistory {
extend DeveloperCommand {
optional Command_GetLogHistory ext = 1001;
}
optional string user_name = 1; // user that created message
optional string ip_address = 2; // ip address of user that created message
optional string game_name = 3; // client id of user that created the message
optional string game_id = 4; // game number the message was sent to
optional string message = 5; // raw message that was sent
repeated string log_location = 6; // destination of message (ex: main room, game room, private chat)
required uint32 date_range = 7; // the length of time (in minutes) to look back for
optional uint32 maximum_results = 8; // the maximum number of query results
}

View file

@ -0,0 +1,8 @@
syntax = "proto2";
import "developer_commands.proto";
message Command_GetServerStats {
extend DeveloperCommand {
optional Command_GetServerStats ext = 1000;
}
}

View file

@ -4,6 +4,7 @@ import "game_commands.proto";
import "room_commands.proto"; import "room_commands.proto";
import "moderator_commands.proto"; import "moderator_commands.proto";
import "admin_commands.proto"; import "admin_commands.proto";
import "developer_commands.proto";
message CommandContainer { message CommandContainer {
optional uint64 cmd_id = 1; optional uint64 cmd_id = 1;
@ -16,4 +17,5 @@ message CommandContainer {
repeated RoomCommand room_command = 102; repeated RoomCommand room_command = 102;
repeated ModeratorCommand moderator_command = 103; repeated ModeratorCommand moderator_command = 103;
repeated AdminCommand admin_command = 104; repeated AdminCommand admin_command = 104;
repeated DeveloperCommand developer_command = 105;
} }

View file

@ -0,0 +1,8 @@
syntax = "proto2";
message DeveloperCommand {
enum DeveloperCommandType {
GET_SERVER_STATS = 1000;
VIEWLOG_HISTORY = 1001;
}
extensions 100 to max;
}

View file

@ -77,6 +77,7 @@ message Response {
FORGOT_PASSWORD_REQUEST = 1016; // Response to password reset request FORGOT_PASSWORD_REQUEST = 1016; // Response to password reset request
PASSWORD_SALT = 1017; // Response containing password salt PASSWORD_SALT = 1017; // Response containing password salt
GET_ADMIN_NOTES = 1018; // Response with admin notes GET_ADMIN_NOTES = 1018; // Response with admin notes
GET_SERVER_STATS = 1019; // Response with server status statistics
REPLAY_LIST = 1100; // Response listing replays REPLAY_LIST = 1100; // Response listing replays
REPLAY_DOWNLOAD = 1101; // Response for replay download REPLAY_DOWNLOAD = 1101; // Response for replay download
REPLAY_GET_CODE = 1102; // Response containing replay code REPLAY_GET_CODE = 1102; // Response containing replay code

View file

@ -0,0 +1,19 @@
syntax = "proto2";
import "response.proto";
message Response_GetServerStats {
extend Response {
optional Response_GetServerStats ext = 1220;
}
optional uint64 users_count = 1;
optional uint64 mods_count = 2;
optional uint64 games_count = 3;
// Traffic recorded during the last status update tick
optional uint64 tx_bytes = 4;
optional uint64 rx_bytes = 5;
optional uint64 uptime_secs = 6;
optional uint64 timest = 7; // unix timestamp of the snapshot
}

View file

@ -8,6 +8,7 @@ message ServerInfo_User {
IsModerator = 4; IsModerator = 4;
IsAdmin = 8; IsAdmin = 8;
IsJudge = 16; IsJudge = 16;
IsDeveloper = 32;
}; };
message PawnColorsOverride { message PawnColorsOverride {
optional string left_side = 1; optional string left_side = 1;

View file

@ -641,6 +641,10 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer
userLevel |= ServerInfo_User::IsJudge; userLevel |= ServerInfo_User::IsJudge;
} }
if (is_admin & 8) {
userLevel |= ServerInfo_User::IsDeveloper;
}
result.set_user_level(userLevel); result.set_user_level(userLevel);
const QString country = query->value(3).toString(); const QString country = query->value(3).toString();
@ -1448,7 +1452,7 @@ QList<ServerInfo_ModeratorLogin> Servatrice_DatabaseInterface::getModeratorLastL
QSqlQuery *query = prepareQuery("SELECT u.name, u.admin, UNIX_TIMESTAMP(a.last_login) " QSqlQuery *query = prepareQuery("SELECT u.name, u.admin, UNIX_TIMESTAMP(a.last_login) "
"FROM {prefix}_users u " "FROM {prefix}_users u "
"LEFT JOIN {prefix}_user_analytics a ON a.id = u.id " "LEFT JOIN {prefix}_user_analytics a ON a.id = u.id "
"WHERE (u.admin & 7) <> 0 ORDER BY u.name"); "WHERE (u.admin & 15) <> 0 ORDER BY u.name");
if (!execSqlQuery(query)) { if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog) << "Failed to collect moderator login information: SQL Error"; qCWarning(DatabaseInterfaceLog) << "Failed to collect moderator login information: SQL Error";
@ -1469,6 +1473,9 @@ QList<ServerInfo_ModeratorLogin> Servatrice_DatabaseInterface::getModeratorLastL
if (isAdmin & 4) { if (isAdmin & 4) {
userLevel |= ServerInfo_User::IsJudge; userLevel |= ServerInfo_User::IsJudge;
} }
if (isAdmin & 8) {
userLevel |= ServerInfo_User::IsDeveloper;
}
loginDetails.set_user_level(userLevel); loginDetails.set_user_level(userLevel);
if (!query->value(2).isNull()) { if (!query->value(2).isNull()) {

View file

@ -47,6 +47,8 @@
#include <libcockatrice/protocol/pb/command_deck_list.pb.h> #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_new_dir.pb.h>
#include <libcockatrice/protocol/pb/command_deck_upload.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_delete_match.pb.h>
#include <libcockatrice/protocol/pb/command_replay_download.pb.h> #include <libcockatrice/protocol/pb/command_replay_download.pb.h>
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.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_deck_upload.pb.h>
#include <libcockatrice/protocol/pb/response_forgotpasswordrequest.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_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_moderator_last_logins.pb.h>
#include <libcockatrice/protocol/pb/response_password_salt.pb.h> #include <libcockatrice/protocol/pb/response_password_salt.pb.h>
#include <libcockatrice/protocol/pb/response_register.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) Response::ResponseCode AbstractServerSocketInterface::cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc)
{ {
if (authState != PasswordRight) { if (authState != PasswordRight) {
@ -1643,6 +1678,38 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportUserInfo(const Co
return Response::RespOk; 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 */, Response::ResponseCode AbstractServerSocketInterface::cmdReportStats(const Command_ReportStats & /*cmd */,
ResponseContainer &rc) ResponseContainer &rc)
{ {
@ -3215,7 +3282,7 @@ bool AbstractServerSocketInterface::removeAdminFlagFromUser(const QString &userN
if (user) { if (user) {
Event_ConnectionClosed event; Event_ConnectionClosed event;
event.set_reason(Event_ConnectionClosed::DEMOTED); 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()); event.set_end_time(QDateTime::currentDateTime().toSecsSinceEpoch());
SessionEvent *se = user->prepareSessionEvent(event); 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; return Response::RespOk;
} }

View file

@ -24,6 +24,8 @@
#include <QMutex> #include <QMutex>
#include <QTcpSocket> #include <QTcpSocket>
#include <QWebSocket> #include <QWebSocket>
#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_download_by_game_id.pb.h> #include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
#include <libcockatrice/protocol/pb/command_report.pb.h> #include <libcockatrice/protocol/pb/command_report.pb.h>
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h> #include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
@ -151,6 +153,8 @@ private:
processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc) override; processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc) override;
Response::ResponseCode Response::ResponseCode
processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc) override; processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc) override;
Response::ResponseCode
processExtendedDeveloperCommand(int cmdType, const DeveloperCommand &cmd, ResponseContainer &rc) override;
Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc);
Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc);
@ -172,6 +176,8 @@ private:
Response::ResponseCode cmdResetUserPassword(const Command_ResetUserPassword &cmd, ResponseContainer &rc); Response::ResponseCode cmdResetUserPassword(const Command_ResetUserPassword &cmd, ResponseContainer &rc);
Response::ResponseCode cmdRemoveUserAvatar(const Command_RemoveUserAvatar &cmd, ResponseContainer &rc); Response::ResponseCode cmdRemoveUserAvatar(const Command_RemoveUserAvatar &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetServerStats(const Command_GetServerStats &cmd, ResponseContainer &rc);
bool addAdminFlagToUser(const QString &user, int flag); bool addAdminFlagToUser(const QString &user, int flag);
bool removeAdminFlagFromUser(const QString &user, int flag); bool removeAdminFlagFromUser(const QString &user, int flag);