[Client] Add moderation tab with investigate, password reset, and avatar removal

- New Moderation tab: search a user to show account info, alternate
  accounts, login sessions, and staff last logins; actions to reset the
  user's password (shows the generated temporary password) and remove the
  user's avatar
- 'Investigate user' entry in the user context menu opens the tab pre-loaded
  for that user
- Warning dialog shows the infraction level of each warning category
- Tab wired into TabSupervisor with a moderator-gated menu action, shortcut,
  and tabs.ini persistence (default closed)
This commit is contained in:
Lukas Brübach 2026-08-11 21:10:53 +02:00
parent ae58cdd2de
commit c6d8bce81f
14 changed files with 640 additions and 6 deletions

View file

@ -379,6 +379,7 @@ set(cockatrice_SOURCES
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
src/interface/widgets/tabs/tab_message.cpp src/interface/widgets/tabs/tab_message.cpp
src/interface/widgets/tabs/tab_moderation.cpp
src/interface/widgets/tabs/tab_report.cpp src/interface/widgets/tabs/tab_report.cpp
src/interface/widgets/tabs/tab_replays.cpp src/interface/widgets/tabs/tab_replays.cpp
src/interface/widgets/tabs/tab_room.cpp src/interface/widgets/tabs/tab_room.cpp

View file

@ -788,6 +788,8 @@ private:
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)}, ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)},
{"Tabs/aTabReport", {"Tabs/aTabReport",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)}, ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)},
{"Tabs/aTabModeration",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Moderation"), parseSequenceString(""), ShortcutGroup::Tabs)},
}; };
}; };

View file

@ -52,6 +52,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
aPromoteToJudge = new QAction(QString(), this); aPromoteToJudge = new QAction(QString(), this);
aDemoteFromJudge = new QAction(QString(), this); aDemoteFromJudge = new QAction(QString(), this);
aGetAdminNotes = new QAction(QString(), this); aGetAdminNotes = new QAction(QString(), this);
aInvestigateUser = new QAction(QString(), this);
retranslateUi(); retranslateUi();
} }
@ -76,6 +77,7 @@ void UserContextMenu::retranslateUi()
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"));
aGetAdminNotes->setText(tr("View admin notes")); aGetAdminNotes->setText(tr("View admin notes"));
aInvestigateUser->setText(tr("Investigate user"));
} }
void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer) void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer)
@ -147,7 +149,8 @@ void UserContextMenu::warnUser_processGetWarningsListResponse(const Response &r)
if (response.warning_size() > 0) { if (response.warning_size() > 0) {
for (int i = 0; i < response.warning_size(); ++i) { for (int i = 0; i < response.warning_size(); ++i) {
dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified()); int startingIl = i < response.warning_il_size() ? response.warning_il(i) : 1;
dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified(), startingIl);
} }
} }
dlg->show(); dlg->show();
@ -414,6 +417,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
menu->addAction(aBanHistory); menu->addAction(aBanHistory);
menu->addSeparator(); menu->addSeparator();
menu->addAction(aGetAdminNotes); menu->addAction(aGetAdminNotes);
menu->addAction(aInvestigateUser);
menu->addSeparator(); menu->addSeparator();
if (userLevel.testFlag(ServerInfo_User::IsModerator) && if (userLevel.testFlag(ServerInfo_User::IsModerator) &&
@ -448,6 +452,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
aBan->setEnabled(anotherUser); aBan->setEnabled(anotherUser);
aBanHistory->setEnabled(anotherUser); aBanHistory->setEnabled(anotherUser);
aGetAdminNotes->setEnabled(anotherUser); aGetAdminNotes->setEnabled(anotherUser);
aInvestigateUser->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser);
@ -492,6 +497,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
execWarnHistory(userName); execWarnHistory(userName);
} else if (actionClicked == aGetAdminNotes) { } else if (actionClicked == aGetAdminNotes) {
execAdminNotes(userName); execAdminNotes(userName);
} else if (actionClicked == aInvestigateUser) {
execInvestigateUser(userName);
} else if (actionClicked == aCopyToClipBoard) { } else if (actionClicked == aCopyToClipBoard) {
QClipboard *clipboard = QGuiApplication::clipboard(); QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setText(deckHash); clipboard->setText(deckHash);
@ -668,6 +675,11 @@ void UserContextMenu::execAdminNotes(const QString &userName)
client->sendCommand(pend); client->sendCommand(pend);
} }
void UserContextMenu::execInvestigateUser(const QString &userName)
{
tabSupervisor->openTabModeration(userName);
}
void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod) void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod)
{ {
Command_AdjustMod cmd; Command_AdjustMod cmd;

View file

@ -48,6 +48,7 @@ private:
QAction *aWarnUser, *aWarnHistory; QAction *aWarnUser, *aWarnHistory;
QAction *aGetAdminNotes; QAction *aGetAdminNotes;
std::function<QList<GameInviteOption>()> gameInviteLinkProvider; std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
QAction *aInvestigateUser;
signals: signals:
void openMessageDialog(const QString &userName, bool focus); void openMessageDialog(const QString &userName, bool focus);
private slots: private slots:
@ -119,6 +120,7 @@ public:
void execBanHistory(const QString &userName); void execBanHistory(const QString &userName);
void execWarnHistory(const QString &userName); void execWarnHistory(const QString &userName);
void execAdminNotes(const QString &userName); void execAdminNotes(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);

View file

@ -213,10 +213,14 @@ int WarningDialog::getDeleteMessages() const
return deleteMessages->isChecked() ? -1 : 0; return deleteMessages->isChecked() ? -1 : 0;
} }
void WarningDialog::addWarningOption(const QString warning) void WarningDialog::addWarningOption(const QString warning, int startingIl)
{ {
if (startingIl > 1) {
warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl));
} else {
warningOption->addItem(warning); warningOption->addItem(warning);
} }
}
void BanDialog::okClicked() void BanDialog::okClicked()
{ {

View file

@ -85,7 +85,7 @@ public:
[[nodiscard]] QString getWarnID() const; [[nodiscard]] QString getWarnID() const;
[[nodiscard]] QString getReason() const; [[nodiscard]] QString getReason() const;
[[nodiscard]] int getDeleteMessages() const; [[nodiscard]] int getDeleteMessages() const;
void addWarningOption(const QString warning); void addWarningOption(const QString warning, int startingIl = 1);
}; };
class AdminNotesDialog : public QDialog class AdminNotesDialog : public QDialog

View file

@ -0,0 +1,462 @@
#include "tab_moderation.h"
#include "abstract_client.h"
#include "tab_supervisor.h"
#include <QDateTime>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSplitter>
#include <QTableWidget>
#include <QTextEdit>
#include <QVBoxLayout>
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/response_moderator_last_logins.pb.h>
#include <libcockatrice/protocol/pb/response_remove_user_avatar.pb.h>
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
#include <libcockatrice/protocol/pb/response_reset_user_password.pb.h>
#include <libcockatrice/protocol/pb/response_user_alts.pb.h>
#include <libcockatrice/protocol/pb/response_user_sessions.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/protocol/pending_command.h>
namespace
{
constexpr int COL_ALTS_USER = 0;
constexpr int COL_ALTS_EMAIL = 1;
constexpr int COL_ALTS_CLIENTID = 2;
constexpr int COL_ALTS_REGISTERED = 3;
constexpr int COL_ALTS_LAST_LOGIN = 4;
constexpr int COL_ALTS_WARNS = 5;
constexpr int COL_ALTS_BANS = 6;
constexpr int COL_ALTS_ACTIVE = 7;
constexpr int COL_ALTS_COUNT = 8;
constexpr int COL_SESSIONS_IP = 0;
constexpr int COL_SESSIONS_CLIENTID = 1;
constexpr int COL_SESSIONS_START = 2;
constexpr int COL_SESSIONS_END = 3;
constexpr int COL_SESSIONS_TYPE = 4;
constexpr int COL_SESSIONS_COUNT = 5;
constexpr int COL_STAFF_USER = 0;
constexpr int COL_STAFF_LEVEL = 1;
constexpr int COL_STAFF_LAST_LOGIN = 2;
constexpr int COL_STAFF_COUNT = 3;
} // namespace
TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser)
: Tab(_tabSupervisor), client(_client)
{
auto *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
searchEdit = new QLineEdit;
searchEdit->setClearButtonEnabled(true);
connect(searchEdit, &QLineEdit::returnPressed, this, &TabModeration::investigateUser);
investigateButton = new QPushButton;
connect(investigateButton, &QPushButton::clicked, this, &TabModeration::investigateUser);
resetPasswordButton = new QPushButton;
connect(resetPasswordButton, &QPushButton::clicked, this, &TabModeration::resetPassword);
removeAvatarButton = new QPushButton;
connect(removeAvatarButton, &QPushButton::clicked, this, &TabModeration::removeAvatar);
auto *topBar = new QHBoxLayout;
topBar->addWidget(searchEdit);
topBar->addWidget(investigateButton);
topBar->addStretch();
topBar->addWidget(resetPasswordButton);
topBar->addWidget(removeAvatarButton);
userInfoGroup = new QGroupBox;
userInfoNameLabel = new QLabel;
userInfoNameValue = new QLabel;
userInfoRegisteredLabel = new QLabel;
userInfoRegisteredValue = new QLabel;
userInfoLastLoginLabel = new QLabel;
userInfoLastLoginValue = new QLabel;
userInfoStatusLabel = new QLabel;
userInfoStatusValue = new QLabel;
userInfoCountsLabel = new QLabel;
userInfoCountsValue = new QLabel;
userInfoNotesLabel = new QLabel;
userInfoNotesEdit = new QTextEdit;
userInfoNotesEdit->setReadOnly(true);
auto *infoGrid = new QGridLayout;
infoGrid->addWidget(userInfoNameLabel, 0, 0);
infoGrid->addWidget(userInfoNameValue, 0, 1);
infoGrid->addWidget(userInfoRegisteredLabel, 0, 2);
infoGrid->addWidget(userInfoRegisteredValue, 0, 3);
infoGrid->addWidget(userInfoLastLoginLabel, 1, 0);
infoGrid->addWidget(userInfoLastLoginValue, 1, 1);
infoGrid->addWidget(userInfoStatusLabel, 1, 2);
infoGrid->addWidget(userInfoStatusValue, 1, 3);
infoGrid->addWidget(userInfoCountsLabel, 2, 0);
infoGrid->addWidget(userInfoCountsValue, 2, 1, 1, 3);
infoGrid->addWidget(userInfoNotesLabel, 3, 0, Qt::AlignTop);
infoGrid->addWidget(userInfoNotesEdit, 3, 1, 1, 3);
infoGrid->setColumnStretch(1, 1);
infoGrid->setColumnStretch(3, 1);
auto *infoLayout = new QVBoxLayout(userInfoGroup);
infoLayout->addLayout(infoGrid);
auto configureTable = [](QTableWidget *table) {
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->verticalHeader()->setVisible(false);
table->setAlternatingRowColors(true);
table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
table->horizontalHeader()->setStretchLastSection(true);
};
altsGroup = new QGroupBox;
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
configureTable(altsTable);
auto *altsLayout = new QVBoxLayout(altsGroup);
altsLayout->addWidget(altsTable);
sessionsGroup = new QGroupBox;
sessionsTable = new QTableWidget(0, COL_SESSIONS_COUNT);
configureTable(sessionsTable);
auto *sessionsLayout = new QVBoxLayout(sessionsGroup);
sessionsLayout->addWidget(sessionsTable);
staffGroup = new QGroupBox;
staffTable = new QTableWidget(0, COL_STAFF_COUNT);
configureTable(staffTable);
refreshStaffButton = new QPushButton;
connect(refreshStaffButton, &QPushButton::clicked, this, &TabModeration::requestModeratorLogins);
auto *staffHeader = new QHBoxLayout;
staffHeader->addStretch();
staffHeader->addWidget(refreshStaffButton);
auto *staffLayout = new QVBoxLayout(staffGroup);
staffLayout->addWidget(staffTable);
staffLayout->addLayout(staffHeader);
auto *splitter = new QSplitter(Qt::Vertical);
splitter->addWidget(userInfoGroup);
splitter->addWidget(altsGroup);
splitter->addWidget(sessionsGroup);
splitter->addWidget(staffGroup);
splitter->setStretchFactor(0, 1);
splitter->setStretchFactor(1, 2);
splitter->setStretchFactor(2, 2);
splitter->setStretchFactor(3, 1);
auto *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->addLayout(topBar);
mainLayout->addWidget(splitter);
retranslateUi();
clearUserData();
requestModeratorLogins();
investigate(initialUser);
}
void TabModeration::retranslateUi()
{
searchEdit->setPlaceholderText(tr("User name"));
investigateButton->setText(tr("Investigate"));
resetPasswordButton->setText(tr("Reset Password"));
removeAvatarButton->setText(tr("Remove Avatar"));
refreshStaffButton->setText(tr("Refresh"));
userInfoGroup->setTitle(tr("User Info"));
userInfoNameLabel->setText(tr("Name:"));
userInfoRegisteredLabel->setText(tr("Registered:"));
userInfoLastLoginLabel->setText(tr("Last login:"));
userInfoStatusLabel->setText(tr("Status:"));
userInfoCountsLabel->setText(tr("Counts:"));
userInfoNotesLabel->setText(tr("Admin notes:"));
altsGroup->setTitle(tr("Alts"));
sessionsGroup->setTitle(tr("Sessions"));
staffGroup->setTitle(tr("Staff Last Logins"));
altsTable->setHorizontalHeaderLabels({tr("User"), tr("eMail"), tr("Client ID"), tr("Registered"), tr("Last login"),
tr("Warns"), tr("Bans"), tr("Active")});
sessionsTable->setHorizontalHeaderLabels({tr("IP"), tr("Client ID"), tr("Start"), tr("End"), tr("Type")});
staffTable->setHorizontalHeaderLabels({tr("User"), tr("Level"), tr("Last login")});
}
QString TabModeration::formatEpoch(quint64 ts) const
{
if (ts == 0) {
return tr("Unknown");
}
return QDateTime::fromSecsSinceEpoch(ts).toLocalTime().toString("yyyy-MM-dd HH:mm");
}
void TabModeration::clearUserData()
{
currentUser.clear();
userInfoNameValue->clear();
userInfoRegisteredValue->clear();
userInfoLastLoginValue->clear();
userInfoStatusValue->clear();
userInfoCountsValue->clear();
userInfoNotesEdit->clear();
altsTable->setRowCount(0);
sessionsTable->setRowCount(0);
resetPasswordButton->setEnabled(false);
removeAvatarButton->setEnabled(false);
}
void TabModeration::investigate(const QString &userName)
{
if (userName.isEmpty()) {
return;
}
searchEdit->setText(userName);
investigateUser();
}
void TabModeration::investigateUser()
{
const QString userName = searchEdit->text().simplified();
if (userName.isEmpty()) {
return;
}
currentUser = userName;
resetPasswordButton->setEnabled(true);
removeAvatarButton->setEnabled(true);
altsTable->setRowCount(0);
sessionsTable->setRowCount(0);
requestUserInfo(userName);
requestSessions(userName);
requestAlts(userName);
}
void TabModeration::requestUserInfo(const QString &userName)
{
userInfoNameValue->setText(userName);
userInfoRegisteredValue->setText(tr("Loading..."));
userInfoLastLoginValue->setText(tr("Loading..."));
userInfoStatusValue->clear();
userInfoCountsValue->clear();
userInfoNotesEdit->clear();
Command_ReportUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabModeration::userInfoResponse);
client->sendCommand(pend);
}
void TabModeration::userInfoResponse(const Response &response)
{
if (response.response_code() != Response::RespOk) {
userInfoRegisteredValue->clear();
userInfoLastLoginValue->clear();
userInfoStatusValue->setText(tr("Error loading user info."));
return;
}
const Response_ReportUserInfo &resp = response.GetExtension(Response_ReportUserInfo::ext);
if (resp.has_user_name() && QString::fromStdString(resp.user_name()) != currentUser) {
return;
}
userInfoRegisteredValue->setText(formatEpoch(resp.registration_time()));
userInfoLastLoginValue->setText(formatEpoch(resp.last_login()));
QStringList statusParts;
statusParts << (resp.is_active() ? tr("active") : tr("inactive"));
if (resp.has_is_admin() && resp.is_admin()) {
statusParts << tr("admin");
}
userInfoStatusValue->setText(statusParts.join(", "));
userInfoCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3")
.arg(resp.total_reports())
.arg(resp.total_bans())
.arg(resp.total_warns()));
if (resp.has_admin_notes() && !resp.admin_notes().empty()) {
userInfoNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes()));
} else {
userInfoNotesEdit->setPlainText(tr("(none)"));
}
}
void TabModeration::requestSessions(const QString &userName)
{
Command_GetUserSessions cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabModeration::sessionsResponse);
client->sendCommand(pend);
}
void TabModeration::sessionsResponse(const Response &response)
{
sessionsTable->setRowCount(0);
if (response.response_code() != Response::RespOk) {
return;
}
const Response_UserSessions &resp = response.GetExtension(Response_UserSessions::ext);
sessionsTable->setRowCount(resp.sessions_size());
for (int i = 0; i < resp.sessions_size(); ++i) {
const ServerInfo_UserSession &session = resp.sessions(i);
sessionsTable->setItem(i, COL_SESSIONS_IP, new QTableWidgetItem(QString::fromStdString(session.ip_address())));
sessionsTable->setItem(i, COL_SESSIONS_CLIENTID,
new QTableWidgetItem(QString::fromStdString(session.clientid())));
sessionsTable->setItem(i, COL_SESSIONS_START, new QTableWidgetItem(formatEpoch(session.start_time())));
sessionsTable->setItem(
i, COL_SESSIONS_END,
new QTableWidgetItem(session.end_time() == 0 ? tr("Active") : formatEpoch(session.end_time())));
sessionsTable->setItem(i, COL_SESSIONS_TYPE,
new QTableWidgetItem(QString::fromStdString(session.connection_type())));
}
}
void TabModeration::requestAlts(const QString &userName)
{
Command_GetUserAlts cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabModeration::altsResponse);
client->sendCommand(pend);
}
void TabModeration::altsResponse(const Response &response)
{
altsTable->setRowCount(0);
if (response.response_code() != Response::RespOk) {
return;
}
const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext);
altsTable->setRowCount(resp.alts_size());
for (int i = 0; i < resp.alts_size(); ++i) {
const ServerInfo_UserAlt &alt = resp.alts(i);
altsTable->setItem(i, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name())));
altsTable->setItem(i, COL_ALTS_EMAIL, new QTableWidgetItem(QString::fromStdString(alt.email())));
altsTable->setItem(i, COL_ALTS_CLIENTID, new QTableWidgetItem(QString::fromStdString(alt.clientid())));
altsTable->setItem(i, COL_ALTS_REGISTERED, new QTableWidgetItem(formatEpoch(alt.registration_time())));
altsTable->setItem(i, COL_ALTS_LAST_LOGIN, new QTableWidgetItem(formatEpoch(alt.last_login())));
altsTable->setItem(i, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count())));
altsTable->setItem(i, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count())));
altsTable->setItem(i, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no")));
}
}
void TabModeration::requestModeratorLogins()
{
Command_GetModeratorLastLogins cmd;
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabModeration::moderatorLoginsResponse);
client->sendCommand(pend);
}
void TabModeration::moderatorLoginsResponse(const Response &response)
{
staffTable->setRowCount(0);
if (response.response_code() != Response::RespOk) {
return;
}
const Response_ModeratorLastLogins &resp = response.GetExtension(Response_ModeratorLastLogins::ext);
staffTable->setRowCount(resp.logins_size());
for (int i = 0; i < resp.logins_size(); ++i) {
const ServerInfo_ModeratorLogin &login = resp.logins(i);
staffTable->setItem(i, COL_STAFF_USER, new QTableWidgetItem(QString::fromStdString(login.user_name())));
staffTable->setItem(i, COL_STAFF_LAST_LOGIN, new QTableWidgetItem(formatEpoch(login.last_login())));
QStringList levels;
if (login.user_level() & ServerInfo_User::IsAdmin) {
levels << tr("Admin");
}
if (login.user_level() & ServerInfo_User::IsModerator) {
levels << tr("Moderator");
}
if (login.user_level() & ServerInfo_User::IsJudge) {
levels << tr("Judge");
}
staffTable->setItem(i, COL_STAFF_LEVEL, new QTableWidgetItem(levels.join(" / ")));
}
}
void TabModeration::resetPassword()
{
if (currentUser.isEmpty()) {
return;
}
QMessageBox::StandardButton choice =
QMessageBox::warning(this, tr("Reset Password"),
tr("Reset the password of %1? A temporary password will be generated and shown to you. "
"The user must change it on their first login.")
.arg(currentUser),
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
if (choice != QMessageBox::Ok) {
return;
}
Command_ResetUserPassword cmd;
cmd.set_user_name(currentUser.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabModeration::resetPasswordResponse);
client->sendCommand(pend);
}
void TabModeration::resetPasswordResponse(const Response &response)
{
if (response.response_code() != Response::RespOk) {
QMessageBox::critical(this, tr("Error"), tr("Password reset failed."));
return;
}
const Response_ResetUserPassword &resp = response.GetExtension(Response_ResetUserPassword::ext);
QMessageBox::information(this, tr("Password reset"),
tr("Temporary password for %1:\n\n%2\n\nPass it to the user through a secure channel.")
.arg(QString::fromStdString(resp.user_name()))
.arg(QString::fromStdString(resp.temporary_password())));
}
void TabModeration::removeAvatar()
{
if (currentUser.isEmpty()) {
return;
}
QMessageBox::StandardButton choice =
QMessageBox::warning(this, tr("Remove Avatar"),
tr("Remove the avatar of %1? The user will have to upload a new one.").arg(currentUser),
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
if (choice != QMessageBox::Ok) {
return;
}
Command_RemoveUserAvatar cmd;
cmd.set_user_name(currentUser.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabModeration::removeAvatarResponse);
client->sendCommand(pend);
}
void TabModeration::removeAvatarResponse(const Response &response)
{
if (response.response_code() != Response::RespOk) {
QMessageBox::critical(this, tr("Error"), tr("Could not remove the avatar."));
return;
}
const Response_RemoveUserAvatar &resp = response.GetExtension(Response_RemoveUserAvatar::ext);
QMessageBox::information(this, tr("Avatar removed"),
tr("The avatar of %1 has been removed.").arg(QString::fromStdString(resp.user_name())));
}

View file

@ -0,0 +1,83 @@
#ifndef TAB_MODERATION_H
#define TAB_MODERATION_H
#include "tab.h"
#include <libcockatrice/protocol/pb/response.pb.h>
class AbstractClient;
class QGroupBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QTableWidget;
class QTextEdit;
/**
* Staff investigation tool. Lets moderators look up a user's account data,
* alternate accounts, login sessions, and staff login activity, and offers
* the password-reset and remove-avatar actions.
*/
class TabModeration : public Tab
{
Q_OBJECT
public:
explicit TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser = {});
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
return tr("Moderation");
}
void investigate(const QString &userName);
private slots:
void investigateUser();
void userInfoResponse(const Response &response);
void sessionsResponse(const Response &response);
void altsResponse(const Response &response);
void moderatorLoginsResponse(const Response &response);
void resetPassword();
void resetPasswordResponse(const Response &response);
void removeAvatar();
void removeAvatarResponse(const Response &response);
private:
void requestUserInfo(const QString &userName);
void requestSessions(const QString &userName);
void requestAlts(const QString &userName);
void requestModeratorLogins();
void clearUserData();
[[nodiscard]] QString formatEpoch(quint64 ts) const;
AbstractClient *client;
QString currentUser;
QLineEdit *searchEdit;
QPushButton *investigateButton;
QPushButton *resetPasswordButton;
QPushButton *removeAvatarButton;
QGroupBox *userInfoGroup;
QLabel *userInfoNameLabel;
QLabel *userInfoNameValue;
QLabel *userInfoRegisteredLabel;
QLabel *userInfoRegisteredValue;
QLabel *userInfoLastLoginLabel;
QLabel *userInfoLastLoginValue;
QLabel *userInfoStatusLabel;
QLabel *userInfoStatusValue;
QLabel *userInfoCountsLabel;
QLabel *userInfoCountsValue;
QLabel *userInfoNotesLabel;
QTextEdit *userInfoNotesEdit;
QGroupBox *altsGroup;
QTableWidget *altsTable;
QGroupBox *sessionsGroup;
QTableWidget *sessionsTable;
QGroupBox *staffGroup;
QTableWidget *staffTable;
QPushButton *refreshStaffButton;
};
#endif // TAB_MODERATION_H

View file

@ -19,6 +19,7 @@
#include "tab_home.h" #include "tab_home.h"
#include "tab_logs.h" #include "tab_logs.h"
#include "tab_message.h" #include "tab_message.h"
#include "tab_moderation.h"
#include "tab_replays.h" #include "tab_replays.h"
#include "tab_report.h" #include "tab_report.h"
#include "tab_room.h" #include "tab_room.h"
@ -117,7 +118,7 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent) TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr), : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr),
tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr),
tabLog(nullptr), tabReport(nullptr), isLocalGame(false) tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), isLocalGame(false)
{ {
setElideMode(Qt::ElideRight); setElideMode(Qt::ElideRight);
setMovable(true); setMovable(true);
@ -198,6 +199,10 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
aTabReport->setCheckable(true); aTabReport->setCheckable(true);
connect(aTabReport, &QAction::triggered, this, &TabSupervisor::actTabReport); connect(aTabReport, &QAction::triggered, this, &TabSupervisor::actTabReport);
aTabModeration = new QAction(this);
aTabModeration->setCheckable(true);
connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration);
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&TabSupervisor::refreshShortcuts); &TabSupervisor::refreshShortcuts);
refreshShortcuts(); refreshShortcuts();
@ -238,6 +243,7 @@ void TabSupervisor::retranslateUi()
aTabAdmin->setText(tr("Administration")); aTabAdmin->setText(tr("Administration"));
aTabLog->setText(tr("Logs")); aTabLog->setText(tr("Logs"));
aTabReport->setText(tr("Report Queue")); aTabReport->setText(tr("Report Queue"));
aTabModeration->setText(tr("Moderation"));
// tabs // tabs
QList<Tab *> tabs; QList<Tab *> tabs;
@ -248,6 +254,7 @@ void TabSupervisor::retranslateUi()
tabs.append(tabAccount); tabs.append(tabAccount);
tabs.append(tabLog); tabs.append(tabLog);
tabs.append(tabReport); tabs.append(tabReport);
tabs.append(tabModeration);
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());
@ -295,6 +302,7 @@ void TabSupervisor::refreshShortcuts()
aTabAdmin->setShortcuts(shortcuts.getShortcut("Tabs/aTabAdmin")); aTabAdmin->setShortcuts(shortcuts.getShortcut("Tabs/aTabAdmin"));
aTabLog->setShortcuts(shortcuts.getShortcut("Tabs/aTabLog")); aTabLog->setShortcuts(shortcuts.getShortcut("Tabs/aTabLog"));
aTabReport->setShortcuts(shortcuts.getShortcut("Tabs/aTabReport")); aTabReport->setShortcuts(shortcuts.getShortcut("Tabs/aTabReport"));
aTabModeration->setShortcuts(shortcuts.getShortcut("Tabs/aTabModeration"));
} }
void TabSupervisor::closeEvent(QCloseEvent *event) void TabSupervisor::closeEvent(QCloseEvent *event)
@ -497,6 +505,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
tabsMenu->addAction(aTabLog); tabsMenu->addAction(aTabLog);
tabsMenu->addAction(aTabCardArtRules); tabsMenu->addAction(aTabCardArtRules);
tabsMenu->addAction(aTabReport); tabsMenu->addAction(aTabReport);
tabsMenu->addAction(aTabModeration);
if (SettingsCache::instance().tabs().getTabAdminOpen()) { if (SettingsCache::instance().tabs().getTabAdminOpen()) {
openTabAdmin(); openTabAdmin();
@ -507,6 +516,9 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
if (SettingsCache::instance().tabs().getTabReportOpen()) { if (SettingsCache::instance().tabs().getTabReportOpen()) {
openTabReport(); openTabReport();
} }
if (SettingsCache::instance().tabs().getTabModerationOpen()) {
openTabModeration();
}
openTabCardArtRules(); openTabCardArtRules();
} }
@ -521,6 +533,7 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
tabAdmin = nullptr; tabAdmin = nullptr;
tabLog = nullptr; tabLog = nullptr;
tabReport = nullptr; tabReport = nullptr;
tabModeration = nullptr;
isLocalGame = true; isLocalGame = true;
userInfo = new ServerInfo_User; userInfo = new ServerInfo_User;
localClients = _clients; localClients = _clients;
@ -565,6 +578,9 @@ void TabSupervisor::stop()
if (tabReport) { if (tabReport) {
tabReport->close(); tabReport->close();
} }
if (tabModeration) {
tabModeration->close();
}
} }
QList<Tab *> tabsToDelete; QList<Tab *> tabsToDelete;
@ -826,6 +842,35 @@ void TabSupervisor::openTabReport()
aTabReport->setChecked(true); aTabReport->setChecked(true);
} }
void TabSupervisor::actTabModeration(bool checked)
{
SettingsCache::instance().tabs().setTabModerationOpen(checked);
if (checked && !tabModeration) {
openTabModeration();
setCurrentWidget(tabModeration);
} else if (!checked && tabModeration) {
tabModeration->closeRequest();
}
}
void TabSupervisor::openTabModeration(const QString &userName)
{
if (tabModeration) {
setCurrentWidget(tabModeration);
if (!userName.isEmpty()) {
tabModeration->investigate(userName);
}
return;
}
tabModeration = new TabModeration(this, client, userName);
myAddTab(tabModeration, aTabModeration);
connect(tabModeration, &QObject::destroyed, this, [this] {
tabModeration = nullptr;
aTabModeration->setChecked(false);
});
aTabModeration->setChecked(true);
}
void TabSupervisor::updatePingTime(int value, int max) void TabSupervisor::updatePingTime(int value, int max)
{ {
if (!tabServer) { if (!tabServer) {

View file

@ -41,6 +41,7 @@ class TabReplays;
class TabAdmin; class TabAdmin;
class TabMessage; class TabMessage;
class TabReport; class TabReport;
class TabModeration;
class TabAccount; class TabAccount;
class TabDeckEditor; class TabDeckEditor;
class TabLog; class TabLog;
@ -105,6 +106,7 @@ private:
TabCardArtRules *tabCardArtRules; TabCardArtRules *tabCardArtRules;
TabLog *tabLog; TabLog *tabLog;
TabReport *tabReport; TabReport *tabReport;
TabModeration *tabModeration;
QMap<int, TabRoom *> roomTabs; QMap<int, TabRoom *> roomTabs;
QMap<int, TabGame *> gameTabs; QMap<int, TabGame *> gameTabs;
QList<TabGame *> replayTabs; QList<TabGame *> replayTabs;
@ -114,7 +116,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; *aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration;
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);
@ -186,6 +188,7 @@ public slots:
TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false); TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false);
void openReplay(GameReplay *replay); void openReplay(GameReplay *replay);
void joinReportGame(int gameId, int roomId); void joinReportGame(int gameId, int roomId);
void openTabModeration(const QString &userName = {});
void switchToFirstAvailableNetworkTab(); void switchToFirstAvailableNetworkTab();
void maximizeMainWindow(); void maximizeMainWindow();
void actTabVisualDeckStorage(bool checked); void actTabVisualDeckStorage(bool checked);
@ -202,6 +205,7 @@ private slots:
void actTabAdmin(bool checked); void actTabAdmin(bool checked);
void actTabLog(bool checked); void actTabLog(bool checked);
void actTabReport(bool checked); void actTabReport(bool checked);
void actTabModeration(bool checked);
void openTabVisualDeckStorage(); void openTabVisualDeckStorage();
void openTabHome(); void openTabHome();

View file

@ -20,6 +20,7 @@ public:
[[nodiscard]] virtual bool getTabAdminOpen() const = 0; [[nodiscard]] virtual bool getTabAdminOpen() const = 0;
[[nodiscard]] virtual bool getTabLogOpen() const = 0; [[nodiscard]] virtual bool getTabLogOpen() const = 0;
[[nodiscard]] virtual bool getTabReportOpen() const = 0; [[nodiscard]] virtual bool getTabReportOpen() const = 0;
[[nodiscard]] virtual bool getTabModerationOpen() const = 0;
}; };
#endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H #endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H

View file

@ -101,6 +101,11 @@ bool TabsSettings::getTabReportOpen() const
return getValue("report", QString(), QString(), false).toBool(); return getValue("report", QString(), QString(), false).toBool();
} }
bool TabsSettings::getTabModerationOpen() const
{
return getValue("moderation", QString(), QString(), false).toBool();
}
void TabsSettings::setTabVisualDeckStorageOpen(bool value) void TabsSettings::setTabVisualDeckStorageOpen(bool value)
{ {
setValue(value, "visualDeckStorage"); setValue(value, "visualDeckStorage");
@ -140,3 +145,8 @@ void TabsSettings::setTabReportOpen(bool value)
{ {
setValue(value, "report"); setValue(value, "report");
} }
void TabsSettings::setTabModerationOpen(bool value)
{
setValue(value, "moderation");
}

View file

@ -42,6 +42,7 @@ public:
[[nodiscard]] bool getTabAdminOpen() const override; [[nodiscard]] bool getTabAdminOpen() const override;
[[nodiscard]] bool getTabLogOpen() const override; [[nodiscard]] bool getTabLogOpen() const override;
[[nodiscard]] bool getTabReportOpen() const override; [[nodiscard]] bool getTabReportOpen() const override;
[[nodiscard]] bool getTabModerationOpen() const override;
void setStartupTabIndex(int value); void setStartupTabIndex(int value);
void setStartupServerHost(const QString &host); void setStartupServerHost(const QString &host);
@ -55,6 +56,7 @@ public:
void setTabAdminOpen(bool value); void setTabAdminOpen(bool value);
void setTabLogOpen(bool value); void setTabLogOpen(bool value);
void setTabReportOpen(bool value); void setTabReportOpen(bool value);
void setTabModerationOpen(bool value);
signals: signals:
void startupTabIndexChanged(int index); void startupTabIndexChanged(int index);

View file

@ -232,6 +232,12 @@ TEST_F(SettingsDefaultsTest, Tabs_AllTabsOpen_Default)
ASSERT_EQ(s.getTabLogOpen(), true); ASSERT_EQ(s.getTabLogOpen(), true);
} }
TEST_F(SettingsDefaultsTest, Tabs_ModerationOpen_Default)
{
TabsSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getTabModerationOpen(), false);
}
// --- ChatSettings --- // --- ChatSettings ---
TEST_F(SettingsDefaultsTest, Chat_Mention_Default) TEST_F(SettingsDefaultsTest, Chat_Mention_Default)