mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
[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:
parent
ae58cdd2de
commit
c6d8bce81f
14 changed files with 640 additions and 6 deletions
|
|
@ -379,6 +379,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/tab_home.cpp
|
||||
src/interface/widgets/tabs/tab_logs.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_replays.cpp
|
||||
src/interface/widgets/tabs/tab_room.cpp
|
||||
|
|
|
|||
|
|
@ -788,6 +788,8 @@ private:
|
|||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabReport",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabModeration",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Moderation"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
|
|||
aPromoteToJudge = new QAction(QString(), this);
|
||||
aDemoteFromJudge = new QAction(QString(), this);
|
||||
aGetAdminNotes = new QAction(QString(), this);
|
||||
aInvestigateUser = new QAction(QString(), this);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
|
@ -76,6 +77,7 @@ void UserContextMenu::retranslateUi()
|
|||
aPromoteToJudge->setText(tr("Promote user to &judge"));
|
||||
aDemoteFromJudge->setText(tr("Demote user from judge"));
|
||||
aGetAdminNotes->setText(tr("View admin notes"));
|
||||
aInvestigateUser->setText(tr("Investigate user"));
|
||||
}
|
||||
|
||||
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) {
|
||||
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();
|
||||
|
|
@ -414,6 +417,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
menu->addAction(aBanHistory);
|
||||
menu->addSeparator();
|
||||
menu->addAction(aGetAdminNotes);
|
||||
menu->addAction(aInvestigateUser);
|
||||
|
||||
menu->addSeparator();
|
||||
if (userLevel.testFlag(ServerInfo_User::IsModerator) &&
|
||||
|
|
@ -448,6 +452,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
aBan->setEnabled(anotherUser);
|
||||
aBanHistory->setEnabled(anotherUser);
|
||||
aGetAdminNotes->setEnabled(anotherUser);
|
||||
aInvestigateUser->setEnabled(anotherUser);
|
||||
aPromoteToMod->setEnabled(anotherUser);
|
||||
aDemoteFromMod->setEnabled(anotherUser);
|
||||
|
||||
|
|
@ -492,6 +497,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
execWarnHistory(userName);
|
||||
} else if (actionClicked == aGetAdminNotes) {
|
||||
execAdminNotes(userName);
|
||||
} else if (actionClicked == aInvestigateUser) {
|
||||
execInvestigateUser(userName);
|
||||
} else if (actionClicked == aCopyToClipBoard) {
|
||||
QClipboard *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText(deckHash);
|
||||
|
|
@ -668,6 +675,11 @@ void UserContextMenu::execAdminNotes(const QString &userName)
|
|||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void UserContextMenu::execInvestigateUser(const QString &userName)
|
||||
{
|
||||
tabSupervisor->openTabModeration(userName);
|
||||
}
|
||||
|
||||
void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod)
|
||||
{
|
||||
Command_AdjustMod cmd;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ private:
|
|||
QAction *aWarnUser, *aWarnHistory;
|
||||
QAction *aGetAdminNotes;
|
||||
std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
|
||||
QAction *aInvestigateUser;
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
private slots:
|
||||
|
|
@ -119,6 +120,7 @@ public:
|
|||
void execBanHistory(const QString &userName);
|
||||
void execWarnHistory(const QString &userName);
|
||||
void execAdminNotes(const QString &userName);
|
||||
void execInvestigateUser(const QString &userName);
|
||||
void execAdjustMod(const QString &userName, bool shouldBeMod);
|
||||
void execAdjustJudge(const QString &userName, bool shouldBeJudge);
|
||||
|
||||
|
|
|
|||
|
|
@ -213,9 +213,13 @@ int WarningDialog::getDeleteMessages() const
|
|||
return deleteMessages->isChecked() ? -1 : 0;
|
||||
}
|
||||
|
||||
void WarningDialog::addWarningOption(const QString warning)
|
||||
void WarningDialog::addWarningOption(const QString warning, int startingIl)
|
||||
{
|
||||
warningOption->addItem(warning);
|
||||
if (startingIl > 1) {
|
||||
warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl));
|
||||
} else {
|
||||
warningOption->addItem(warning);
|
||||
}
|
||||
}
|
||||
|
||||
void BanDialog::okClicked()
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public:
|
|||
[[nodiscard]] QString getWarnID() const;
|
||||
[[nodiscard]] QString getReason() const;
|
||||
[[nodiscard]] int getDeleteMessages() const;
|
||||
void addWarningOption(const QString warning);
|
||||
void addWarningOption(const QString warning, int startingIl = 1);
|
||||
};
|
||||
|
||||
class AdminNotesDialog : public QDialog
|
||||
|
|
|
|||
462
cockatrice/src/interface/widgets/tabs/tab_moderation.cpp
Normal file
462
cockatrice/src/interface/widgets/tabs/tab_moderation.cpp
Normal 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())));
|
||||
}
|
||||
83
cockatrice/src/interface/widgets/tabs/tab_moderation.h
Normal file
83
cockatrice/src/interface/widgets/tabs/tab_moderation.h
Normal 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
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
#include "tab_home.h"
|
||||
#include "tab_logs.h"
|
||||
#include "tab_message.h"
|
||||
#include "tab_moderation.h"
|
||||
#include "tab_replays.h"
|
||||
#include "tab_report.h"
|
||||
#include "tab_room.h"
|
||||
|
|
@ -117,7 +118,7 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
|||
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
|
||||
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(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);
|
||||
setMovable(true);
|
||||
|
|
@ -198,6 +199,10 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
|
|||
aTabReport->setCheckable(true);
|
||||
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,
|
||||
&TabSupervisor::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
|
|
@ -238,6 +243,7 @@ void TabSupervisor::retranslateUi()
|
|||
aTabAdmin->setText(tr("Administration"));
|
||||
aTabLog->setText(tr("Logs"));
|
||||
aTabReport->setText(tr("Report Queue"));
|
||||
aTabModeration->setText(tr("Moderation"));
|
||||
|
||||
// tabs
|
||||
QList<Tab *> tabs;
|
||||
|
|
@ -248,6 +254,7 @@ void TabSupervisor::retranslateUi()
|
|||
tabs.append(tabAccount);
|
||||
tabs.append(tabLog);
|
||||
tabs.append(tabReport);
|
||||
tabs.append(tabModeration);
|
||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||
while (roomIterator.hasNext()) {
|
||||
tabs.append(roomIterator.next().value());
|
||||
|
|
@ -295,6 +302,7 @@ void TabSupervisor::refreshShortcuts()
|
|||
aTabAdmin->setShortcuts(shortcuts.getShortcut("Tabs/aTabAdmin"));
|
||||
aTabLog->setShortcuts(shortcuts.getShortcut("Tabs/aTabLog"));
|
||||
aTabReport->setShortcuts(shortcuts.getShortcut("Tabs/aTabReport"));
|
||||
aTabModeration->setShortcuts(shortcuts.getShortcut("Tabs/aTabModeration"));
|
||||
}
|
||||
|
||||
void TabSupervisor::closeEvent(QCloseEvent *event)
|
||||
|
|
@ -497,6 +505,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
|||
tabsMenu->addAction(aTabLog);
|
||||
tabsMenu->addAction(aTabCardArtRules);
|
||||
tabsMenu->addAction(aTabReport);
|
||||
tabsMenu->addAction(aTabModeration);
|
||||
|
||||
if (SettingsCache::instance().tabs().getTabAdminOpen()) {
|
||||
openTabAdmin();
|
||||
|
|
@ -507,6 +516,9 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
|||
if (SettingsCache::instance().tabs().getTabReportOpen()) {
|
||||
openTabReport();
|
||||
}
|
||||
if (SettingsCache::instance().tabs().getTabModerationOpen()) {
|
||||
openTabModeration();
|
||||
}
|
||||
openTabCardArtRules();
|
||||
}
|
||||
|
||||
|
|
@ -521,6 +533,7 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
|
|||
tabAdmin = nullptr;
|
||||
tabLog = nullptr;
|
||||
tabReport = nullptr;
|
||||
tabModeration = nullptr;
|
||||
isLocalGame = true;
|
||||
userInfo = new ServerInfo_User;
|
||||
localClients = _clients;
|
||||
|
|
@ -565,6 +578,9 @@ void TabSupervisor::stop()
|
|||
if (tabReport) {
|
||||
tabReport->close();
|
||||
}
|
||||
if (tabModeration) {
|
||||
tabModeration->close();
|
||||
}
|
||||
}
|
||||
|
||||
QList<Tab *> tabsToDelete;
|
||||
|
|
@ -826,6 +842,35 @@ void TabSupervisor::openTabReport()
|
|||
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)
|
||||
{
|
||||
if (!tabServer) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class TabReplays;
|
|||
class TabAdmin;
|
||||
class TabMessage;
|
||||
class TabReport;
|
||||
class TabModeration;
|
||||
class TabAccount;
|
||||
class TabDeckEditor;
|
||||
class TabLog;
|
||||
|
|
@ -105,6 +106,7 @@ private:
|
|||
TabCardArtRules *tabCardArtRules;
|
||||
TabLog *tabLog;
|
||||
TabReport *tabReport;
|
||||
TabModeration *tabModeration;
|
||||
QMap<int, TabRoom *> roomTabs;
|
||||
QMap<int, TabGame *> gameTabs;
|
||||
QList<TabGame *> replayTabs;
|
||||
|
|
@ -114,7 +116,7 @@ private:
|
|||
|
||||
QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage,
|
||||
*aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin,
|
||||
*aTabCardArtRules, *aTabLog, *aTabReport;
|
||||
*aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration;
|
||||
|
||||
int myAddTab(Tab *tab, QAction *manager = nullptr);
|
||||
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
|
||||
|
|
@ -186,6 +188,7 @@ public slots:
|
|||
TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false);
|
||||
void openReplay(GameReplay *replay);
|
||||
void joinReportGame(int gameId, int roomId);
|
||||
void openTabModeration(const QString &userName = {});
|
||||
void switchToFirstAvailableNetworkTab();
|
||||
void maximizeMainWindow();
|
||||
void actTabVisualDeckStorage(bool checked);
|
||||
|
|
@ -202,6 +205,7 @@ private slots:
|
|||
void actTabAdmin(bool checked);
|
||||
void actTabLog(bool checked);
|
||||
void actTabReport(bool checked);
|
||||
void actTabModeration(bool checked);
|
||||
|
||||
void openTabVisualDeckStorage();
|
||||
void openTabHome();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public:
|
|||
[[nodiscard]] virtual bool getTabAdminOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabLogOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabReportOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabModerationOpen() const = 0;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
|
||||
|
|
|
|||
|
|
@ -101,6 +101,11 @@ bool TabsSettings::getTabReportOpen() const
|
|||
return getValue("report", QString(), QString(), false).toBool();
|
||||
}
|
||||
|
||||
bool TabsSettings::getTabModerationOpen() const
|
||||
{
|
||||
return getValue("moderation", QString(), QString(), false).toBool();
|
||||
}
|
||||
|
||||
void TabsSettings::setTabVisualDeckStorageOpen(bool value)
|
||||
{
|
||||
setValue(value, "visualDeckStorage");
|
||||
|
|
@ -140,3 +145,8 @@ void TabsSettings::setTabReportOpen(bool value)
|
|||
{
|
||||
setValue(value, "report");
|
||||
}
|
||||
|
||||
void TabsSettings::setTabModerationOpen(bool value)
|
||||
{
|
||||
setValue(value, "moderation");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public:
|
|||
[[nodiscard]] bool getTabAdminOpen() const override;
|
||||
[[nodiscard]] bool getTabLogOpen() const override;
|
||||
[[nodiscard]] bool getTabReportOpen() const override;
|
||||
[[nodiscard]] bool getTabModerationOpen() const override;
|
||||
|
||||
void setStartupTabIndex(int value);
|
||||
void setStartupServerHost(const QString &host);
|
||||
|
|
@ -55,6 +56,7 @@ public:
|
|||
void setTabAdminOpen(bool value);
|
||||
void setTabLogOpen(bool value);
|
||||
void setTabReportOpen(bool value);
|
||||
void setTabModerationOpen(bool value);
|
||||
|
||||
signals:
|
||||
void startupTabIndexChanged(int index);
|
||||
|
|
|
|||
|
|
@ -232,6 +232,12 @@ TEST_F(SettingsDefaultsTest, Tabs_AllTabsOpen_Default)
|
|||
ASSERT_EQ(s.getTabLogOpen(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_ModerationOpen_Default)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getTabModerationOpen(), false);
|
||||
}
|
||||
|
||||
// --- ChatSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_Mention_Default)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue