[Client] Moderation and alts dashboards

Turn the two staff tables into dashboards without behavior changes: TabModeration gets an identity header (display-size name, status, registered/last-login subline), Reports/Warnings/Bans/Alts count cards with an Open-graph deep link, full-width admin notes, and Alts/Sessions/Staff tabbed sections with tab memory. TabAlts gets toggle chips with match counts, bold usernames, palette-token dimming of shared-IP rows, and a resizable side card for the selected-user timeline with selection restore. Row activation works via Enter as well as double-click; async responses are guarded against stale investigations and retranslation re-applies cached dashboard data.
This commit is contained in:
Lukas Brübach 2026-10-13 21:26:45 +02:00
parent e36b59c2f5
commit 459c61933b
4 changed files with 286 additions and 126 deletions

View file

@ -5,17 +5,19 @@
#include "libcockatrice/protocol/pb/response_user_alts.pb.h"
#include <QAbstractItemView>
#include <QCheckBox>
#include <QDateTime>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QItemSelectionModel>
#include <QLabel>
#include <QLineEdit>
#include <QList>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSpinBox>
#include <QSplitter>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QTextEdit>
@ -46,6 +48,17 @@ constexpr int COL_ALTS_BANS = 9;
constexpr int COL_ALTS_ACTIVE = 10;
constexpr int COL_ALTS_COUNT = 11;
constexpr int COL_WARNS_TIME = 0;
constexpr int COL_WARNS_MODERATOR = 1;
constexpr int COL_WARNS_REASON = 2;
constexpr int COL_WARNS_COUNT = 3;
constexpr int COL_BANS_TIME = 0;
constexpr int COL_BANS_MODERATOR = 1;
constexpr int COL_BANS_LENGTH = 2;
constexpr int COL_BANS_REASON = 3;
constexpr int COL_BANS_COUNT = 4;
constexpr int MAX_ALT_DEPTH = 3;
// Starting default only (adjustable 1-100000): shared VPN exits routinely
// cover dozens of users, so 10 hides the obvious VPN noise but keeps
@ -94,15 +107,16 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
topBar->addWidget(depthSpin);
topBar->addWidget(investigateButton);
emailCheck = new QCheckBox;
emailCheck->setChecked(true);
connect(emailCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
clientIdCheck = new QCheckBox;
clientIdCheck->setChecked(true);
connect(clientIdCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
ipCheck = new QCheckBox;
ipCheck->setChecked(true);
connect(ipCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
auto makeChip = [this](const QString &text) {
auto *chip = new QPushButton(text);
chip->setCheckable(true);
chip->setChecked(true);
connect(chip, &QPushButton::toggled, this, &TabAlts::onFilterChanged);
return chip;
};
emailChip = makeChip(QString());
clientIdChip = makeChip(QString());
ipChip = makeChip(QString());
vpnLabel = new QLabel;
vpnSpin = new QSpinBox;
vpnSpin->setMinimum(1);
@ -111,17 +125,20 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
connect(vpnSpin, &QSpinBox::valueChanged, this, &TabAlts::onFilterChanged);
auto *filterBar = new QHBoxLayout;
filterBar->addWidget(emailCheck);
filterBar->addWidget(clientIdCheck);
filterBar->addWidget(ipCheck);
filterBar->addWidget(emailChip);
filterBar->addWidget(clientIdChip);
filterBar->addWidget(ipChip);
filterBar->addWidget(vpnLabel);
filterBar->addWidget(vpnSpin);
filterBar->addStretch();
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
configureAltTable(altsTable);
connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
auto *userItem = altsTable->item(row, COL_ALTS_USER);
connect(altsTable, &QTableWidget::activated, this, [this](const QModelIndex &index) {
if (!index.isValid()) {
return;
}
auto *userItem = altsTable->item(index.row(), COL_ALTS_USER);
if (userItem != nullptr && !userItem->text().isEmpty() && userItem->text() != currentUser) {
investigate(userItem->text());
}
@ -134,9 +151,9 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
detailNotesEdit->setReadOnly(true);
// Compact cap so the warn/ban tables stay visible without scrolling the tab.
detailNotesEdit->setMaximumHeight(60);
warnsTable = new QTableWidget(0, 3);
warnsTable = new QTableWidget(0, COL_WARNS_COUNT);
configureAltTable(warnsTable);
bansTable = new QTableWidget(0, 4);
bansTable = new QTableWidget(0, COL_BANS_COUNT);
configureAltTable(bansTable);
auto *detailGrid = new QGridLayout;
@ -150,21 +167,24 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
detailGrid->addWidget(detailNotesLabel, 1, 0);
detailGrid->addWidget(detailNotesEdit, 1, 1, 1, 3);
auto *detailTables = new QHBoxLayout;
detailTables->addWidget(warnsTable);
detailTables->addWidget(bansTable);
detailsGroup = new QGroupBox;
auto *detailsLayout = new QVBoxLayout(detailsGroup);
detailsLayout->addLayout(detailGrid);
detailsLayout->addLayout(detailTables);
detailsLayout->addWidget(warnsTable, 1);
detailsLayout->addWidget(bansTable, 1);
auto *contentSplitter = new QSplitter(Qt::Horizontal);
contentSplitter->addWidget(altsTable);
contentSplitter->addWidget(detailsGroup);
contentSplitter->setStretchFactor(0, 3);
contentSplitter->setStretchFactor(1, 2);
detailsGroup->setMinimumWidth(260);
auto *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->addLayout(topBar);
mainLayout->addLayout(filterBar);
mainLayout->addWidget(statusLabel);
mainLayout->addWidget(altsTable, 3);
mainLayout->addWidget(detailsGroup, 2);
mainLayout->addWidget(contentSplitter, 1);
retranslateUi();
clearDetails();
@ -183,15 +203,19 @@ void TabAlts::retranslateUi()
statusLabel->setText(tr("Enter a user name to investigate."));
}
emailCheck->setText(tr("Email"));
clientIdCheck->setText(tr("Client ID"));
ipCheck->setText(tr("IP"));
emailChip->setText(tr("Email"));
emailChip->setToolTip(tr("Show or hide email matches"));
clientIdChip->setText(tr("Client ID"));
clientIdChip->setToolTip(tr("Show or hide client ID matches"));
ipChip->setText(tr("IP"));
ipChip->setToolTip(tr("Show or hide IP address matches"));
vpnLabel->setText(tr("Hide IPs shared by more than"));
vpnSpin->setSuffix(tr(" users"));
altsTable->setHorizontalHeaderLabels({tr("User"), tr("Via"), tr("Depth"), tr("Shared"), tr("eMail"),
tr("Client ID"), tr("Registered"), tr("Last login"), tr("Warns"), tr("Bans"),
tr("Active")});
altsTable->setToolTip(tr("Double-click or press Enter on a row to investigate that account"));
warnsTable->setHorizontalHeaderLabels({tr("Warning time"), tr("Moderator"), tr("Reason")});
bansTable->setHorizontalHeaderLabels({tr("Ban time"), tr("Moderator"), tr("Length"), tr("Reason")});
@ -199,6 +223,10 @@ void TabAlts::retranslateUi()
detailNameLabel->setText(tr("Name:"));
detailCountsLabel->setText(tr("Counts:"));
detailNotesLabel->setText(tr("Admin notes:"));
if (!lastAlts.isEmpty()) {
renderTable();
}
}
QString TabAlts::formatEpoch(quint64 ts) const
@ -259,7 +287,11 @@ void TabAlts::requestAlts(const QString &userName, int depth)
cmd.set_user_name(userName.toStdString());
cmd.set_depth(static_cast<google::protobuf::uint32>(depth));
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabAlts::altsResponse);
connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) {
if (userName == currentUser) {
altsResponse(response);
}
});
client->sendCommand(pend);
}
@ -290,13 +322,13 @@ bool TabAlts::rowVisible(const ServerInfo_UserAlt &alt) const
}
const QString reason = QString::fromStdString(alt.match_reason());
if (reason == QStringLiteral("email")) {
return emailCheck->isChecked();
return emailChip->isChecked();
}
if (reason.startsWith(QStringLiteral("clientid:"))) {
return clientIdCheck->isChecked();
return clientIdChip->isChecked();
}
if (reason.startsWith(QStringLiteral("ip:"))) {
return ipCheck->isChecked() &&
return ipChip->isChecked() &&
(!alt.has_shared_count() || alt.shared_count() <= static_cast<unsigned>(vpnSpin->value()));
}
return true;
@ -304,7 +336,32 @@ bool TabAlts::rowVisible(const ServerInfo_UserAlt &alt) const
void TabAlts::renderTable()
{
// Blocked so the programmatic refill neither wipes the details panel
// (via the selection-cleared path) nor refetches them on restore.
const QSignalBlocker blocker(altsTable);
altsTable->setRowCount(0);
int emailCount = 0;
int clientIdCount = 0;
int ipCount = 0;
for (const ServerInfo_UserAlt &alt : lastAlts) {
const QString reason = QString::fromStdString(alt.match_reason());
if (reason == QStringLiteral("email")) {
++emailCount;
} else if (reason.startsWith(QStringLiteral("clientid:"))) {
++clientIdCount;
} else if (reason.startsWith(QStringLiteral("ip:"))) {
++ipCount;
}
}
emailChip->setText(tr("Email (%1)").arg(emailCount));
clientIdChip->setText(tr("Client ID (%1)").arg(clientIdCount));
ipChip->setText(tr("IP (%1)").arg(ipCount));
QFont userFont = altsTable->font();
userFont.setBold(true);
const QColor dimColor = palette().color(QPalette::Disabled, QPalette::Text);
int hidden = 0;
int row = 0;
for (const ServerInfo_UserAlt &alt : lastAlts) {
@ -313,7 +370,9 @@ void TabAlts::renderTable()
continue;
}
altsTable->insertRow(row);
altsTable->setItem(row, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name())));
auto *userItem = new QTableWidgetItem(QString::fromStdString(alt.user_name()));
userItem->setFont(userFont);
altsTable->setItem(row, COL_ALTS_USER, userItem);
altsTable->setItem(row, COL_ALTS_VIA,
new QTableWidgetItem(alt.has_match_reason() && !alt.match_reason().empty()
? QString::fromStdString(alt.match_reason())
@ -330,6 +389,14 @@ void TabAlts::renderTable()
altsTable->setItem(row, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count())));
altsTable->setItem(row, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count())));
altsTable->setItem(row, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no")));
if (QString::fromStdString(alt.match_reason()).startsWith(QStringLiteral("ip:")) && alt.has_shared_count() &&
alt.shared_count() > 1) {
for (int col = 0; col < COL_ALTS_COUNT; ++col) {
if (auto *item = altsTable->item(row, col)) {
item->setForeground(dimColor);
}
}
}
++row;
}
if (hidden == 0) {
@ -341,6 +408,22 @@ void TabAlts::renderTable()
.arg(lastAlts.size())
.arg(currentUser));
}
if (!selectedUser.isEmpty()) {
bool restored = false;
for (int restoreRow = 0; restoreRow < altsTable->rowCount(); ++restoreRow) {
auto *userItem = altsTable->item(restoreRow, COL_ALTS_USER);
if (userItem != nullptr && userItem->text() == selectedUser) {
altsTable->setCurrentItem(userItem, QItemSelectionModel::Select | QItemSelectionModel::Rows);
restored = true;
break;
}
}
if (!restored) {
selectedUser.clear();
clearDetails();
}
}
}
void TabAlts::onFilterChanged()
@ -409,7 +492,7 @@ void TabAlts::requestDetails(const QString &userName)
void TabAlts::clearDetails()
{
detailNameValue->clear();
detailCountsValue->clear();
detailCountsValue->setText(tr("Select a row to inspect its history."));
detailNotesEdit->clear();
warnsTable->setRowCount(0);
bansTable->setRowCount(0);
@ -446,9 +529,9 @@ void TabAlts::warnHistoryResponse(const Response &response)
warnsTable->setRowCount(resp.warn_list_size());
for (int i = 0; i < resp.warn_list_size(); ++i) {
const ServerInfo_Warning &warn = resp.warn_list(i);
warnsTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(warn.time_of())));
warnsTable->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(warn.admin_name())));
warnsTable->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(warn.reason())));
warnsTable->setItem(i, COL_WARNS_TIME, new QTableWidgetItem(QString::fromStdString(warn.time_of())));
warnsTable->setItem(i, COL_WARNS_MODERATOR, new QTableWidgetItem(QString::fromStdString(warn.admin_name())));
warnsTable->setItem(i, COL_WARNS_REASON, new QTableWidgetItem(QString::fromStdString(warn.reason())));
}
}
@ -463,9 +546,9 @@ void TabAlts::banHistoryResponse(const Response &response)
bansTable->setRowCount(resp.ban_list_size());
for (int i = 0; i < resp.ban_list_size(); ++i) {
const ServerInfo_Ban &ban = resp.ban_list(i);
bansTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(ban.ban_time())));
bansTable->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(ban.admin_name())));
bansTable->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(ban.ban_length())));
bansTable->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(ban.ban_reason())));
bansTable->setItem(i, COL_BANS_TIME, new QTableWidgetItem(QString::fromStdString(ban.ban_time())));
bansTable->setItem(i, COL_BANS_MODERATOR, new QTableWidgetItem(QString::fromStdString(ban.admin_name())));
bansTable->setItem(i, COL_BANS_LENGTH, new QTableWidgetItem(QString::fromStdString(ban.ban_length())));
bansTable->setItem(i, COL_BANS_REASON, new QTableWidgetItem(QString::fromStdString(ban.ban_reason())));
}
}

