diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp index 67e076983..9509f48ec 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -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(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(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(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() diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.h b/cockatrice/src/interface/widgets/tabs/tab_logs.h index fc8ef1ea0..f2f5842c5 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.h +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.h @@ -10,6 +10,7 @@ #include "tab.h" #include +#include #include 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: diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index 292917fa9..04cb31d98 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -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 { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto index 2ec0aff28..1041bca74 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto @@ -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 } diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 45ae9bde8..a4f184e38 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -2249,8 +2250,14 @@ QList 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 results; ServerInfo_ChatMessage chatMessage; @@ -2259,8 +2266,31 @@ QList 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 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 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 Servatrice_DatabaseInterface::getMessageLogHistory results << chatMessage; } + if (detectMore && results.size() > pageSize) { + results.removeLast(); + *hasMore = true; + } + return results; } diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index a27bd55b8..eb74063ac 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -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 getUserSessions(const QString &userName, int limit); QList getUserAlts(const QString &userName); QList getModeratorLastLogins(); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 67e807380..c38d790b1 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -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(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 messageIterator(sqlInterface->getMessageLogHistory( - userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults)); + bool hasMore = false; + QList history = + sqlInterface->getMessageLogHistory(userName, ipAddress, gameName, gameID, message, chatType, gameType, + roomType, dateRange, maximumResults, offset, beforeTime, &hasMore); + QListIterator 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;