[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

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