View file

@ -10,7 +10,7 @@
#include <qtmetamacros.h>
class AbstractClient;
class QCheckBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QPushButton;
@ -70,9 +70,9 @@ private:
QLabel *statusLabel;
QTableWidget *altsTable;
QCheckBox *emailCheck;
QCheckBox *clientIdCheck;
QCheckBox *ipCheck;
QPushButton *emailChip;
QPushButton *clientIdChip;
QPushButton *ipChip;
QLabel *vpnLabel;
QSpinBox *vpnSpin;

View file

@ -22,6 +22,7 @@
#include <QSharedPointer>
#include <QSplitter>
#include <QStringList>
#include <QTabWidget>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QTextEdit>
@ -94,38 +95,67 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli
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;
QFont displayFont = userInfoNameValue->font();
displayFont.setPointSize(displayFont.pointSize() + 6);
displayFont.setBold(true);
userInfoNameValue->setFont(displayFont);
userInfoStatusValue = new QLabel;
userInfoCountsLabel = new QLabel;
userInfoCountsValue = new QLabel;
QFont statusFont = userInfoStatusValue->font();
statusFont.setBold(true);
userInfoStatusValue->setFont(statusFont);
sublineLabel = new QLabel;
auto makeCountCard = [](const QString &title, QLabel *&value) {
auto *card = new QGroupBox(title);
value = new QLabel(QStringLiteral("–"), card);
QFont cardFont = value->font();
cardFont.setPointSize(cardFont.pointSize() + 6);
cardFont.setBold(true);
value->setFont(cardFont);
value->setAlignment(Qt::AlignCenter);
auto *cardLayout = new QVBoxLayout(card);
cardLayout->addWidget(value);
return card;
};
cardReports = makeCountCard(QString(), cardReportsValue);
cardWarns = makeCountCard(QString(), cardWarnsValue);
cardBans = makeCountCard(QString(), cardBansValue);
altsCard = new QGroupBox;
altsCountValue = new QLabel(QStringLiteral("–"), altsCard);
QFont altsFont = altsCountValue->font();
altsFont.setPointSize(altsFont.pointSize() + 6);
altsFont.setBold(true);
altsCountValue->setFont(altsFont);
altsCountValue->setAlignment(Qt::AlignCenter);
openAltsButton = new QPushButton;
connect(openAltsButton, &QPushButton::clicked, this, &TabModeration::openAltsGraph);
auto *altsCardLayout = new QVBoxLayout(altsCard);
altsCardLayout->addWidget(altsCountValue);
altsCardLayout->addWidget(openAltsButton);
auto *cardsRow = new QHBoxLayout;
cardsRow->addWidget(cardReports);
cardsRow->addWidget(cardWarns);
cardsRow->addWidget(cardBans);
cardsRow->addWidget(altsCard);
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 *identityRow = new QHBoxLayout;
identityRow->addWidget(userInfoNameValue);
identityRow->addStretch();
identityRow->addWidget(userInfoStatusValue);
auto *infoLayout = new QVBoxLayout(userInfoGroup);
infoLayout->addLayout(infoGrid);
infoLayout->addLayout(identityRow);
infoLayout->addWidget(sublineLabel);
infoLayout->addLayout(cardsRow);
infoLayout->addWidget(userInfoNotesLabel);
infoLayout->addWidget(userInfoNotesEdit);
auto configureTable = [](QTableWidget *table) {
table->setSelectionBehavior(QAbstractItemView::SelectRows);
@ -137,31 +167,27 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli
table->horizontalHeader()->setStretchLastSection(true);
};
altsGroup = new QGroupBox;
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
configureTable(altsTable);
connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
auto *userItem = altsTable->item(row, COL_ALTS_USER);
connect(altsTable, &QTableWidget::activated, this, [this](const QModelIndex &index) {
if (!index.isValid()) {
return;
}
auto *userItem = altsTable->item(index.row(), COL_ALTS_USER);
if (userItem != nullptr && !userItem->text().isEmpty()) {
getTabSupervisor()->openTabAlts(userItem->text());
}
});
openAltsButton = new QPushButton;
connect(openAltsButton, &QPushButton::clicked, this, &TabModeration::openAltsGraph);
auto *altsHeader = new QHBoxLayout;
altsHeader->addStretch();
altsHeader->addWidget(openAltsButton);
auto *altsLayout = new QVBoxLayout(altsGroup);
altsLayout->addLayout(altsHeader);
auto *altsPage = new QWidget;
auto *altsLayout = new QVBoxLayout(altsPage);
altsLayout->addWidget(altsTable);
sessionsGroup = new QGroupBox;
sessionsTable = new QTableWidget(0, COL_SESSIONS_COUNT);
configureTable(sessionsTable);
auto *sessionsLayout = new QVBoxLayout(sessionsGroup);
auto *sessionsPage = new QWidget;
auto *sessionsLayout = new QVBoxLayout(sessionsPage);
sessionsLayout->addWidget(sessionsTable);
staffGroup = new QGroupBox;
staffTable = new QTableWidget(0, COL_STAFF_COUNT);
configureTable(staffTable);
refreshStaffButton = new QPushButton;
@ -169,19 +195,21 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli
auto *staffHeader = new QHBoxLayout;
staffHeader->addStretch();
staffHeader->addWidget(refreshStaffButton);
auto *staffLayout = new QVBoxLayout(staffGroup);
auto *staffPage = new QWidget;
auto *staffLayout = new QVBoxLayout(staffPage);
staffLayout->addWidget(staffTable);
staffLayout->addLayout(staffHeader);
sectionTabs = new QTabWidget;
sectionTabs->addTab(altsPage, QString());
sectionTabs->addTab(sessionsPage, QString());
sectionTabs->addTab(staffPage, QString());
auto *splitter = new QSplitter(Qt::Vertical);
splitter->addWidget(userInfoGroup);
splitter->addWidget(altsGroup);
splitter->addWidget(sessionsGroup);
splitter->addWidget(staffGroup);
splitter->addWidget(sectionTabs);
splitter->setStretchFactor(0, 1);
splitter->setStretchFactor(1, 2);
splitter->setStretchFactor(2, 2);
splitter->setStretchFactor(3, 1);
auto *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->addLayout(topBar);
@ -202,21 +230,28 @@ void TabModeration::retranslateUi()
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"));
openAltsButton->setText(tr("Open alt graph"));
cardReports->setTitle(tr("Reports"));
cardWarns->setTitle(tr("Warnings"));
cardBans->setTitle(tr("Bans"));
altsCard->setTitle(tr("Alts"));
openAltsButton->setText(tr("Open graph"));
openAltsButton->setToolTip(tr("Open this user in the User Alts tab"));
sessionsGroup->setTitle(tr("Sessions"));
staffGroup->setTitle(tr("Staff Last Logins"));
sectionTabs->setTabText(0, tr("Alts"));
sectionTabs->setTabText(1, tr("Sessions"));
sectionTabs->setTabText(2, tr("Staff Last Logins"));
if (cachedUserInfoValid) {
applyUserInfo();
}
if (cachedAltsCount >= 0) {
applyAltsCount();
}
altsTable->setHorizontalHeaderLabels({tr("User"), tr("eMail"), tr("Client ID"), tr("Registered"), tr("Last login"),
tr("Warns"), tr("Bans"), tr("Active")});
altsTable->setToolTip(tr("Double-click or press Enter on a row to open it in the User Alts tab"));
sessionsTable->setHorizontalHeaderLabels({tr("IP"), tr("Client ID"), tr("Start"), tr("End"), tr("Type")});
staffTable->setHorizontalHeaderLabels({tr("User"), tr("Level"), tr("Last login")});
}
@ -232,11 +267,15 @@ QString TabModeration::formatEpoch(quint64 ts) const
void TabModeration::clearUserData()
{
currentUser.clear();
cachedUserInfoValid = false;
cachedAltsCount = -1;
userInfoNameValue->clear();
userInfoRegisteredValue->clear();
userInfoLastLoginValue->clear();
userInfoStatusValue->clear();
userInfoCountsValue->clear();
sublineLabel->clear();
cardReportsValue->setText(QStringLiteral("–"));
cardWarnsValue->setText(QStringLiteral("–"));
cardBansValue->setText(QStringLiteral("–"));
altsCountValue->setText(QStringLiteral("–"));
userInfoNotesEdit->clear();
altsTable->setRowCount(0);
sessionsTable->setRowCount(0);
@ -261,9 +300,12 @@ void TabModeration::investigateUser()
return;
}
currentUser = userName;
cachedUserInfoValid = false;
cachedAltsCount = -1;
resetPasswordButton->setEnabled(true);
removeAvatarButton->setEnabled(true);
openAltsButton->setEnabled(true);
altsCountValue->setText(QStringLiteral("…"));
altsTable->setRowCount(0);
sessionsTable->setRowCount(0);
requestUserInfo(userName);
@ -282,10 +324,11 @@ void TabModeration::openAltsGraph()
void TabModeration::requestUserInfo(const QString &userName)
{
userInfoNameValue->setText(userName);
userInfoRegisteredValue->setText(tr("Loading..."));
userInfoLastLoginValue->setText(tr("Loading..."));
sublineLabel->setText(tr("Loading..."));
userInfoStatusValue->clear();
userInfoCountsValue->clear();
cardReportsValue->setText(QStringLiteral("…"));
cardWarnsValue->setText(QStringLiteral("…"));
cardBansValue->setText(QStringLiteral("…"));
userInfoNotesEdit->clear();
Command_ReportUserInfo cmd;
@ -298,9 +341,12 @@ void TabModeration::requestUserInfo(const QString &userName)
void TabModeration::userInfoResponse(const Response &response)
{
if (response.response_code() != Response::RespOk) {
userInfoRegisteredValue->clear();
userInfoLastLoginValue->clear();
cachedUserInfoValid = false;
sublineLabel->clear();
userInfoStatusValue->setText(tr("Error loading user info."));
cardReportsValue->setText(QStringLiteral("–"));
cardWarnsValue->setText(QStringLiteral("–"));
cardBansValue->setText(QStringLiteral("–"));
return;
}
@ -309,20 +355,28 @@ void TabModeration::userInfoResponse(const Response &response)
return;
}
userInfoRegisteredValue->setText(formatEpoch(resp.registration_time()));
userInfoLastLoginValue->setText(formatEpoch(resp.last_login()));
cachedUserInfo.CopyFrom(resp);
cachedUserInfoValid = true;
applyUserInfo();
}
void TabModeration::applyUserInfo()
{
const Response_ReportUserInfo &resp = cachedUserInfo;
sublineLabel->setText(tr("Registered %1 · Last login %2")
.arg(formatEpoch(resp.registration_time()))
.arg(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(", "));
userInfoStatusValue->setText(statusParts.join(QStringLiteral(", ")));
userInfoCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3")
.arg(resp.total_reports())
.arg(resp.total_bans())
.arg(resp.total_warns()));
cardReportsValue->setText(QString::number(resp.total_reports()));
cardWarnsValue->setText(QString::number(resp.total_warns()));
cardBansValue->setText(QString::number(resp.total_bans()));
if (resp.has_admin_notes() && !resp.admin_notes().empty()) {
userInfoNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes()));
@ -331,12 +385,21 @@ void TabModeration::userInfoResponse(const Response &response)
}
}
void TabModeration::applyAltsCount()
{
altsCountValue->setText(tr("%1 related").arg(cachedAltsCount));
}
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);
connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) {
if (userName == currentUser) {
sessionsResponse(response);
}
});
client->sendCommand(pend);
}
@ -368,7 +431,11 @@ 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);
connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) {
if (userName == currentUser) {
altsResponse(response);
}
});
client->sendCommand(pend);
}
@ -376,10 +443,14 @@ void TabModeration::altsResponse(const Response &response)
{
altsTable->setRowCount(0);
if (response.response_code() != Response::RespOk) {
cachedAltsCount = -1;
altsCountValue->setText(QStringLiteral("–"));
return;
}
const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext);
cachedAltsCount = resp.alts_size();
applyAltsCount();
altsTable->setRowCount(resp.alts_size());
for (int i = 0; i < resp.alts_size(); ++i) {
const ServerInfo_UserAlt &alt = resp.alts(i);

View file

@ -5,6 +5,7 @@
#include <QString>
#include <QtGlobal>
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
#include <qtmetamacros.h>
class AbstractClient;
@ -13,6 +14,7 @@ class QLabel;
class QLineEdit;
class QPushButton;
class QTableWidget;
class QTabWidget;
class QTextEdit;
class Response;
class TabSupervisor;
@ -52,10 +54,15 @@ private:
void requestAlts(const QString &userName);
void requestModeratorLogins();
void clearUserData();
void applyUserInfo();
void applyAltsCount();
[[nodiscard]] QString formatEpoch(quint64 ts) const;
AbstractClient *client;
QString currentUser;
Response_ReportUserInfo cachedUserInfo;
bool cachedUserInfoValid = false;
int cachedAltsCount = -1;
QLineEdit *searchEdit;
QPushButton *investigateButton;
@ -63,26 +70,25 @@ private:
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 *sublineLabel;
QGroupBox *cardReports;
QGroupBox *cardWarns;
QGroupBox *cardBans;
QGroupBox *altsCard;
QLabel *cardReportsValue;
QLabel *cardWarnsValue;
QLabel *cardBansValue;
QLabel *altsCountValue;
QLabel *userInfoNotesLabel;
QTextEdit *userInfoNotesEdit;
QGroupBox *altsGroup;
QTabWidget *sectionTabs;
QTableWidget *altsTable;
QPushButton *openAltsButton;
QGroupBox *sessionsGroup;
QTableWidget *sessionsTable;
QGroupBox *staffGroup;
QTableWidget *staffTable;
QPushButton *openAltsButton;
QPushButton *refreshStaffButton;
};