mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-22 09:35:08 -07:00
[Server/Client/Protocol] Reporting users + moderation queue functionality (#7091)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* [Server/Client/Protocol] Reporting users + moderation queue functionality Took 6 minutes Took 3 minutes Took 8 seconds Took 11 minutes Took 12 minutes Took 7 minutes Took 15 seconds Took 2 minutes Took 1 minute Took 30 seconds Took 16 seconds * CI Fix Took 6 minutes * CI Fix Took 6 minutes * [Protocol] Add moderation investigation commands Adds the protocol layer for the moderation investigation suite: - Command_GetUserSessions/GetUserAlts/GetModeratorLastLogins/ResetUserPassword/RemoveUserAvatar (1013-1017) - Response extensions 1215-1219 with ServerInfo messages for sessions, alts, and staff logins - last_login on Response_ReportUserInfo and warning_il on Response_WarnList Took 2 minutes * [Utility] Add warning categories parser with infraction levels Parses the server's 'officialwarnings' setting (comma-separated, optional '|IL' suffix) into WarningCategory structs so the client can display the infraction level of each warning category. Includes GTest coverage. * [Server] Add moderation investigation tools Implements the server side of the moderation suite: - getUserSessions/getUserAlts/getModeratorLastLogins/removeUserAvatar DB methods - Handlers for all five new commands with audit records (PASSWORD_RESET, REMOVE_USER_AVATAR); password resets return a generated temporary password - cmdGetWarnList now reports per-category infraction levels from the officialwarnings setting; cmdReportUserInfo reports last_login - Update servatrice.ini.example with the warning taxonomy - Password/avatar mutations report RespNameNotFound when the user does not exist * [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) * [Server/Client/Protocol] Address PR #7091 review: security, bug, and perf fixes Security: - Promote RESET_USER_PASSWORD to admin-only dispatch (was moderator-accessible) - Reject password reset on users with equal/higher privilege than caller - Notify affected user via Event_NotifyUser::CUSTOM when password is reset - Add server-side category whitelist for reports - Drop reporter name fallback in comment/details authorization (ID-only) - Force password change: new DB column + login enforcement + client disconnect Bugs: - XSS via QTextEdit::append() → insertPlainText() in report tab and utils - allNotified initialized to true even with empty recipients list - Warning combo box: use currentData() instead of baked-in display text - Report resolution now records who resolved (resolved_by column + audit) Performance: - IP-correlation subquery: add 6-month window + LIMIT 200 - getUserSessions: clamp limit to 500 Non-blocking: - Palette-aware colors in report_utils.cpp (dark/light mode) - Report list pagination: offset/limit fields + total_count in response - SessionCommand enum gap comment for reserved values 1201-1203 Schema: 36→37 (force_password_change), 37→38 (resolved_by) Took 12 minutes Took 16 seconds * Fix macOs pedantry Took 5 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
9eafd90a91
commit
ed4eb1cb31
83 changed files with 4768 additions and 43 deletions
|
|
@ -1,6 +1,7 @@
|
|||
#include "tab_account.h"
|
||||
|
||||
#include "../client/sound_engine.h"
|
||||
#include "../interface/widgets/dialogs/dlg_my_reports.h"
|
||||
#include "../interface/widgets/server/user/user_info_box.h"
|
||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||
#include "../interface/widgets/server/user/user_list_widget.h"
|
||||
|
|
@ -49,6 +50,11 @@ TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, c
|
|||
|
||||
auto *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(userInfoBox);
|
||||
|
||||
myReportsButton = new QPushButton(tr("My Reports"));
|
||||
connect(myReportsButton, &QPushButton::clicked, this, &TabAccount::openMyReports);
|
||||
vbox->addWidget(myReportsButton);
|
||||
|
||||
vbox->addWidget(allUsersList);
|
||||
|
||||
auto *addToBuddyList = new QHBoxLayout;
|
||||
|
|
@ -126,6 +132,7 @@ void TabAccount::addToList(const std::string &listName, const QString &userName)
|
|||
|
||||
void TabAccount::retranslateUi()
|
||||
{
|
||||
myReportsButton->setText(tr("My Reports"));
|
||||
allUsersList->retranslateUi();
|
||||
buddyList->retranslateUi();
|
||||
ignoreList->retranslateUi();
|
||||
|
|
@ -240,3 +247,10 @@ void TabAccount::processRemoveFromListEvent(const Event_RemoveFromList &event)
|
|||
|
||||
userList->deleteUser(user);
|
||||
}
|
||||
|
||||
void TabAccount::openMyReports()
|
||||
{
|
||||
auto *dlg = new DlgMyReports(client, this);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dlg->exec();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class Event_RemoveFromList;
|
|||
class Event_UserJoined;
|
||||
class Event_UserLeft;
|
||||
class LineEditUnfocusable;
|
||||
class QPushButton;
|
||||
class Response;
|
||||
class ServerInfo_User;
|
||||
class UserInfoBox;
|
||||
|
|
@ -41,6 +42,7 @@ private slots:
|
|||
void processRemoveFromListEvent(const Event_RemoveFromList &event);
|
||||
void addToIgnoreList();
|
||||
void addToBuddyList();
|
||||
void openMyReports();
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
|
|
@ -50,6 +52,7 @@ private:
|
|||
UserInfoBox *userInfoBox;
|
||||
LineEditUnfocusable *addBuddyEdit;
|
||||
LineEditUnfocusable *addIgnoreEdit;
|
||||
QPushButton *myReportsButton;
|
||||
void addToList(const std::string &listName, const QString &userName);
|
||||
|
||||
public:
|
||||
|
|
|
|||
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
|
||||
927
cockatrice/src/interface/widgets/tabs/tab_report.cpp
Normal file
927
cockatrice/src/interface/widgets/tabs/tab_report.cpp
Normal file
|
|
@ -0,0 +1,927 @@
|
|||
#include "tab_report.h"
|
||||
|
||||
#include "../utility/report_utils.h"
|
||||
#include "abstract_client.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDateTime>
|
||||
#include <QFontDatabase>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTextCursor>
|
||||
#include <QTextEdit>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_replay_download_by_game_id.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int COL_ID = 0;
|
||||
constexpr int COL_TIME = 1;
|
||||
constexpr int COL_REPORTER = 2;
|
||||
constexpr int COL_REPORTED = 3;
|
||||
constexpr int COL_CATEGORY = 4;
|
||||
constexpr int COL_GAMEID = 5;
|
||||
constexpr int COL_STATUS = 6;
|
||||
constexpr int COL_ASSIGNED = 7;
|
||||
constexpr int COL_REPLAY = 8;
|
||||
constexpr int COL_ROOM = 9;
|
||||
constexpr int COL_COUNT = 10;
|
||||
constexpr int REFRESH_INTERVAL_MS = 300000;
|
||||
} // namespace
|
||||
|
||||
TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
auto *centralWidget = new QWidget(this);
|
||||
setCentralWidget(centralWidget);
|
||||
|
||||
searchEdit = new QLineEdit;
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
connect(searchEdit, &QLineEdit::textChanged, this, &TabReport::applyFilters);
|
||||
|
||||
statusFilter = new QComboBox;
|
||||
connect(statusFilter, &QComboBox::currentIndexChanged, this, &TabReport::applyFilters);
|
||||
|
||||
unresolvedOnlyBox = new QCheckBox;
|
||||
unresolvedOnlyBox->setChecked(true);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
connect(unresolvedOnlyBox, &QCheckBox::checkStateChanged, this, &TabReport::refreshList);
|
||||
#else
|
||||
connect(unresolvedOnlyBox, &QCheckBox::stateChanged, this, &TabReport::refreshList);
|
||||
#endif
|
||||
|
||||
refreshButton = new QPushButton;
|
||||
connect(refreshButton, &QPushButton::clicked, this, &TabReport::refreshList);
|
||||
|
||||
refreshTimer = new QTimer(this);
|
||||
refreshTimer->setInterval(REFRESH_INTERVAL_MS);
|
||||
connect(refreshTimer, &QTimer::timeout, this, [this]() {
|
||||
if (tabSupervisor->currentWidget() == this) {
|
||||
refreshList();
|
||||
}
|
||||
});
|
||||
refreshTimer->start();
|
||||
|
||||
auto *topBar = new QHBoxLayout;
|
||||
topBar->addWidget(searchEdit);
|
||||
topBar->addWidget(statusFilter);
|
||||
topBar->addWidget(unresolvedOnlyBox);
|
||||
topBar->addStretch();
|
||||
topBar->addWidget(refreshButton);
|
||||
|
||||
statsLabel = new QLabel;
|
||||
|
||||
table = new QTableWidget(0, COL_COUNT);
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->setSortingEnabled(true);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->setAlternatingRowColors(true);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_TIME, QHeaderView::ResizeToContents);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents);
|
||||
connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged);
|
||||
|
||||
descGroup = new QGroupBox;
|
||||
descriptionEdit = new QTextEdit;
|
||||
descriptionEdit->setReadOnly(true);
|
||||
auto *descLayout = new QVBoxLayout(descGroup);
|
||||
descLayout->setContentsMargins(4, 4, 4, 4);
|
||||
descLayout->addWidget(descriptionEdit);
|
||||
|
||||
chatGroup = new QGroupBox;
|
||||
chatLogEdit = new QTextEdit;
|
||||
chatLogEdit->setReadOnly(true);
|
||||
QFont monoFont("monospace");
|
||||
monoFont.setStyleHint(QFont::Monospace);
|
||||
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
|
||||
if (systemPointSize > 0) {
|
||||
monoFont.setPointSize(systemPointSize);
|
||||
}
|
||||
chatLogEdit->setFont(monoFont);
|
||||
auto *chatLayout = new QVBoxLayout(chatGroup);
|
||||
chatLayout->setContentsMargins(4, 4, 4, 4);
|
||||
chatLayout->addWidget(chatLogEdit);
|
||||
|
||||
commentsGroup = new QGroupBox;
|
||||
commentsEdit = new QTextEdit;
|
||||
commentsEdit->setReadOnly(true);
|
||||
auto *commentInputRow = new QHBoxLayout;
|
||||
commentInput = new QLineEdit;
|
||||
commentButton = new QPushButton;
|
||||
commentButton->setEnabled(false);
|
||||
connect(commentButton, &QPushButton::clicked, this, &TabReport::addComment);
|
||||
connect(commentInput, &QLineEdit::returnPressed, this, &TabReport::addComment);
|
||||
commentInputRow->addWidget(commentInput);
|
||||
commentInputRow->addWidget(commentButton);
|
||||
auto *commentsLayout = new QVBoxLayout(commentsGroup);
|
||||
commentsLayout->setContentsMargins(4, 4, 4, 4);
|
||||
commentsLayout->addWidget(commentsEdit);
|
||||
commentsLayout->addLayout(commentInputRow);
|
||||
|
||||
userContextGroup = new QGroupBox;
|
||||
userContextName = new QLabel;
|
||||
userContextAccountAge = new QLabel;
|
||||
userContextReports = new QLabel;
|
||||
userContextBans = new QLabel;
|
||||
userContextWarns = new QLabel;
|
||||
userContextNotes = new QTextEdit;
|
||||
userContextNotes->setReadOnly(true);
|
||||
userContextNotes->setMaximumHeight(80);
|
||||
userContextRecentReports = new QTextEdit;
|
||||
userContextRecentReports->setReadOnly(true);
|
||||
userContextRecentReports->setMaximumHeight(120);
|
||||
userContextUserLabel = new QLabel;
|
||||
userContextAgeLabel = new QLabel;
|
||||
userContextReportsLabel = new QLabel;
|
||||
userContextBansLabel = new QLabel;
|
||||
userContextWarnsLabel = new QLabel;
|
||||
userContextNotesLabel = new QLabel;
|
||||
userContextRecentReportsLabel = new QLabel;
|
||||
auto *ucLayout = new QGridLayout(userContextGroup);
|
||||
ucLayout->setContentsMargins(8, 8, 8, 8);
|
||||
ucLayout->addWidget(userContextUserLabel, 0, 0);
|
||||
ucLayout->addWidget(userContextName, 0, 1, 1, 3);
|
||||
ucLayout->addWidget(userContextAgeLabel, 1, 0);
|
||||
ucLayout->addWidget(userContextAccountAge, 1, 1);
|
||||
ucLayout->addWidget(userContextReportsLabel, 1, 2);
|
||||
ucLayout->addWidget(userContextReports, 1, 3);
|
||||
ucLayout->addWidget(userContextBansLabel, 2, 0);
|
||||
ucLayout->addWidget(userContextBans, 2, 1);
|
||||
ucLayout->addWidget(userContextWarnsLabel, 2, 2);
|
||||
ucLayout->addWidget(userContextWarns, 2, 3);
|
||||
ucLayout->addWidget(userContextNotesLabel, 3, 0, Qt::AlignTop);
|
||||
ucLayout->addWidget(userContextNotes, 3, 1, 1, 3);
|
||||
ucLayout->addWidget(userContextRecentReportsLabel, 4, 0, 1, 4);
|
||||
ucLayout->addWidget(userContextRecentReports, 5, 0, 1, 4);
|
||||
userContextGroup->setVisible(false);
|
||||
|
||||
statsGroup = new QGroupBox;
|
||||
statsGroup->setCheckable(true);
|
||||
statsTotalLabel = new QLabel;
|
||||
statsTrendLabel = new QLabel;
|
||||
statsCategoriesLabel = new QLabel;
|
||||
statsDetailText = new QTextEdit;
|
||||
statsDetailText->setReadOnly(true);
|
||||
statsDetailText->setMaximumHeight(150);
|
||||
auto *statsContent = new QWidget;
|
||||
auto *sgLayout = new QVBoxLayout(statsContent);
|
||||
sgLayout->setContentsMargins(8, 8, 8, 8);
|
||||
sgLayout->addWidget(statsTotalLabel);
|
||||
sgLayout->addWidget(statsTrendLabel);
|
||||
sgLayout->addWidget(statsCategoriesLabel);
|
||||
sgLayout->addWidget(statsDetailText);
|
||||
auto *statsGroupLayout = new QVBoxLayout(statsGroup);
|
||||
statsGroupLayout->setContentsMargins(0, 0, 0, 0);
|
||||
statsGroupLayout->addWidget(statsContent);
|
||||
statsGroup->setChecked(true);
|
||||
connect(statsGroup, &QGroupBox::toggled, this, [this, statsContent](bool checked) {
|
||||
statsContent->setVisible(checked);
|
||||
if (checked) {
|
||||
requestStats();
|
||||
}
|
||||
});
|
||||
|
||||
detailSplitter = new QSplitter(Qt::Vertical);
|
||||
detailSplitter->addWidget(descGroup);
|
||||
detailSplitter->addWidget(chatGroup);
|
||||
detailSplitter->addWidget(commentsGroup);
|
||||
detailSplitter->setStretchFactor(0, 1);
|
||||
detailSplitter->setStretchFactor(1, 1);
|
||||
detailSplitter->setStretchFactor(2, 2);
|
||||
|
||||
assignButton = new QPushButton;
|
||||
resolveButton = new QPushButton;
|
||||
resolveWithNoteButton = new QPushButton;
|
||||
dismissButton = new QPushButton;
|
||||
viewReplayButton = new QPushButton;
|
||||
joinGameButton = new QPushButton;
|
||||
connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport);
|
||||
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false, false); });
|
||||
connect(resolveWithNoteButton, &QPushButton::clicked, this, [this]() { resolveReport(false, true); });
|
||||
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true, true); });
|
||||
connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay);
|
||||
connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame);
|
||||
|
||||
statusLabel = new QLabel;
|
||||
|
||||
auto *actionBar = new QHBoxLayout;
|
||||
actionBar->addWidget(assignButton);
|
||||
actionBar->addWidget(resolveButton);
|
||||
actionBar->addWidget(resolveWithNoteButton);
|
||||
actionBar->addWidget(dismissButton);
|
||||
actionBar->addSpacing(20);
|
||||
actionBar->addWidget(viewReplayButton);
|
||||
actionBar->addWidget(joinGameButton);
|
||||
actionBar->addStretch();
|
||||
actionBar->addWidget(statusLabel);
|
||||
|
||||
auto *layout = new QVBoxLayout(centralWidget);
|
||||
layout->addLayout(topBar);
|
||||
layout->addWidget(statsLabel);
|
||||
layout->addWidget(table, 2);
|
||||
layout->addWidget(detailSplitter, 1);
|
||||
layout->addWidget(userContextGroup);
|
||||
layout->addWidget(statsGroup);
|
||||
layout->addLayout(actionBar);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
setActionsEnabled(false);
|
||||
refreshList();
|
||||
}
|
||||
|
||||
void TabReport::retranslateUi()
|
||||
{
|
||||
searchEdit->setPlaceholderText(tr("Search by username, category..."));
|
||||
statusFilter->clear();
|
||||
statusFilter->addItem(tr("All Statuses"), "");
|
||||
statusFilter->addItem(tr("Open"), "open");
|
||||
statusFilter->addItem(tr("Assigned"), "assigned");
|
||||
statusFilter->addItem(tr("Resolved"), "resolved");
|
||||
statusFilter->addItem(tr("Dismissed"), "dismissed");
|
||||
unresolvedOnlyBox->setText(tr("Unresolved only"));
|
||||
refreshButton->setText(tr("Refresh"));
|
||||
|
||||
table->setHorizontalHeaderLabels({tr("#"), tr("Time"), tr("Reporter"), tr("Reported User"), tr("Category"),
|
||||
tr("Game ID"), tr("Status"), tr("Assigned To"), tr("Replay"), tr("Room")});
|
||||
|
||||
descGroup->setTitle(tr("Description"));
|
||||
chatGroup->setTitle(tr("Chat Log Context"));
|
||||
chatGroup->setToolTip(
|
||||
tr("Chat log attached by the reporter. It is captured from their client and may be incomplete or edited."));
|
||||
commentsGroup->setTitle(tr("Comments / Thread"));
|
||||
descriptionEdit->setPlaceholderText(tr("No description."));
|
||||
chatLogEdit->setPlaceholderText(tr("No chat log attached."));
|
||||
commentsEdit->setPlaceholderText(tr("No comments yet."));
|
||||
commentInput->setPlaceholderText(tr("Type a reply..."));
|
||||
commentButton->setText(tr("Send"));
|
||||
|
||||
userContextGroup->setTitle(tr("Reported User Context"));
|
||||
userContextUserLabel->setText(tr("User:"));
|
||||
userContextAgeLabel->setText(tr("Account Age:"));
|
||||
userContextReportsLabel->setText(tr("Reports:"));
|
||||
userContextBansLabel->setText(tr("Bans:"));
|
||||
userContextWarnsLabel->setText(tr("Warns:"));
|
||||
userContextNotesLabel->setText(tr("Admin Notes:"));
|
||||
userContextRecentReportsLabel->setText(tr("Recent Reports Against User:"));
|
||||
userContextAccountAge->setText(QString());
|
||||
userContextReports->setText(QString());
|
||||
userContextBans->setText(QString());
|
||||
userContextWarns->setText(QString());
|
||||
|
||||
statsGroup->setTitle(tr("Report Statistics"));
|
||||
|
||||
assignButton->setText(tr("Assign to Me"));
|
||||
resolveButton->setText(tr("Resolve"));
|
||||
resolveWithNoteButton->setText(tr("Resolve with note..."));
|
||||
dismissButton->setText(tr("Dismiss..."));
|
||||
viewReplayButton->setText(tr("View Replay"));
|
||||
joinGameButton->setText(tr("Join Game"));
|
||||
}
|
||||
|
||||
void TabReport::refreshList()
|
||||
{
|
||||
selectedReportIdBeforeRefresh = selectedReportId();
|
||||
commentDraftBeforeRefresh = commentInput->text();
|
||||
previousSelectedReportValid = selectedReportInfo(previousSelectedReport);
|
||||
|
||||
refreshButton->setEnabled(false);
|
||||
statusLabel->setText(tr("Loading..."));
|
||||
table->setRowCount(0);
|
||||
allReports.clear();
|
||||
filteredReports.clear();
|
||||
commentButton->setEnabled(false);
|
||||
setActionsEnabled(false);
|
||||
|
||||
Command_ReportList cmd;
|
||||
cmd.set_unresolved_only(unresolvedOnlyBox->isChecked());
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::reportListResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::reportListResponse(const Response &response)
|
||||
{
|
||||
refreshButton->setEnabled(true);
|
||||
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statusLabel->setText(tr("Failed to load reports."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportList &resp = response.GetExtension(Response_ReportList::ext);
|
||||
allReports.clear();
|
||||
for (int i = 0; i < resp.reports_size(); ++i) {
|
||||
allReports.append(resp.reports(i));
|
||||
}
|
||||
|
||||
applyFilters();
|
||||
updateStats();
|
||||
|
||||
if (selectedReportIdBeforeRefresh >= 0) {
|
||||
bool found = false;
|
||||
for (int row = 0; row < table->rowCount(); ++row) {
|
||||
if (table->item(row, COL_ID) &&
|
||||
table->item(row, COL_ID)->data(Qt::UserRole).toInt() == selectedReportIdBeforeRefresh) {
|
||||
found = true;
|
||||
{
|
||||
QSignalBlocker blocker(table);
|
||||
table->setCurrentCell(row, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
bool dataChanged = false;
|
||||
ServerInfo_Report newReport;
|
||||
if (!selectedReportInfo(newReport) || !previousSelectedReportValid ||
|
||||
previousSelectedReport.description() != newReport.description() ||
|
||||
previousSelectedReport.status() != newReport.status() ||
|
||||
previousSelectedReport.resolution_note() != newReport.resolution_note() ||
|
||||
previousSelectedReport.assigned_mod_name() != newReport.assigned_mod_name()) {
|
||||
dataChanged = true;
|
||||
}
|
||||
|
||||
if (dataChanged || detailsRequestedReportId != selectedReportIdBeforeRefresh) {
|
||||
loadReportDetails(selectedReportIdBeforeRefresh);
|
||||
}
|
||||
updateActionStates();
|
||||
} else {
|
||||
descriptionEdit->clear();
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
setActionsEnabled(false);
|
||||
userContextGroup->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (commentInput->text().isEmpty()) {
|
||||
commentInput->setText(commentDraftBeforeRefresh);
|
||||
}
|
||||
|
||||
if (statsGroup->isChecked()) {
|
||||
requestStats();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::applyFilters()
|
||||
{
|
||||
filteredReports.clear();
|
||||
|
||||
const QString searchText = searchEdit->text().toLower();
|
||||
const QString statusFilterValue = statusFilter->currentData().toString();
|
||||
|
||||
for (const ServerInfo_Report &r : allReports) {
|
||||
bool matchesSearch = searchText.isEmpty() ||
|
||||
QString::fromStdString(r.reporter_name()).toLower().contains(searchText) ||
|
||||
QString::fromStdString(r.reported_user_name()).toLower().contains(searchText) ||
|
||||
QString::fromStdString(r.category()).toLower().contains(searchText) ||
|
||||
QString::fromStdString(r.description()).toLower().contains(searchText);
|
||||
|
||||
bool matchesStatus = statusFilterValue.isEmpty() || QString::fromStdString(r.status()) == statusFilterValue;
|
||||
|
||||
if (matchesSearch && matchesStatus) {
|
||||
filteredReports.append(r);
|
||||
}
|
||||
}
|
||||
|
||||
table->setSortingEnabled(false);
|
||||
table->setRowCount(filteredReports.size());
|
||||
|
||||
for (int row = 0; row < filteredReports.size(); ++row) {
|
||||
const ServerInfo_Report &r = filteredReports[row];
|
||||
|
||||
report_utils::fillReportTableRow(table, row, r, COL_ID, COL_TIME, COL_REPORTED, COL_CATEGORY, COL_GAMEID,
|
||||
COL_STATUS, COL_ASSIGNED);
|
||||
|
||||
table->setItem(row, COL_REPORTER, new QTableWidgetItem(QString::fromStdString(r.reporter_name())));
|
||||
|
||||
table->setItem(row, COL_REPLAY,
|
||||
new QTableWidgetItem(r.has_replay_id() && r.replay_id() > 0 ? tr("Yes") : tr("No")));
|
||||
|
||||
table->setItem(row, COL_ROOM,
|
||||
new QTableWidgetItem(r.has_room_id() && r.room_id() > 0 ? QString::number(r.room_id()) : ""));
|
||||
}
|
||||
|
||||
table->setSortingEnabled(true);
|
||||
table->resizeColumnsToContents();
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
|
||||
statusLabel->setText(tr("%1 report(s)").arg(filteredReports.size()));
|
||||
}
|
||||
|
||||
void TabReport::updateStats()
|
||||
{
|
||||
int open = 0, assigned = 0, resolved = 0, dismissed = 0;
|
||||
for (const ServerInfo_Report &r : allReports) {
|
||||
QString status = QString::fromStdString(r.status());
|
||||
if (status == "open") {
|
||||
open++;
|
||||
} else if (status == "assigned") {
|
||||
assigned++;
|
||||
} else if (status == "resolved") {
|
||||
resolved++;
|
||||
} else if (status == "dismissed") {
|
||||
dismissed++;
|
||||
}
|
||||
}
|
||||
|
||||
statsLabel->setText(tr("%1 open | %2 assigned | %3 resolved | %4 dismissed")
|
||||
.arg(open)
|
||||
.arg(assigned)
|
||||
.arg(resolved)
|
||||
.arg(dismissed));
|
||||
}
|
||||
|
||||
void TabReport::onSelectionChanged()
|
||||
{
|
||||
const int row = table->currentRow();
|
||||
if (row < 0 || !table->item(row, COL_ID)) {
|
||||
descriptionEdit->clear();
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
commentInput->clear();
|
||||
commentButton->setEnabled(false);
|
||||
setActionsEnabled(false);
|
||||
userContextGroup->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const int reportId = table->item(row, COL_ID)->data(Qt::UserRole).toInt();
|
||||
loadReportDetails(reportId);
|
||||
updateActionStates();
|
||||
}
|
||||
|
||||
void TabReport::loadReportDetails(int reportId)
|
||||
{
|
||||
detailsRequestedReportId = reportId;
|
||||
|
||||
for (const ServerInfo_Report &r : filteredReports) {
|
||||
if (r.report_id() == reportId) {
|
||||
descriptionEdit->setPlainText(QString::fromStdString(r.description()));
|
||||
|
||||
QString reportedUser = QString::fromStdString(r.reported_user_name());
|
||||
if (!reportedUser.isEmpty()) {
|
||||
requestUserInfo(reportedUser);
|
||||
} else {
|
||||
userContextGroup->setVisible(false);
|
||||
}
|
||||
|
||||
QString status = QString::fromStdString(r.status());
|
||||
bool canComment = (status == "open" || status == "assigned");
|
||||
commentButton->setEnabled(canComment);
|
||||
commentInput->setEnabled(canComment);
|
||||
if (!canComment) {
|
||||
commentInput->setPlaceholderText(tr("This report is closed."));
|
||||
} else {
|
||||
commentInput->setPlaceholderText(tr("Type a reply..."));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chatLogEdit->setPlainText(tr("Loading..."));
|
||||
commentsEdit->setPlainText(tr("Loading..."));
|
||||
|
||||
Command_ReportDetails cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
|
||||
const int seq = ++detailsRequestSeq;
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, [this, seq](const Response &response) {
|
||||
if (seq != detailsRequestSeq) {
|
||||
return;
|
||||
}
|
||||
reportDetailsResponse(response);
|
||||
});
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::reportDetailsResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
if (selectedReportId() == detailsRequestedReportId) {
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->setPlainText(tr("Failed to load report details."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportDetails &resp = response.GetExtension(Response_ReportDetails::ext);
|
||||
const ServerInfo_Report &r = resp.report();
|
||||
|
||||
if (selectedReportId() != r.report_id()) {
|
||||
return;
|
||||
}
|
||||
|
||||
report_utils::renderReportDetails(chatLogEdit, commentsEdit, r, tr("No comments yet."), tr("[Moderator]"),
|
||||
tr("[Reporter]"));
|
||||
}
|
||||
|
||||
void TabReport::updateActionStates()
|
||||
{
|
||||
const int row = table->currentRow();
|
||||
if (row < 0 || !table->item(row, COL_STATUS)) {
|
||||
setActionsEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ServerInfo_Report report;
|
||||
if (!selectedReportInfo(report)) {
|
||||
setActionsEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const QString status = QString::fromStdString(report.status());
|
||||
assignButton->setEnabled(status == "open");
|
||||
resolveButton->setEnabled(status == "open" || status == "assigned");
|
||||
resolveWithNoteButton->setEnabled(status == "open" || status == "assigned");
|
||||
dismissButton->setEnabled(status == "open" || status == "assigned");
|
||||
|
||||
const bool hasGameId = report.game_id() > 0;
|
||||
const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0;
|
||||
viewReplayButton->setEnabled(hasReplay);
|
||||
joinGameButton->setEnabled(hasGameId && report.has_room_id() && report.room_id() > 0);
|
||||
}
|
||||
|
||||
void TabReport::setActionsEnabled(bool enabled)
|
||||
{
|
||||
assignButton->setEnabled(enabled);
|
||||
resolveButton->setEnabled(enabled);
|
||||
resolveWithNoteButton->setEnabled(enabled);
|
||||
dismissButton->setEnabled(enabled);
|
||||
viewReplayButton->setEnabled(false);
|
||||
joinGameButton->setEnabled(false);
|
||||
commentButton->setEnabled(enabled);
|
||||
commentInput->setEnabled(enabled);
|
||||
}
|
||||
|
||||
int TabReport::selectedReportId() const
|
||||
{
|
||||
const int row = table->currentRow();
|
||||
if (row < 0 || !table->item(row, COL_ID)) {
|
||||
return -1;
|
||||
}
|
||||
return table->item(row, COL_ID)->data(Qt::UserRole).toInt();
|
||||
}
|
||||
|
||||
bool TabReport::selectedReportInfo(ServerInfo_Report &info) const
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ServerInfo_Report &r : filteredReports) {
|
||||
if (r.report_id() == reportId) {
|
||||
info = r;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void TabReport::assignReport()
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionsEnabled(false);
|
||||
statusLabel->setText(tr("Assigning..."));
|
||||
|
||||
Command_ReportAssign cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::assignResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::assignResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
statusLabel->setText(tr("Assigned."));
|
||||
refreshList();
|
||||
} else {
|
||||
statusLabel->setText(tr("Assignment failed."));
|
||||
updateActionStates();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::resolveReport(bool dismissed, bool promptNote)
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString note;
|
||||
if (promptNote) {
|
||||
bool ok;
|
||||
note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"),
|
||||
dismissed ? tr("Optional note:") : tr("Resolution note (optional):"),
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setActionsEnabled(false);
|
||||
statusLabel->setText(dismissed ? tr("Dismissing...") : tr("Resolving..."));
|
||||
|
||||
Command_ReportResolve cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
cmd.set_dismissed(dismissed);
|
||||
if (!note.isEmpty()) {
|
||||
cmd.set_resolution_note(note.toStdString());
|
||||
}
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::resolveResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::resolveResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
statusLabel->setText(tr("Done."));
|
||||
refreshList();
|
||||
} else {
|
||||
statusLabel->setText(tr("Action failed."));
|
||||
updateActionStates();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::viewReplay()
|
||||
{
|
||||
ServerInfo_Report report;
|
||||
if (!selectedReportInfo(report) || report.game_id() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionsEnabled(false);
|
||||
statusLabel->setText(tr("Loading replay..."));
|
||||
|
||||
Command_ReplayDownloadByGameId cmd;
|
||||
cmd.set_game_id(report.game_id());
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::viewReplayResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::viewReplayResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statusLabel->setText(tr("No replay available for this game."));
|
||||
updateActionStates();
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReplayDownloadByGameId &resp = response.GetExtension(Response_ReplayDownloadByGameId::ext);
|
||||
GameReplay *replay = new GameReplay;
|
||||
if (replay->ParseFromString(resp.replay_data())) {
|
||||
emit openReplay(replay);
|
||||
statusLabel->setText(tr("Replay opened."));
|
||||
} else {
|
||||
delete replay;
|
||||
statusLabel->setText(tr("Failed to parse replay."));
|
||||
}
|
||||
|
||||
updateActionStates();
|
||||
}
|
||||
|
||||
void TabReport::joinGame()
|
||||
{
|
||||
ServerInfo_Report report;
|
||||
if (!selectedReportInfo(report) || report.game_id() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int roomId = report.has_room_id() ? report.room_id() : -1;
|
||||
if (roomId <= 0) {
|
||||
statusLabel->setText(tr("No room recorded for this report, use the replay instead."));
|
||||
return;
|
||||
}
|
||||
|
||||
emit requestJoinGame(report.game_id(), roomId);
|
||||
statusLabel->setText(tr("Attempting to join game..."));
|
||||
}
|
||||
|
||||
void TabReport::addComment()
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text = commentInput->text().trimmed();
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
commentButton->setEnabled(false);
|
||||
|
||||
Command_ReportAddComment cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
cmd.set_comment(text.toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::addCommentResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::addCommentResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
commentInput->clear();
|
||||
refreshList();
|
||||
} else {
|
||||
commentButton->setEnabled(true);
|
||||
statusLabel->setText(tr("Failed to send comment."));
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::requestUserInfo(const QString &userName)
|
||||
{
|
||||
if (userName.isEmpty()) {
|
||||
userContextGroup->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
lastRequestedUser = userName;
|
||||
userContextGroup->setVisible(true);
|
||||
userContextName->setText(userName);
|
||||
userContextAccountAge->setText(tr("Loading..."));
|
||||
userContextReports->clear();
|
||||
userContextBans->clear();
|
||||
userContextWarns->clear();
|
||||
userContextNotes->clear();
|
||||
userContextRecentReports->clear();
|
||||
|
||||
Command_ReportUserInfo cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::userInfoResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::userInfoResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
userContextAccountAge->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()) != lastRequestedUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDateTime regTime = QDateTime::fromSecsSinceEpoch(resp.registration_time());
|
||||
qint64 daysSinceReg = regTime.daysTo(QDateTime::currentDateTime());
|
||||
userContextAccountAge->setText(tr("%1 days (since %2)").arg(daysSinceReg).arg(regTime.toString("yyyy-MM-dd")));
|
||||
|
||||
userContextReports->setText(QString::number(resp.total_reports()));
|
||||
userContextBans->setText(QString::number(resp.total_bans()));
|
||||
userContextWarns->setText(QString::number(resp.total_warns()));
|
||||
|
||||
if (resp.has_admin_notes() && !resp.admin_notes().empty()) {
|
||||
userContextNotes->setPlainText(QString::fromStdString(resp.admin_notes()));
|
||||
} else {
|
||||
userContextNotes->setPlainText(tr("(none)"));
|
||||
}
|
||||
|
||||
userContextRecentReports->clear();
|
||||
if (resp.recent_reports_size() == 0) {
|
||||
userContextRecentReports->setPlainText(tr("No previous reports against this user."));
|
||||
} else {
|
||||
for (int i = 0; i < resp.recent_reports_size(); ++i) {
|
||||
const ServerInfo_Report &r = resp.recent_reports(i);
|
||||
QDateTime dt = QDateTime::fromSecsSinceEpoch(r.report_time());
|
||||
userContextRecentReports->moveCursor(QTextCursor::End);
|
||||
userContextRecentReports->insertPlainText(QString("[%1] #%2 by %3 [%4]: %5\n")
|
||||
.arg(dt.toString("yyyy-MM-dd"))
|
||||
.arg(r.report_id())
|
||||
.arg(QString::fromStdString(r.reporter_name()))
|
||||
.arg(QString::fromStdString(r.status()))
|
||||
.arg(QString::fromStdString(r.category())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::requestStats()
|
||||
{
|
||||
statsTotalLabel->setText(tr("Loading statistics..."));
|
||||
statsTrendLabel->clear();
|
||||
statsCategoriesLabel->clear();
|
||||
statsDetailText->clear();
|
||||
|
||||
Command_ReportStats cmd;
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::statsResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::statsResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statsTotalLabel->setText(tr("Error loading statistics."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportStats &resp = response.GetExtension(Response_ReportStats::ext);
|
||||
|
||||
statsTotalLabel->setText(tr("Total: %1 reports (%2 open, %3 assigned, %4 resolved/dismissed)")
|
||||
.arg(resp.total_reports())
|
||||
.arg(resp.total_pending())
|
||||
.arg(resp.total_assigned())
|
||||
.arg(resp.total_resolved()));
|
||||
|
||||
statsTrendLabel->setText(tr("Last 24h: %1 | Last 7d: %2 | Last 30d: %3 | Avg resolution: %4h")
|
||||
.arg(resp.reports_last_24h())
|
||||
.arg(resp.reports_last_7d())
|
||||
.arg(resp.reports_last_30d())
|
||||
.arg(resp.avg_resolution_hours(), 0, 'f', 1));
|
||||
|
||||
QString weekCompare =
|
||||
tr("This week: %1 vs last week: %2 (%3%)")
|
||||
.arg(resp.reports_this_week())
|
||||
.arg(resp.reports_last_week())
|
||||
.arg(resp.reports_last_week() > 0 ? QString::number(((resp.reports_this_week() - resp.reports_last_week()) *
|
||||
100.0 / resp.reports_last_week()),
|
||||
'f', 0)
|
||||
: resp.reports_this_week() > 0 ? "new"
|
||||
: "0");
|
||||
statsTrendLabel->setText(statsTrendLabel->text() + " | " + weekCompare);
|
||||
|
||||
statsCategoriesLabel->setText(tr("By category:"));
|
||||
statsDetailText->clear();
|
||||
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText(tr("=== Top Categories ===") + "\n");
|
||||
for (int i = 0; i < resp.category_counts_size(); ++i) {
|
||||
const ReportCategoryCount &cc = resp.category_counts(i);
|
||||
QString cat = QString::fromStdString(cc.category());
|
||||
cat.replace('_', ' ');
|
||||
cat = cat.left(1).toUpper() + cat.mid(1);
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText(QString(" %1: %2\n").arg(cat).arg(cc.count()));
|
||||
}
|
||||
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText("\n" + tr("=== Most Reported Users ===") + "\n");
|
||||
for (int i = 0; i < resp.top_reported_users_size(); ++i) {
|
||||
const ReportTopUser &tu = resp.top_reported_users(i);
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText(
|
||||
QString(" %1: %2 reports\n").arg(QString::fromStdString(tu.user_name())).arg(tu.count()));
|
||||
}
|
||||
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText("\n" + tr("=== Top Reporters ===") + "\n");
|
||||
for (int i = 0; i < resp.top_reporters_size(); ++i) {
|
||||
const ReportTopUser &tu = resp.top_reporters(i);
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText(
|
||||
QString(" %1: %2 reports filed\n").arg(QString::fromStdString(tu.user_name())).arg(tu.count()));
|
||||
}
|
||||
}
|
||||
124
cockatrice/src/interface/widgets/tabs/tab_report.h
Normal file
124
cockatrice/src/interface/widgets/tabs/tab_report.h
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#ifndef TAB_REPORT_H
|
||||
#define TAB_REPORT_H
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
#include <QList>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QSplitter;
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
class QTimer;
|
||||
class GameReplay;
|
||||
|
||||
class TabReport : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi() override;
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Report Queue");
|
||||
}
|
||||
|
||||
signals:
|
||||
void openReplay(GameReplay *replay);
|
||||
void requestJoinGame(int gameId, int roomId);
|
||||
|
||||
private slots:
|
||||
void refreshList();
|
||||
void reportListResponse(const Response &response);
|
||||
void assignReport();
|
||||
void assignResponse(const Response &response);
|
||||
void resolveReport(bool dismissed, bool promptNote);
|
||||
void resolveResponse(const Response &response);
|
||||
void onSelectionChanged();
|
||||
void viewReplay();
|
||||
void viewReplayResponse(const Response &response);
|
||||
void joinGame();
|
||||
void addComment();
|
||||
void addCommentResponse(const Response &response);
|
||||
void requestUserInfo(const QString &userName);
|
||||
void userInfoResponse(const Response &response);
|
||||
void reportDetailsResponse(const Response &response);
|
||||
void requestStats();
|
||||
void statsResponse(const Response &response);
|
||||
|
||||
private:
|
||||
int selectedReportId() const;
|
||||
bool selectedReportInfo(ServerInfo_Report &info) const;
|
||||
void setActionsEnabled(bool enabled);
|
||||
void updateActionStates();
|
||||
void applyFilters();
|
||||
void updateStats();
|
||||
void loadReportDetails(int reportId);
|
||||
|
||||
AbstractClient *client;
|
||||
|
||||
QLineEdit *searchEdit;
|
||||
QComboBox *statusFilter;
|
||||
QCheckBox *unresolvedOnlyBox;
|
||||
QPushButton *refreshButton;
|
||||
QLabel *statsLabel;
|
||||
QTableWidget *table;
|
||||
QSplitter *detailSplitter;
|
||||
QGroupBox *descGroup;
|
||||
QGroupBox *chatGroup;
|
||||
QGroupBox *commentsGroup;
|
||||
QTextEdit *descriptionEdit;
|
||||
QTextEdit *chatLogEdit;
|
||||
QTextEdit *commentsEdit;
|
||||
QLineEdit *commentInput;
|
||||
QPushButton *commentButton;
|
||||
QPushButton *assignButton;
|
||||
QPushButton *resolveButton;
|
||||
QPushButton *resolveWithNoteButton;
|
||||
QPushButton *dismissButton;
|
||||
QPushButton *viewReplayButton;
|
||||
QPushButton *joinGameButton;
|
||||
QLabel *statusLabel;
|
||||
QTimer *refreshTimer;
|
||||
|
||||
QGroupBox *userContextGroup;
|
||||
QLabel *userContextName;
|
||||
QLabel *userContextAccountAge;
|
||||
QLabel *userContextReports;
|
||||
QLabel *userContextBans;
|
||||
QLabel *userContextWarns;
|
||||
QTextEdit *userContextNotes;
|
||||
QTextEdit *userContextRecentReports;
|
||||
QLabel *userContextUserLabel;
|
||||
QLabel *userContextAgeLabel;
|
||||
QLabel *userContextReportsLabel;
|
||||
QLabel *userContextBansLabel;
|
||||
QLabel *userContextWarnsLabel;
|
||||
QLabel *userContextNotesLabel;
|
||||
QLabel *userContextRecentReportsLabel;
|
||||
QString lastRequestedUser;
|
||||
QGroupBox *statsGroup;
|
||||
QLabel *statsTotalLabel;
|
||||
QLabel *statsTrendLabel;
|
||||
QLabel *statsCategoriesLabel;
|
||||
QTextEdit *statsDetailText;
|
||||
|
||||
QList<ServerInfo_Report> allReports;
|
||||
QList<ServerInfo_Report> filteredReports;
|
||||
int detailsRequestedReportId = -1;
|
||||
int detailsRequestSeq = 0;
|
||||
int selectedReportIdBeforeRefresh = -1;
|
||||
QString commentDraftBeforeRefresh;
|
||||
ServerInfo_Report previousSelectedReport;
|
||||
bool previousSelectedReportValid = false;
|
||||
};
|
||||
|
||||
#endif // TAB_REPORT_H
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../intents/intent_join_server_game.h"
|
||||
#include "../interface/pixel_map_generator.h"
|
||||
#include "../interface/widgets/server/game_link.h"
|
||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||
|
|
@ -18,7 +19,9 @@
|
|||
#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"
|
||||
#include "tab_server.h"
|
||||
#include "tab_visual_database_display.h"
|
||||
|
|
@ -31,6 +34,7 @@
|
|||
#include <QPainter>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_joined.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_notify_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_user_message.pb.h>
|
||||
|
|
@ -39,6 +43,7 @@
|
|||
#include <libcockatrice/protocol/pb/room_event.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/chat_settings.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
|
@ -113,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), isLocalGame(false)
|
||||
tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), isLocalGame(false)
|
||||
{
|
||||
setElideMode(Qt::ElideRight);
|
||||
setMovable(true);
|
||||
|
|
@ -190,6 +195,14 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
|
|||
aTabLog->setCheckable(true);
|
||||
connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog);
|
||||
|
||||
aTabReport = new QAction(this);
|
||||
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();
|
||||
|
|
@ -229,6 +242,8 @@ void TabSupervisor::retranslateUi()
|
|||
aTabReplays->setText(tr("Game Replays"));
|
||||
aTabAdmin->setText(tr("Administration"));
|
||||
aTabLog->setText(tr("Logs"));
|
||||
aTabReport->setText(tr("Report Queue"));
|
||||
aTabModeration->setText(tr("Moderation"));
|
||||
|
||||
// tabs
|
||||
QList<Tab *> tabs;
|
||||
|
|
@ -238,6 +253,8 @@ void TabSupervisor::retranslateUi()
|
|||
tabs.append(tabAdmin);
|
||||
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());
|
||||
|
|
@ -284,6 +301,8 @@ void TabSupervisor::refreshShortcuts()
|
|||
aTabReplays->setShortcuts(shortcuts.getShortcut("Tabs/aTabReplays"));
|
||||
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)
|
||||
|
|
@ -485,6 +504,8 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
|||
tabsMenu->addAction(aTabAdmin);
|
||||
tabsMenu->addAction(aTabLog);
|
||||
tabsMenu->addAction(aTabCardArtRules);
|
||||
tabsMenu->addAction(aTabReport);
|
||||
tabsMenu->addAction(aTabModeration);
|
||||
|
||||
if (SettingsCache::instance().tabs().getTabAdminOpen()) {
|
||||
openTabAdmin();
|
||||
|
|
@ -492,6 +513,12 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
|||
if (SettingsCache::instance().tabs().getTabLogOpen()) {
|
||||
openTabLog();
|
||||
}
|
||||
if (SettingsCache::instance().tabs().getTabReportOpen()) {
|
||||
openTabReport();
|
||||
}
|
||||
if (SettingsCache::instance().tabs().getTabModerationOpen()) {
|
||||
openTabModeration();
|
||||
}
|
||||
openTabCardArtRules();
|
||||
}
|
||||
|
||||
|
|
@ -505,6 +532,8 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
|
|||
tabAccount = nullptr;
|
||||
tabAdmin = nullptr;
|
||||
tabLog = nullptr;
|
||||
tabReport = nullptr;
|
||||
tabModeration = nullptr;
|
||||
isLocalGame = true;
|
||||
userInfo = new ServerInfo_User;
|
||||
localClients = _clients;
|
||||
|
|
@ -546,6 +575,12 @@ void TabSupervisor::stop()
|
|||
if (tabLog) {
|
||||
tabLog->close();
|
||||
}
|
||||
if (tabReport) {
|
||||
tabReport->close();
|
||||
}
|
||||
if (tabModeration) {
|
||||
tabModeration->close();
|
||||
}
|
||||
}
|
||||
|
||||
QList<Tab *> tabsToDelete;
|
||||
|
|
@ -783,6 +818,59 @@ void TabSupervisor::openTabLog()
|
|||
aTabLog->setChecked(true);
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabReport(bool checked)
|
||||
{
|
||||
SettingsCache::instance().tabs().setTabReportOpen(checked);
|
||||
if (checked && !tabReport) {
|
||||
openTabReport();
|
||||
setCurrentWidget(tabReport);
|
||||
} else if (!checked && tabReport) {
|
||||
tabReport->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::openTabReport()
|
||||
{
|
||||
tabReport = new TabReport(this, client);
|
||||
myAddTab(tabReport, aTabReport);
|
||||
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
|
||||
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
|
||||
connect(tabReport, &QObject::destroyed, this, [this] {
|
||||
tabReport = nullptr;
|
||||
aTabReport->setChecked(false);
|
||||
});
|
||||
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) {
|
||||
|
|
@ -900,6 +988,30 @@ void TabSupervisor::replayLeft(TabGame *tab)
|
|||
replayTabs.removeOne(tab);
|
||||
}
|
||||
|
||||
void TabSupervisor::joinReportGame(const int gameId, const int roomId)
|
||||
{
|
||||
auto *remoteClient = qobject_cast<RemoteClient *>(client);
|
||||
if (!remoteClient) {
|
||||
actShowPopup(tr("Report joins are only available on a remote server."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto ctx = std::make_unique<ContextJoinGame>();
|
||||
ctx->roomContext.serverContext.hostname = remoteClient->peerName();
|
||||
ctx->roomContext.serverContext.port = QString::number(remoteClient->peerPort());
|
||||
ctx->roomContext.roomId = roomId;
|
||||
ctx->gameId = gameId;
|
||||
ctx->asSpectator = true;
|
||||
|
||||
auto *joinGameIntent = new IntentJoinServerGame(this, remoteClient, std::move(ctx));
|
||||
joinGameIntent->setParent(this);
|
||||
connect(joinGameIntent, &Intent::failed, this, [gameId](const QString &reason) {
|
||||
actShowPopup(tr("Could not join game %1.\n%2").arg(gameId).arg(reason));
|
||||
});
|
||||
|
||||
joinGameIntent->execute();
|
||||
}
|
||||
|
||||
TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus)
|
||||
{
|
||||
if (receiverName == QString::fromStdString(userInfo->name())) {
|
||||
|
|
@ -1284,6 +1396,24 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
|
|||
}
|
||||
break;
|
||||
}
|
||||
case Event_NotifyUser::REPORT_RESOLVED: {
|
||||
QString title = QString::fromStdString(event.custom_title()).simplified();
|
||||
QString content = QString::fromStdString(event.custom_content()).trimmed();
|
||||
if (!title.isEmpty() && !content.isEmpty()) {
|
||||
actShowPopup(title + "\n" + content);
|
||||
QApplication::alert(this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Event_NotifyUser::REPORT_COMMENT: {
|
||||
QString title = QString::fromStdString(event.custom_title()).simplified();
|
||||
QString content = QString::fromStdString(event.custom_content()).trimmed();
|
||||
if (!title.isEmpty() && !content.isEmpty()) {
|
||||
actShowPopup(title + "\n" + content);
|
||||
QApplication::alert(this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ class TabDeckStorage;
|
|||
class TabReplays;
|
||||
class TabAdmin;
|
||||
class TabMessage;
|
||||
class TabReport;
|
||||
class TabModeration;
|
||||
class TabAccount;
|
||||
class TabDeckEditor;
|
||||
class TabLog;
|
||||
|
|
@ -103,6 +105,8 @@ private:
|
|||
TabAdmin *tabAdmin;
|
||||
TabCardArtRules *tabCardArtRules;
|
||||
TabLog *tabLog;
|
||||
TabReport *tabReport;
|
||||
TabModeration *tabModeration;
|
||||
QMap<int, TabRoom *> roomTabs;
|
||||
QMap<int, TabGame *> gameTabs;
|
||||
QList<TabGame *> replayTabs;
|
||||
|
|
@ -112,7 +116,7 @@ private:
|
|||
|
||||
QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage,
|
||||
*aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin,
|
||||
*aTabCardArtRules, *aTabLog;
|
||||
*aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration;
|
||||
|
||||
int myAddTab(Tab *tab, QAction *manager = nullptr);
|
||||
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
|
||||
|
|
@ -183,6 +187,8 @@ public slots:
|
|||
TabArchidekt *addArchidektTab();
|
||||
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);
|
||||
|
|
@ -198,6 +204,8 @@ private slots:
|
|||
void actTabDeckStorage(bool checked);
|
||||
void actTabAdmin(bool checked);
|
||||
void actTabLog(bool checked);
|
||||
void actTabReport(bool checked);
|
||||
void actTabModeration(bool checked);
|
||||
|
||||
void openTabVisualDeckStorage();
|
||||
void openTabHome();
|
||||
|
|
@ -208,6 +216,7 @@ private slots:
|
|||
void actTabCardArtRules(bool checked);
|
||||
void openTabCardArtRules();
|
||||
void openTabLog();
|
||||
void openTabReport();
|
||||
|
||||
void updateCurrent(int index);
|
||||
void updatePingTime(int value, int max);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue