mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Logs] Browse room history without filters with paginated infinite scroll (#7531)
* [Logs] Browse room history without filters with paginated infinite scroll * [Logs] Continue pagination when switching log tabs --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
3cf8bdb00e
commit
94e6281d87
7 changed files with 321 additions and 91 deletions
|
|
@ -13,6 +13,7 @@
|
|||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QScrollBar>
|
||||
#include <QSharedPointer>
|
||||
#include <QSizePolicy>
|
||||
#include <QSpinBox>
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
#include <QTableWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
|
||||
|
|
@ -31,6 +33,17 @@
|
|||
|
||||
class TabSupervisor;
|
||||
|
||||
namespace
|
||||
{
|
||||
/** @brief Identifies the newest row of a page well enough to recognise a repeated page. */
|
||||
QString pageStartKey(const Response_ViewLogHistory &response)
|
||||
{
|
||||
const ServerInfo_ChatMessage &first = response.log_message(0);
|
||||
return QString::fromStdString(first.time()) + QChar(0x1f) + QString::fromStdString(first.sender_name()) +
|
||||
QChar(0x1f) + QString::fromStdString(first.message());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands)
|
||||
: Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands)
|
||||
{
|
||||
|
|
@ -52,12 +65,20 @@ TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _can
|
|||
chatTable->setHorizontalHeaderLabels(
|
||||
QString(tr("Time;SenderName;SenderIP;Message;TargetID;TargetName")).split(";"));
|
||||
|
||||
QTabWidget *tabManager = new QTabWidget();
|
||||
tabManager = new QTabWidget();
|
||||
tabManager->addTab(roomTable, tr("Room Logs"));
|
||||
tabManager->addTab(gameTable, tr("Game Logs"));
|
||||
tabManager->addTab(chatTable, tr("Chat Logs"));
|
||||
setCentralWidget(tabManager);
|
||||
|
||||
connect(roomTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
|
||||
connect(gameTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
|
||||
connect(chatTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
|
||||
// A tab that gains only a few rows has nothing to scroll, so its scrollbar never fires:
|
||||
// switching to it would leave its next page unfetched until the moderator scrolled
|
||||
// another tab. Re-check on every tab switch so the visible table can continue the browse.
|
||||
connect(tabManager, &QTabWidget::currentChanged, this, &TabLog::checkFetchMore);
|
||||
|
||||
createDock();
|
||||
restartLayout();
|
||||
clearClicked();
|
||||
|
|
@ -74,12 +95,9 @@ void TabLog::retranslateUi()
|
|||
|
||||
void TabLog::getClicked()
|
||||
{
|
||||
if (findUsername->text().isEmpty() && findIPAddress->text().isEmpty() && findGameName->text().isEmpty() &&
|
||||
findGameID->text().isEmpty() && findMessage->text().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("You must select at least one filter."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty filters are a valid browse: room/game history going back from
|
||||
// today, paginated. Only require a date range + location, both of which
|
||||
// get sensible defaults below so a bare "Get" just works.
|
||||
if (!lastHour->isChecked() && !today->isChecked() && !pastDays->isChecked()) {
|
||||
pastDays->setChecked(true);
|
||||
pastXDays->setValue(20);
|
||||
|
|
@ -99,39 +117,71 @@ void TabLog::getClicked()
|
|||
}
|
||||
|
||||
if (maximumResults->value() == 0) {
|
||||
maximumResults->setValue(1000);
|
||||
maximumResults->setValue(200);
|
||||
}
|
||||
pageSize = qBound(1, maximumResults->value(), 1000);
|
||||
|
||||
int dateRange = 0;
|
||||
if (lastHour->isChecked()) {
|
||||
dateRange = 1;
|
||||
}
|
||||
|
||||
if (today->isChecked()) {
|
||||
dateRange = 24;
|
||||
}
|
||||
|
||||
if (pastDays->isChecked()) {
|
||||
dateRange = pastXDays->value() * 24;
|
||||
}
|
||||
|
||||
Command_ViewLogHistory cmd;
|
||||
cmd.set_user_name(findUsername->text().toStdString());
|
||||
cmd.set_ip_address(findIPAddress->text().toStdString());
|
||||
cmd.set_game_name(findGameName->text().toStdString());
|
||||
cmd.set_game_id(findGameID->text().toStdString());
|
||||
cmd.set_message(findMessage->text().toStdString());
|
||||
// Build the command once, here, and let every page copy it: rebuilding it from the
|
||||
// widgets per page would run page 2+ against filters edited after Get was clicked,
|
||||
// appending rows selected by a different query under the first page's results.
|
||||
currentQuery.Clear();
|
||||
currentQuery.set_user_name(findUsername->text().toStdString());
|
||||
currentQuery.set_ip_address(findIPAddress->text().toStdString());
|
||||
currentQuery.set_game_name(findGameName->text().toStdString());
|
||||
currentQuery.set_game_id(findGameID->text().toStdString());
|
||||
currentQuery.set_message(findMessage->text().toStdString());
|
||||
if (mainRoom->isChecked()) {
|
||||
cmd.add_log_location("room");
|
||||
};
|
||||
currentQuery.add_log_location("room");
|
||||
}
|
||||
if (gameRoom->isChecked()) {
|
||||
cmd.add_log_location("game");
|
||||
};
|
||||
currentQuery.add_log_location("game");
|
||||
}
|
||||
if (privateChat->isChecked()) {
|
||||
cmd.add_log_location("chat");
|
||||
};
|
||||
cmd.set_date_range(dateRange);
|
||||
cmd.set_maximum_results(maximumResults->value());
|
||||
currentQuery.add_log_location("chat");
|
||||
}
|
||||
currentQuery.set_date_range(dateRangeForSelection());
|
||||
currentQuery.set_maximum_results(pageSize);
|
||||
hasCurrentQuery = true;
|
||||
snapshotBefore.clear();
|
||||
lastPageStartKey.clear();
|
||||
|
||||
// Fresh search: reset pagination state and clear previous pages.
|
||||
currentOffset = 0;
|
||||
roomCounter = 0;
|
||||
gameCounter = 0;
|
||||
chatCounter = 0;
|
||||
roomTable->setRowCount(0);
|
||||
gameTable->setRowCount(0);
|
||||
chatTable->setRowCount(0);
|
||||
|
||||
fetchPage(0);
|
||||
}
|
||||
|
||||
int TabLog::dateRangeForSelection() const
|
||||
{
|
||||
if (lastHour->isChecked()) {
|
||||
return 1;
|
||||
}
|
||||
if (today->isChecked()) {
|
||||
return 24;
|
||||
}
|
||||
if (pastDays->isChecked()) {
|
||||
return pastXDays->value() * 24;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TabLog::fetchPage(int offset)
|
||||
{
|
||||
if (isFetching || !hasCurrentQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_ViewLogHistory cmd = currentQuery;
|
||||
cmd.set_offset(offset);
|
||||
if (!snapshotBefore.isEmpty()) {
|
||||
cmd.set_before_time(snapshotBefore.toStdString());
|
||||
}
|
||||
|
||||
PendingCommand *pend;
|
||||
if (canUseDeveloperCommands) {
|
||||
|
|
@ -141,10 +191,42 @@ void TabLog::getClicked()
|
|||
pend = client->prepareModeratorCommand(cmd);
|
||||
}
|
||||
|
||||
isFetching = true;
|
||||
getButton->setEnabled(false);
|
||||
|
||||
connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabLog::checkFetchMore()
|
||||
{
|
||||
if (isFetching || !hasMoreResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only the table the moderator is looking at may pull the next page. Checking all
|
||||
// three would make an empty or hidden table nearBottom by definition (maximum() and
|
||||
// value() are both zero), so a room-only browse chained page after page without any
|
||||
// scrolling - pulling the whole range with a growing OFFSET scan per page, and
|
||||
// looping forever against a server that ignores `offset`.
|
||||
auto *table = qobject_cast<QTableWidget *>(tabManager->currentWidget());
|
||||
if (table == nullptr || table->rowCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ScrollPerItem (the default) counts rows, not pixels: `value` is the top visible row
|
||||
// and `maximum` the last one that can be scrolled to. Prefetch half a page ahead so
|
||||
// scrolling never visibly stalls, without the fixed 300-row threshold firing the next
|
||||
// page the moment a page loads.
|
||||
const QScrollBar *bar = table->verticalScrollBar();
|
||||
const int prefetchRows = qMax(1, pageSize / 2);
|
||||
if (bar->maximum() - bar->value() > prefetchRows) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchPage(currentOffset);
|
||||
}
|
||||
|
||||
void TabLog::clearClicked()
|
||||
{
|
||||
findUsername->clear();
|
||||
|
|
@ -166,6 +248,10 @@ void TabLog::clearClicked()
|
|||
pastDays->setAutoExclusive(true);
|
||||
today->setAutoExclusive(true);
|
||||
lastHour->setAutoExclusive(true);
|
||||
currentOffset = 0;
|
||||
hasMoreResults = false;
|
||||
hasCurrentQuery = false;
|
||||
snapshotBefore.clear();
|
||||
}
|
||||
|
||||
void TabLog::createDock()
|
||||
|
|
@ -209,14 +295,15 @@ void TabLog::createDock()
|
|||
pastXDays = new QSpinBox;
|
||||
pastXDays->setMaximum(20);
|
||||
|
||||
labelMaximum = new QLabel(tr("Maximum Results: "));
|
||||
labelMaximum = new QLabel(tr("Results per page: "));
|
||||
maximumResults = new QSpinBox;
|
||||
maximumResults->setMaximum(1000);
|
||||
maximumResults->setToolTip(tr("Page size for smooth scrolling: more rows load automatically as you scroll."));
|
||||
|
||||
labelDescription = new QLabel(tr(
|
||||
"At least one filter is required.\nThe more information you put in, the more specific your results will be."));
|
||||
labelDescription = new QLabel(tr("Leave filters empty to browse recent room and game messages.\n"
|
||||
"More rows load automatically as you scroll."));
|
||||
|
||||
getButton = new QPushButton(tr("Get User Logs"));
|
||||
getButton = new QPushButton(tr("Get Logs"));
|
||||
getButton->setAutoDefault(true);
|
||||
connect(getButton, &QPushButton::clicked, this, &TabLog::getClicked);
|
||||
|
||||
|
|
@ -262,7 +349,7 @@ void TabLog::createDock()
|
|||
maxResultsGrid->addWidget(labelMaximum, 0, 0);
|
||||
maxResultsGrid->addWidget(maximumResults, 0, 1);
|
||||
|
||||
maxResultsGroupBox = new QGroupBox(tr("Maximum Results"));
|
||||
maxResultsGroupBox = new QGroupBox(tr("Results per page"));
|
||||
maxResultsGroupBox->setLayout(maxResultsGrid);
|
||||
|
||||
descriptionGrid = new QGridLayout;
|
||||
|
|
@ -295,84 +382,134 @@ void TabLog::createDock()
|
|||
searchDock->setWidget(searchDockContents);
|
||||
}
|
||||
|
||||
void TabLog::appendChatMessage(QTableWidget *table,
|
||||
int &counter,
|
||||
const QString &time,
|
||||
const QString &senderName,
|
||||
const QString &senderIp,
|
||||
const QString &messageText,
|
||||
const QString &targetId,
|
||||
const QString &targetName,
|
||||
const QString &channel)
|
||||
{
|
||||
table->insertRow(counter);
|
||||
table->setItem(counter, 0, new QTableWidgetItem(time));
|
||||
table->setItem(counter, 1, new QTableWidgetItem(senderName));
|
||||
table->setItem(counter, 2, new QTableWidgetItem(senderIp));
|
||||
table->setItem(counter, 3, new QTableWidgetItem(messageText));
|
||||
table->setItem(counter, 4, new QTableWidgetItem(targetId));
|
||||
table->setItem(counter, 5, new QTableWidgetItem(targetName));
|
||||
if (table->columnCount() > 6) {
|
||||
table->setItem(counter, 6, new QTableWidgetItem(channel));
|
||||
}
|
||||
++counter;
|
||||
}
|
||||
|
||||
void TabLog::viewLogHistory_processResponse(const Response &resp)
|
||||
{
|
||||
const Response_ViewLogHistory &response = resp.GetExtension(Response_ViewLogHistory::ext);
|
||||
const bool isFirstPage = (currentOffset == 0);
|
||||
isFetching = false;
|
||||
getButton->setEnabled(true);
|
||||
|
||||
if (resp.response_code() != Response::RespOk) {
|
||||
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Message History"),
|
||||
tr("Failed to collect message history information."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.log_message_size() == 0) {
|
||||
if (isFirstPage && response.log_message_size() == 0) {
|
||||
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Message History"),
|
||||
tr("There are no messages for the selected filters."));
|
||||
hasMoreResults = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int roomCounter = 0, gameCounter = 0, chatCounter = 0;
|
||||
roomTable->setRowCount(roomCounter);
|
||||
gameTable->setRowCount(gameCounter);
|
||||
chatTable->setRowCount(chatCounter);
|
||||
const QString thisPageStart = response.log_message_size() > 0 ? pageStartKey(response) : QString();
|
||||
|
||||
if (isFirstPage) {
|
||||
// Rows come newest first, so the first one fixes the snapshot every later page is
|
||||
// bounded by. Without it, each message logged between two page fetches pushes the
|
||||
// window down by one and repeats a row the moderator already has.
|
||||
snapshotBefore = QString::fromStdString(response.log_message(0).time());
|
||||
} else if (!response.has_has_more() && !thisPageStart.isEmpty() && thisPageStart == lastPageStartKey) {
|
||||
// A server that predates `offset` ignores it and returns the same page again. It
|
||||
// also predates `has_more`, so the full-page heuristic below can never prove
|
||||
// exhaustion and every scroll would append the same rows forever. The identical
|
||||
// first row is the proof; stop instead of looping.
|
||||
hasMoreResults = false;
|
||||
return;
|
||||
}
|
||||
lastPageStartKey = thisPageStart;
|
||||
|
||||
for (int i = 0; i < response.log_message_size(); ++i) {
|
||||
ServerInfo_ChatMessage message = response.log_message(i);
|
||||
const QString targetType = QString::fromStdString(message.target_type());
|
||||
if (targetType == "room" || targetType == "room_channel") {
|
||||
roomTable->insertRow(roomCounter);
|
||||
roomTable->setItem(roomCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
||||
roomTable->setItem(roomCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
||||
roomTable->setItem(roomCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
||||
roomTable->setItem(roomCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
||||
roomTable->setItem(roomCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
|
||||
roomTable->setItem(roomCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
|
||||
roomTable->setItem(roomCounter, 6, new QTableWidgetItem(QString::fromStdString(message.channel())));
|
||||
++roomCounter;
|
||||
appendChatMessage(roomTable, roomCounter, QString::fromStdString(message.time()),
|
||||
QString::fromStdString(message.sender_name()),
|
||||
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||
QString::fromStdString(message.target_id()),
|
||||
QString::fromStdString(message.target_name()), QString::fromStdString(message.channel()));
|
||||
}
|
||||
|
||||
if (targetType == "game") {
|
||||
gameTable->insertRow(gameCounter);
|
||||
gameTable->setItem(gameCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
||||
gameTable->setItem(gameCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
||||
gameTable->setItem(gameCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
||||
gameTable->setItem(gameCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
||||
gameTable->setItem(gameCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
|
||||
gameTable->setItem(gameCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
|
||||
++gameCounter;
|
||||
appendChatMessage(gameTable, gameCounter, QString::fromStdString(message.time()),
|
||||
QString::fromStdString(message.sender_name()),
|
||||
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||
QString::fromStdString(message.target_id()),
|
||||
QString::fromStdString(message.target_name()));
|
||||
}
|
||||
|
||||
if (targetType == "chat") {
|
||||
chatTable->insertRow(chatCounter);
|
||||
chatTable->setItem(chatCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
||||
chatTable->setItem(chatCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
||||
chatTable->setItem(chatCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
||||
chatTable->setItem(chatCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
||||
chatTable->setItem(chatCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
|
||||
chatTable->setItem(chatCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
|
||||
++chatCounter;
|
||||
appendChatMessage(chatTable, chatCounter, QString::fromStdString(message.time()),
|
||||
QString::fromStdString(message.sender_name()),
|
||||
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||
QString::fromStdString(message.target_id()),
|
||||
QString::fromStdString(message.target_name()));
|
||||
}
|
||||
}
|
||||
|
||||
currentOffset += response.log_message_size();
|
||||
if (response.has_has_more()) {
|
||||
hasMoreResults = response.has_more();
|
||||
} else {
|
||||
// Legacy server without has_more: a full page implies more may exist.
|
||||
hasMoreResults = (response.log_message_size() >= pageSize);
|
||||
}
|
||||
|
||||
if (roomCounter) {
|
||||
roomTable->show();
|
||||
roomTable->resizeColumnsToContents();
|
||||
if (isFirstPage) {
|
||||
roomTable->resizeColumnsToContents();
|
||||
}
|
||||
} else {
|
||||
roomTable->hide();
|
||||
}
|
||||
|
||||
if (gameCounter) {
|
||||
gameTable->resizeColumnsToContents();
|
||||
if (isFirstPage) {
|
||||
gameTable->resizeColumnsToContents();
|
||||
}
|
||||
gameTable->show();
|
||||
} else {
|
||||
gameTable->hide();
|
||||
}
|
||||
|
||||
if (chatCounter) {
|
||||
chatTable->resizeColumnsToContents();
|
||||
if (isFirstPage) {
|
||||
chatTable->resizeColumnsToContents();
|
||||
}
|
||||
chatTable->show();
|
||||
} else {
|
||||
chatTable->hide();
|
||||
}
|
||||
|
||||
// The new rows may already fill less than a viewport (or the user is
|
||||
// still parked at the bottom), so chain the next page without waiting
|
||||
// for another scroll event. The prefetch stays invisible: rows simply
|
||||
// keep appearing as you scroll.
|
||||
checkFetchMore();
|
||||
}
|
||||
|
||||
void TabLog::restartLayout()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "tab.h"
|
||||
|
||||
#include <QString>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class AbstractClient;
|
||||
|
|
@ -25,6 +26,7 @@ class QWidget;
|
|||
class QGridLayout;
|
||||
class QVBoxLayout;
|
||||
class QTableWidget;
|
||||
class QTabWidget;
|
||||
class Response;
|
||||
class TabSupervisor;
|
||||
|
||||
|
|
@ -48,8 +50,37 @@ private:
|
|||
*buttonGroupBox;
|
||||
QVBoxLayout *mainLayout;
|
||||
QTableWidget *roomTable, *gameTable, *chatTable;
|
||||
QTabWidget *tabManager = nullptr;
|
||||
|
||||
//! The filters of the search in flight, captured when Get was clicked so that
|
||||
//! later pages cannot pick up edits the moderator made after starting it.
|
||||
Command_ViewLogHistory currentQuery;
|
||||
bool hasCurrentQuery = false;
|
||||
//! Newest `log_time` of the first page, sent back as the upper bound of every
|
||||
//! later page so messages logged in the meantime do not shift the window.
|
||||
QString snapshotBefore;
|
||||
//! Identity of the previous page's first row, used to spot a server that ignores
|
||||
//! `offset` and hands back the same page forever.
|
||||
QString lastPageStartKey;
|
||||
int currentOffset = 0;
|
||||
int pageSize = 200;
|
||||
int roomCounter = 0, gameCounter = 0, chatCounter = 0;
|
||||
bool isFetching = false;
|
||||
bool hasMoreResults = false;
|
||||
|
||||
void createDock();
|
||||
[[nodiscard]] int dateRangeForSelection() const;
|
||||
void fetchPage(int offset);
|
||||
void appendChatMessage(QTableWidget *table,
|
||||
int &counter,
|
||||
const QString &time,
|
||||
const QString &senderName,
|
||||
const QString &senderIp,
|
||||
const QString &messageText,
|
||||
const QString &targetId,
|
||||
const QString &targetName,
|
||||
const QString &channel = QString());
|
||||
void checkFetchMore();
|
||||
signals:
|
||||
|
||||
private slots:
|
||||
|
|
|
|||
|
|
@ -87,14 +87,16 @@ message Command_ViewLogHistory {
|
|||
extend DeveloperCommand {
|
||||
optional Command_ViewLogHistory dev_ext = 1001;
|
||||
}
|
||||
optional string user_name = 1; // user that created message
|
||||
optional string ip_address = 2; // ip address of user that created message
|
||||
optional string game_name = 3; // client id of user that created the message
|
||||
optional string game_id = 4; // game number the message was sent to
|
||||
optional string message = 5; // raw message that was sent
|
||||
repeated string log_location = 6; // destination of message (ex: main room, game room, private chat)
|
||||
required uint32 date_range = 7; // the length of time (in minutes) to look back for
|
||||
optional uint32 maximum_results = 8; // the maximum number of query results
|
||||
optional string user_name = 1; // user that created message
|
||||
optional string ip_address = 2; // ip address of user that created message
|
||||
optional string game_name = 3; // client id of user that created the message
|
||||
optional string game_id = 4; // game number the message was sent to
|
||||
optional string message = 5; // raw message that was sent
|
||||
repeated string log_location = 6; // destination of message (ex: main room, game room, private chat)
|
||||
required uint32 date_range = 7; // the length of time (in minutes) to look back for
|
||||
optional uint32 maximum_results = 8; // the maximum number of query results (page size when paginating)
|
||||
optional uint32 offset = 9 [default = 0]; // rows to skip for pagination, ORDER BY log_time DESC
|
||||
optional string before_time = 10; // page those rows against this `log_time` snapshot (newest first page row)
|
||||
}
|
||||
|
||||
message Command_GrantReplayAccess {
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ message Response_ViewLogHistory {
|
|||
optional Response_ViewLogHistory ext = 1015;
|
||||
}
|
||||
repeated ServerInfo_ChatMessage log_message = 1;
|
||||
optional bool has_more = 2 [default = false]; // true when more rows exist past this page
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QtGlobal>
|
||||
#include <google/protobuf/stubs/common.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
|
|
@ -2249,8 +2250,14 @@ QList<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory
|
|||
bool &game,
|
||||
bool &room,
|
||||
int &range,
|
||||
int &maxresults)
|
||||
int &maxresults,
|
||||
int offset,
|
||||
const QString &beforeTime,
|
||||
bool *hasMore)
|
||||
{
|
||||
if (hasMore != nullptr) {
|
||||
*hasMore = false;
|
||||
}
|
||||
|
||||
QList<ServerInfo_ChatMessage> results;
|
||||
ServerInfo_ChatMessage chatMessage;
|
||||
|
|
@ -2259,8 +2266,31 @@ QList<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory
|
|||
return results;
|
||||
}
|
||||
|
||||
if (user.isEmpty() && ipaddress.isEmpty() && gameid.isEmpty() && gamename.isEmpty()) {
|
||||
// To ensure quick results and minimal lag, require an indexed field
|
||||
// Page size guard: always bound the scan so a filter-less browse of room
|
||||
// history cannot become a 50M-row fetch. Zero means "client did not set a
|
||||
// size", so fall back to one smooth-scroll page.
|
||||
const int pageSize = maxresults <= 0 ? 200 : qBound(1, maxresults, 1000);
|
||||
maxresults = pageSize;
|
||||
const int safeOffset = qMax(0, offset);
|
||||
|
||||
const bool hasIndexedFilter = !user.isEmpty() || !ipaddress.isEmpty() || !gameid.isEmpty() || !gamename.isEmpty();
|
||||
const bool hasLocationFilter = chat || game || room;
|
||||
if (!hasLocationFilter) {
|
||||
// No target_type restriction would mix private chat into a room/game
|
||||
// browse, so refuse rather than leak.
|
||||
return results;
|
||||
}
|
||||
// A browse narrowed only by a message substring has no index to serve it, and `range`
|
||||
// has no upper bound on the wire: the client offers 20 days, but a hand-built command
|
||||
// can ask for any value and every LIKE candidate in it is a row the server examines.
|
||||
// Clamping here keeps the guard independent of what the client happens to send.
|
||||
constexpr int maxFilterlessRangeHours = 20 * 24;
|
||||
if (!hasIndexedFilter && range > maxFilterlessRangeHours) {
|
||||
range = maxFilterlessRangeHours;
|
||||
}
|
||||
if (!hasIndexedFilter && range <= 0) {
|
||||
// Filter-less browse must stay time-bounded (idx_log_time) so the
|
||||
// query stays an index range scan instead of a full table scan.
|
||||
return results;
|
||||
}
|
||||
|
||||
|
|
@ -2315,10 +2345,23 @@ QList<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory
|
|||
queryString.append(" AND log_time >= DATE_SUB(now(), INTERVAL :range_time HOUR)");
|
||||
}
|
||||
|
||||
if (maxresults) {
|
||||
queryString.append(" LIMIT :limit_size");
|
||||
if (!beforeTime.isEmpty()) {
|
||||
// Pages after the first run against the snapshot fixed by the first one, so rows
|
||||
// logged while the moderator scrolls can no longer push the OFFSET window down and
|
||||
// repeat a row already on screen.
|
||||
queryString.append(" AND log_time <= :before_time");
|
||||
}
|
||||
|
||||
// Newest-first keeps "going back from today" stable across pages and lets
|
||||
// the client append older rows at the bottom for smooth infinite scroll.
|
||||
queryString.append(" ORDER BY log_time DESC");
|
||||
|
||||
// Fetch one extra row when the caller wants a has_more cursor so a full
|
||||
// page does not need a second empty round-trip to prove exhaustion.
|
||||
const bool detectMore = (hasMore != nullptr);
|
||||
const int fetchSize = detectMore ? pageSize + 1 : pageSize;
|
||||
queryString.append(" LIMIT :limit_size OFFSET :offset_size");
|
||||
|
||||
QSqlQuery *query = prepareQuery(queryString);
|
||||
if (!user.isEmpty()) {
|
||||
query->bindValue(":user_name", user);
|
||||
|
|
@ -2338,9 +2381,11 @@ QList<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory
|
|||
if (range) {
|
||||
query->bindValue(":range_time", range);
|
||||
}
|
||||
if (maxresults) {
|
||||
query->bindValue(":limit_size", maxresults);
|
||||
if (!beforeTime.isEmpty()) {
|
||||
query->bindValue(":before_time", beforeTime);
|
||||
}
|
||||
query->bindValue(":limit_size", fetchSize);
|
||||
query->bindValue(":offset_size", safeOffset);
|
||||
|
||||
if (!execSqlQuery(query)) {
|
||||
qCWarning(DatabaseInterfaceLog) << "Failed to collect log history information: SQL Error";
|
||||
|
|
@ -2360,6 +2405,11 @@ QList<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory
|
|||
results << chatMessage;
|
||||
}
|
||||
|
||||
if (detectMore && results.size() > pageSize) {
|
||||
results.removeLast();
|
||||
*hasMore = true;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,10 @@ public:
|
|||
bool &game,
|
||||
bool &room,
|
||||
int &range,
|
||||
int &maxresults);
|
||||
int &maxresults,
|
||||
int offset = 0,
|
||||
const QString &beforeTime = QString(),
|
||||
bool *hasMore = nullptr);
|
||||
QList<ServerInfo_UserSession> getUserSessions(const QString &userName, int limit);
|
||||
QList<ServerInfo_UserAlt> getUserAlts(const QString &userName);
|
||||
QList<ServerInfo_ModeratorLogin> getModeratorLastLogins();
|
||||
|
|
|
|||
|
|
@ -1743,12 +1743,17 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com
|
|||
|
||||
int dateRange = cmd.date_range();
|
||||
int maximumResults = cmd.maximum_results();
|
||||
const int offset = cmd.has_offset() ? static_cast<int>(cmd.offset()) : 0;
|
||||
const QString beforeTime = cmd.has_before_time() ? QString::fromStdString(cmd.before_time()) : QString();
|
||||
|
||||
Response_ViewLogHistory *re = new Response_ViewLogHistory;
|
||||
|
||||
if (servatrice->getEnableLogQuery()) {
|
||||
QListIterator<ServerInfo_ChatMessage> messageIterator(sqlInterface->getMessageLogHistory(
|
||||
userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults));
|
||||
bool hasMore = false;
|
||||
QList<ServerInfo_ChatMessage> history =
|
||||
sqlInterface->getMessageLogHistory(userName, ipAddress, gameName, gameID, message, chatType, gameType,
|
||||
roomType, dateRange, maximumResults, offset, beforeTime, &hasMore);
|
||||
QListIterator<ServerInfo_ChatMessage> messageIterator(history);
|
||||
while (messageIterator.hasNext()) {
|
||||
ServerInfo_ChatMessage chatMessage = messageIterator.next();
|
||||
if (!allowPrivateChat) {
|
||||
|
|
@ -1756,6 +1761,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com
|
|||
}
|
||||
re->add_log_message()->CopyFrom(chatMessage);
|
||||
}
|
||||
re->set_has_more(hasMore);
|
||||
} else {
|
||||
ServerInfo_ChatMessage chatMessage;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue