[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:
BruebachL 2026-11-02 07:36:49 +01:00 • committed by GitHub
parent 3cf8bdb00e
commit 94e6281d87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 321 additions and 91 deletions

View file

@ -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();
if (isFirstPage) {
roomTable->resizeColumnsToContents();
}
} else {
roomTable->hide();
}
if (gameCounter) {
if (isFirstPage) {
gameTable->resizeColumnsToContents();
}
gameTable->show();
} else {
gameTable->hide();
}
if (chatCounter) {
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()

View file

@ -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:

View file

@ -94,7 +94,9 @@ message Command_ViewLogHistory {
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 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 {

View file

@ -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
}

View file

@ -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;
}

View file

@ -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();

View file

@ -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;