This commit is contained in:
BruebachL 2026-11-04 10:30:36 -07:00 • committed by GitHub
commit fe2cb019b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 292 additions and 5 deletions

View file

@ -290,6 +290,7 @@ set(cockatrice_SOURCES
src/interface/widgets/settings_page/storage_settings_page.cpp
src/interface/widgets/settings_page/user_interface_settings_page.cpp
src/interface/widgets/utility/custom_line_edit.cpp
src/interface/widgets/utility/epoch_format.h
src/interface/widgets/utility/get_text_with_max.cpp
src/interface/widgets/utility/report_utils.cpp
src/interface/widgets/utility/report_utils.h
@ -383,6 +384,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/tab.cpp
src/interface/widgets/tabs/tab_account.cpp
src/interface/widgets/tabs/tab_admin.cpp
src/interface/widgets/tabs/tab_alts.cpp
src/interface/widgets/tabs/tab_card_art_rules.cpp
src/interface/widgets/tabs/tab_deck_editor.cpp
src/interface/widgets/tabs/tab_deck_storage.cpp

View file

@ -0,0 +1,183 @@
#include "tab_alts.h"
#include "../utility/epoch_format.h"
#include "abstract_client.h"
#include "libcockatrice/protocol/pb/response.pb.h"
#include "libcockatrice/protocol/pb/response_user_alts.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_user_alt.pb.h"
#include <QAbstractItemView>
#include <QDateTime>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSpinBox>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QVBoxLayout>
#include <QWidget>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <string>
namespace
{
constexpr int COL_ALTS_USER = 0;
constexpr int COL_ALTS_VIA = 1;
constexpr int COL_ALTS_DEPTH = 2;
constexpr int COL_ALTS_SHARED = 3;
constexpr int COL_ALTS_EMAIL = 4;
constexpr int COL_ALTS_CLIENTID = 5;
constexpr int COL_ALTS_REGISTERED = 6;
constexpr int COL_ALTS_LAST_LOGIN = 7;
constexpr int COL_ALTS_WARNS = 8;
constexpr int COL_ALTS_BANS = 9;
constexpr int COL_ALTS_ACTIVE = 10;
constexpr int COL_ALTS_COUNT = 11;
constexpr int MAX_ALT_DEPTH = 3;
} // namespace
TabAlts::TabAlts(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, &TabAlts::investigateUser);
depthSpin = new QSpinBox;
depthSpin->setMinimum(1);
depthSpin->setMaximum(MAX_ALT_DEPTH);
depthSpin->setValue(1);
investigateButton = new QPushButton;
connect(investigateButton, &QPushButton::clicked, this, &TabAlts::investigateUser);
statusLabel = new QLabel;
auto *topBar = new QHBoxLayout;
topBar->addWidget(searchEdit);
topBar->addWidget(depthSpin);
topBar->addWidget(investigateButton);
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
altsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
altsTable->setSelectionMode(QAbstractItemView::SingleSelection);
altsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
altsTable->verticalHeader()->setVisible(false);
altsTable->setAlternatingRowColors(true);
altsTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
altsTable->horizontalHeader()->setStretchLastSection(true);
auto *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->addLayout(topBar);
mainLayout->addWidget(statusLabel);
mainLayout->addWidget(altsTable);
retranslateUi();
investigate(initialUser);
}
void TabAlts::retranslateUi()
{
searchEdit->setPlaceholderText(tr("User name"));
depthSpin->setPrefix(tr("Depth: "));
depthSpin->setToolTip(tr("Search depth: 1 finds direct matches, higher values follow alts of alts."));
investigateButton->setText(tr("Investigate"));
if (currentUser.isEmpty()) {
statusLabel->setText(tr("Enter a user name to investigate."));
}
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")});
}
QString TabAlts::formatEpoch(quint64 ts) const
{
return formatEpochTimestamp(ts, tr("Unknown"));
}
void TabAlts::investigate(const QString &userName)
{
if (userName.isEmpty()) {
return;
}
searchEdit->setText(userName);
investigateUser();
}
void TabAlts::investigateUser()
{
const QString userName = searchEdit->text().simplified();
if (userName.isEmpty()) {
return;
}
currentUser = userName;
altsTable->setRowCount(0);
statusLabel->setText(tr("Loading alts for %1...").arg(currentUser));
requestAlts(userName, depthSpin->value());
}
void TabAlts::requestAlts(const QString &userName, int depth)
{
Command_GetUserAlts cmd;
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, [this, userName](const Response &response) {
if (userName == currentUser) {
altsResponse(response);
}
});
client->sendCommand(pend);
}
void TabAlts::altsResponse(const Response &response)
{
altsTable->setRowCount(0);
if (response.response_code() != Response::RespOk) {
statusLabel->setText(tr("Could not load alts for %1.").arg(currentUser));
return;
}
const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext);
// The server answers RespOk with no rows when the user does not exist.
if (resp.alts_size() == 0) {
statusLabel->setText(tr("User %1 not found.").arg(currentUser));
return;
}
altsTable->setRowCount(resp.alts_size());
int relatedCount = 0;
for (int i = 0; i < resp.alts_size(); ++i) {
const ServerInfo_UserAlt &alt = resp.alts(i);
// The server appends the frontier user as a depth-0 seed row.
if (!alt.has_depth() || alt.depth() > 0) {
++relatedCount;
}
altsTable->setItem(i, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name())));
altsTable->setItem(i, COL_ALTS_VIA,
new QTableWidgetItem(alt.has_match_reason() && !alt.match_reason().empty()
? QString::fromStdString(alt.match_reason())
: QStringLiteral("-")));
altsTable->setItem(i, COL_ALTS_DEPTH,
new QTableWidgetItem(alt.has_depth() ? QString::number(alt.depth()) : QStringLiteral("-")));
altsTable->setItem(
i, COL_ALTS_SHARED,
new QTableWidgetItem(alt.has_shared_count() ? QString::number(alt.shared_count()) : QStringLiteral("-")));
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")));
}
statusLabel->setText(tr("%1 account(s) related to %2.").arg(relatedCount).arg(currentUser));
}

View file

@ -0,0 +1,54 @@
#ifndef TAB_ALTS_H
#define TAB_ALTS_H
#include "tab.h"
#include <QString>
#include <QtGlobal>
#include <qtmetamacros.h>
class AbstractClient;
class QLabel;
class QLineEdit;
class QPushButton;
class QSpinBox;
class QTableWidget;
class Response;
class TabSupervisor;
/**
* Staff alt-graph investigation tool. Shows accounts related to the
* investigated user (shared email, client id, or recent IP address),
* including transitive alts-of-alts, with per-row match evidence.
*/
class TabAlts : public Tab
{
Q_OBJECT
public:
explicit TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser = {});
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
return tr("User Alts");
}
void investigate(const QString &userName);
private slots:
void investigateUser();
void altsResponse(const Response &response);
private:
void requestAlts(const QString &userName, int depth);
[[nodiscard]] QString formatEpoch(quint64 ts) const;
AbstractClient *client;
QString currentUser;
QLineEdit *searchEdit;
QSpinBox *depthSpin;
QPushButton *investigateButton;
QLabel *statusLabel;
QTableWidget *altsTable;
};
#endif // TAB_ALTS_H

View file

@ -1,5 +1,6 @@
#include "tab_moderation.h"
#include "../utility/epoch_format.h"
#include "abstract_client.h"
#include "libcockatrice/protocol/pb/admin_commands.pb.h"
#include "libcockatrice/protocol/pb/response.pb.h"
@ -215,10 +216,7 @@ void TabModeration::retranslateUi()
QString TabModeration::formatEpoch(quint64 ts) const
{
if (ts == 0) {
return tr("Unknown");
}
return QDateTime::fromSecsSinceEpoch(ts).toLocalTime().toString("yyyy-MM-dd HH:mm");
return formatEpochTimestamp(ts, tr("Unknown"));
}
void TabModeration::clearUserData()

View file

@ -21,6 +21,7 @@
#include "tab.h"
#include "tab_account.h"
#include "tab_admin.h"
#include "tab_alts.h"
#include "tab_card_art_rules.h"
#include "tab_deck_editor.h"
#include "tab_deck_storage.h"
@ -156,7 +157,7 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr),
tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr),
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false)
tabModeration(nullptr), tabAlts(nullptr), tabDeveloper(nullptr), isLocalGame(false)
{
setObjectName("tabSupervisor");
@ -574,6 +575,7 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
tabLog = nullptr;
tabReport = nullptr;
tabModeration = nullptr;
tabAlts = nullptr;
tabDeveloper = nullptr;
isLocalGame = true;
userInfo = new ServerInfo_User;
@ -622,6 +624,9 @@ void TabSupervisor::stop()
if (tabModeration) {
tabModeration->close();
}
if (tabAlts) {
tabAlts->close();
}
if (tabCardArtRules) {
tabCardArtRules->close();
}
@ -930,6 +935,21 @@ void TabSupervisor::openTabModeration(const QString &userName)
aTabModeration->setChecked(true);
}
void TabSupervisor::openTabAlts(const QString &userName)
{
if (tabAlts) {
setCurrentWidget(tabAlts);
if (!userName.isEmpty()) {
tabAlts->investigate(userName);
}
return;
}
tabAlts = new TabAlts(this, client, userName);
myAddTab(tabAlts);
setCurrentWidget(tabAlts);
connect(tabAlts, &QObject::destroyed, this, [this] { tabAlts = nullptr; });
}
void TabSupervisor::actTabDeveloper(bool checked)
{
if (checked && !tabDeveloper) {

View file

@ -50,6 +50,7 @@ class TabAdmin;
class TabMessage;
class TabReport;
class TabModeration;
class TabAlts;
class TabAccount;
class TabDeckEditor;
class TabDeveloper;
@ -116,6 +117,7 @@ private:
TabLog *tabLog;
TabReport *tabReport;
TabModeration *tabModeration;
TabAlts *tabAlts;
TabDeveloper *tabDeveloper;
QMap<int, TabRoom *> roomTabs;
QMap<int, TabGame *> gameTabs;
@ -209,6 +211,7 @@ public slots:
/** Joins or spectates a game listed on a user's profile hover card. */
void joinGameFromUserCard(int gameId, int roomId, bool asSpectator);
void openTabModeration(const QString &userName = {});
void openTabAlts(const QString &userName = {});
void switchToFirstAvailableNetworkTab();
void maximizeMainWindow();
void actTabVisualDeckStorage(bool checked);

View file

@ -0,0 +1,27 @@
/**
* @file epoch_format.h
* @brief Shared timestamp formatting for the moderation tables.
*/
#ifndef EPOCH_FORMAT_H
#define EPOCH_FORMAT_H
#include <QDateTime>
#include <QString>
/**
* @brief Format a unix timestamp the way the moderation tabs show dates.
*
* @param ts Seconds since the epoch; 0 means "unknown".
* @param unknownText Label returned for 0, translated by the caller so the
* translation context stays with the tab.
*/
inline QString formatEpochTimestamp(quint64 ts, const QString &unknownText)
{
if (ts == 0) {
return unknownText;
}
return QDateTime::fromSecsSinceEpoch(ts).toLocalTime().toString(QStringLiteral("yyyy-MM-dd HH:mm"));
}
#endif // EPOCH_FORMAT_H