[Server] Move replay access reads and writes into the database interface (#7838)

cmdReplayList, cmdReplayDownload, cmdReplayModifyMatch and cmdReplayDeleteMatch issued their own SQL through Servatrice_DatabaseInterface. Add ReplayMatchRecord/ReplayEntry and the ReplayAccessResult/ReplayWriteResult enums to Server_DatabaseInterface, implement the list, data, do-not-hide and delete operations with the same statements and response mapping, and let the commands ask the injected interface. checkSql stays in the two write commands where it already was.
This commit is contained in:
BruebachL 2026-11-10 12:16:24 +01:00 • committed by GitHub
parent ba037cbf04
commit b2e69d3443
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 196 additions and 78 deletions

View file

@ -11,6 +11,7 @@
#include "libcockatrice/protocol/pb/serverinfo_warning.pb.h" #include "libcockatrice/protocol/pb/serverinfo_warning.pb.h"
#include "server.h" #include "server.h"
#include <QByteArray>
#include <QMap> #include <QMap>
#include <QObject> #include <QObject>
#include <QString> #include <QString>
@ -311,6 +312,55 @@ public:
{ {
return false; return false;
} }
/** @brief One downloaded replay file of a match. */
struct ReplayEntry
{
int replayId = 0;
int duration = 0;
};
/** @brief One replay match visible to a user, with its players and replay files. */
struct ReplayMatchRecord
{
int gameId = 0;
QString replayName;
QString roomName;
int timeStarted = 0;
int timeFinished = 0;
QString description;
bool doNotHide = false;
QStringList playerNames;
QList<ReplayEntry> replays;
};
/** @brief Why a replay read succeeded or not, so callers can map it to a response code. */
enum class ReplayAccessResult
{
Ok,
AccessDenied,
NotFound,
Failed
};
enum class ReplayWriteResult
{
Ok,
NotFound,
Failed
};
virtual QList<ReplayMatchRecord> getReplayList(int /* userId */)
{
return {};
}
virtual ReplayAccessResult getReplayData(int /* replayId */, int /* userId */, QByteArray & /* data */)
{
return ReplayAccessResult::Failed;
}
virtual ReplayWriteResult setReplayDoNotHide(int /* gameId */, int /* userId */, bool /* doNotHide */)
{
return ReplayWriteResult::Failed;
}
virtual ReplayWriteResult deleteReplayAccess(int /* gameId */, int /* userId */)
{
return ReplayWriteResult::Failed;
}
/** @brief Append a moderator/admin action to the server audit log. */ /** @brief Append a moderator/admin action to the server audit log. */
virtual void addAuditRecord(const QString & /* user */, virtual void addAuditRecord(const QString & /* user */,
const QString & /* ipaddress */, const QString & /* ipaddress */,

View file

@ -1838,6 +1838,107 @@ int Servatrice_DatabaseInterface::getDeckSharesCreatedToday(int userId)
return query->value(0).toInt(); return query->value(0).toInt();
} }
QList<Server_DatabaseInterface::ReplayMatchRecord> Servatrice_DatabaseInterface::getReplayList(int userId)
{
QList<ReplayMatchRecord> matches;
QSqlQuery *query1 = prepareQuery(
"select a.id_game, a.replay_name, b.room_name, b.time_started, b.time_finished, b.descr, a.do_not_hide from "
"{prefix}_replays_access a left join {prefix}_games b on b.id = a.id_game where a.id_player = :id_player and "
"(a.do_not_hide = 1 or date_add(b.time_started, interval 7 day) > now())");
query1->bindValue(":id_player", userId);
execSqlQuery(query1);
while (query1->next()) {
ReplayMatchRecord match;
match.gameId = query1->value(0).toInt();
match.replayName = query1->value(1).toString();
match.roomName = query1->value(2).toString();
match.timeStarted = query1->value(3).toDateTime().toSecsSinceEpoch();
match.timeFinished = query1->value(4).toDateTime().toSecsSinceEpoch();
match.description = query1->value(5).toString();
match.doNotHide = query1->value(6).toBool();
{
QSqlQuery *query2 = prepareQuery("select player_name from {prefix}_games_players where id_game = :id_game");
query2->bindValue(":id_game", match.gameId);
execSqlQuery(query2);
while (query2->next()) {
match.playerNames.append(query2->value(0).toString());
}
}
{
QSqlQuery *query3 = prepareQuery("select id, duration from {prefix}_replays where id_game = :id_game");
query3->bindValue(":id_game", match.gameId);
execSqlQuery(query3);
while (query3->next()) {
match.replays.append(
ReplayEntry{.replayId = query3->value(0).toInt(), .duration = query3->value(1).toInt()});
}
}
matches.append(match);
}
return matches;
}
Server_DatabaseInterface::ReplayAccessResult
Servatrice_DatabaseInterface::getReplayData(int replayId, int userId, QByteArray &data)
{
{
QSqlQuery *query = prepareQuery("select 1 from {prefix}_replays_access a left join {prefix}_replays b on "
"a.id_game = b.id_game where b.id = :id_replay and a.id_player = :id_player");
query->bindValue(":id_replay", replayId);
query->bindValue(":id_player", userId);
if (!execSqlQuery(query)) {
return ReplayAccessResult::Failed;
}
if (!query->next()) {
return ReplayAccessResult::AccessDenied;
}
}
QSqlQuery *query = prepareQuery("select replay from {prefix}_replays where id = :id_replay");
query->bindValue(":id_replay", replayId);
if (!execSqlQuery(query)) {
return ReplayAccessResult::Failed;
}
if (!query->next()) {
return ReplayAccessResult::NotFound;
}
data = query->value(0).toByteArray();
return ReplayAccessResult::Ok;
}
Server_DatabaseInterface::ReplayWriteResult
Servatrice_DatabaseInterface::setReplayDoNotHide(int gameId, int userId, bool doNotHide)
{
QSqlQuery *query = prepareQuery("update {prefix}_replays_access set do_not_hide=:do_not_hide where "
"id_player = :id_player and id_game = :id_game");
query->bindValue(":id_player", userId);
query->bindValue(":id_game", gameId);
query->bindValue(":do_not_hide", doNotHide);
if (!execSqlQuery(query)) {
return ReplayWriteResult::Failed;
}
return query->numRowsAffected() > 0 ? ReplayWriteResult::Ok : ReplayWriteResult::NotFound;
}
Server_DatabaseInterface::ReplayWriteResult Servatrice_DatabaseInterface::deleteReplayAccess(int gameId, int userId)
{
QSqlQuery *query =
prepareQuery("delete from {prefix}_replays_access where id_player = :id_player and id_game = :id_game");
query->bindValue(":id_player", userId);
query->bindValue(":id_game", gameId);
if (!execSqlQuery(query)) {
return ReplayWriteResult::Failed;
}
return query->numRowsAffected() > 0 ? ReplayWriteResult::Ok : ReplayWriteResult::NotFound;
}
bool Servatrice_DatabaseInterface::getDeckFilesInFolder(int folderId, int userId, QList<DeckFileListItem> &files) bool Servatrice_DatabaseInterface::getDeckFilesInFolder(int folderId, int userId, QList<DeckFileListItem> &files)
{ {
checkSql(); checkSql();

View file

@ -158,6 +158,10 @@ public:
bool getDeckShareItem(const QString &token, int itemId, QString &content) override; bool getDeckShareItem(const QString &token, int itemId, QString &content) override;
/** @brief Id and color identity of every deck file in a folder, for the share builder. */ /** @brief Id and color identity of every deck file in a folder, for the share builder. */
bool getDeckFilesInFolder(int folderId, int userId, QList<DeckFileListItem> &files) override; bool getDeckFilesInFolder(int folderId, int userId, QList<DeckFileListItem> &files) override;
QList<ReplayMatchRecord> getReplayList(int userId) override;
ReplayAccessResult getReplayData(int replayId, int userId, QByteArray &data) override;
ReplayWriteResult setReplayDoNotHide(int gameId, int userId, bool doNotHide) override;
ReplayWriteResult deleteReplayAccess(int gameId, int userId) override;
/** @brief Deletes all expired share bundles (cascades to their items). */ /** @brief Deletes all expired share bundles (cascades to their items). */
void cleanupExpiredDeckShares(); void cleanupExpiredDeckShares();

View file

@ -1252,47 +1252,24 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Comman
} }
Response_ReplayList *re = new Response_ReplayList; Response_ReplayList *re = new Response_ReplayList;
for (const Server_DatabaseInterface::ReplayMatchRecord &match : databaseInterface->getReplayList(userInfo->id())) {
QSqlQuery *query1 = sqlInterface->prepareQuery(
"select a.id_game, a.replay_name, b.room_name, b.time_started, b.time_finished, b.descr, a.do_not_hide from "
"{prefix}_replays_access a left join {prefix}_games b on b.id = a.id_game where a.id_player = :id_player and "
"(a.do_not_hide = 1 or date_add(b.time_started, interval 7 day) > now())");
query1->bindValue(":id_player", userInfo->id());
sqlInterface->execSqlQuery(query1);
while (query1->next()) {
ServerInfo_ReplayMatch *matchInfo = re->add_match_list(); ServerInfo_ReplayMatch *matchInfo = re->add_match_list();
const int gameId = query1->value(0).toInt(); matchInfo->set_game_id(match.gameId);
matchInfo->set_game_id(gameId); matchInfo->set_room_name(match.roomName.toStdString());
matchInfo->set_room_name(query1->value(2).toString().toStdString()); matchInfo->set_time_started(match.timeStarted);
const int timeStarted = query1->value(3).toDateTime().toSecsSinceEpoch(); matchInfo->set_length(match.timeFinished - match.timeStarted);
const int timeFinished = query1->value(4).toDateTime().toSecsSinceEpoch(); matchInfo->set_game_name(match.description.toStdString());
matchInfo->set_time_started(timeStarted); matchInfo->set_do_not_hide(match.doNotHide);
matchInfo->set_length(timeFinished - timeStarted);
matchInfo->set_game_name(query1->value(5).toString().toStdString());
const QString replayName = query1->value(1).toString();
matchInfo->set_do_not_hide(query1->value(6).toBool());
{ for (const QString &playerName : match.playerNames) {
QSqlQuery *query2 = matchInfo->add_player_names(playerName.toStdString());
sqlInterface->prepareQuery("select player_name from {prefix}_games_players where id_game = :id_game");
query2->bindValue(":id_game", gameId);
sqlInterface->execSqlQuery(query2);
while (query2->next()) {
matchInfo->add_player_names(query2->value(0).toString().toStdString());
}
} }
{ for (const Server_DatabaseInterface::ReplayEntry &replay : match.replays) {
QSqlQuery *query3 = ServerInfo_Replay *replayInfo = matchInfo->add_replay_list();
sqlInterface->prepareQuery("select id, duration from {prefix}_replays where id_game = :id_game"); replayInfo->set_replay_id(replay.replayId);
query3->bindValue(":id_game", gameId); replayInfo->set_replay_name(match.replayName.toStdString());
sqlInterface->execSqlQuery(query3); replayInfo->set_duration(replay.duration);
while (query3->next()) {
ServerInfo_Replay *replayInfo = matchInfo->add_replay_list();
replayInfo->set_replay_id(query3->value(0).toInt());
replayInfo->set_replay_name(replayName.toStdString());
replayInfo->set_duration(query3->value(1).toInt());
}
} }
} }
@ -1307,31 +1284,18 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayDownload(const Co
return Response::RespFunctionNotAllowed; return Response::RespFunctionNotAllowed;
} }
{ QByteArray data;
QSqlQuery *query = switch (databaseInterface->getReplayData(cmd.replay_id(), userInfo->id(), data)) {
sqlInterface->prepareQuery("select 1 from {prefix}_replays_access a left join {prefix}_replays b on " case Server_DatabaseInterface::ReplayAccessResult::Ok:
"a.id_game = b.id_game where b.id = :id_replay and a.id_player = :id_player"); break;
query->bindValue(":id_replay", cmd.replay_id()); case Server_DatabaseInterface::ReplayAccessResult::AccessDenied:
query->bindValue(":id_player", userInfo->id());
if (!sqlInterface->execSqlQuery(query)) {
return Response::RespInternalError;
}
if (!query->next()) {
return Response::RespAccessDenied; return Response::RespAccessDenied;
} case Server_DatabaseInterface::ReplayAccessResult::NotFound:
return Response::RespNameNotFound;
case Server_DatabaseInterface::ReplayAccessResult::Failed:
return Response::RespInternalError;
} }
QSqlQuery *query = sqlInterface->prepareQuery("select replay from {prefix}_replays where id = :id_replay");
query->bindValue(":id_replay", cmd.replay_id());
if (!sqlInterface->execSqlQuery(query)) {
return Response::RespInternalError;
}
if (!query->next()) {
return Response::RespNameNotFound;
}
QByteArray data = query->value(0).toByteArray();
Response_ReplayDownload *re = new Response_ReplayDownload; Response_ReplayDownload *re = new Response_ReplayDownload;
re->set_replay_data(data.data(), data.size()); re->set_replay_data(data.data(), data.size());
rc.setResponseExtension(re); rc.setResponseExtension(re);
@ -1346,20 +1310,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayModifyMatch(const
return Response::RespFunctionNotAllowed; return Response::RespFunctionNotAllowed;
} }
if (!sqlInterface->checkSql()) { if (!databaseInterface->checkSql()) {
return Response::RespInternalError; return Response::RespInternalError;
} }
QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_replays_access set do_not_hide=:do_not_hide where " switch (databaseInterface->setReplayDoNotHide(cmd.game_id(), userInfo->id(), cmd.do_not_hide())) {
"id_player = :id_player and id_game = :id_game"); case Server_DatabaseInterface::ReplayWriteResult::Ok:
query->bindValue(":id_player", userInfo->id()); return Response::RespOk;
query->bindValue(":id_game", cmd.game_id()); case Server_DatabaseInterface::ReplayWriteResult::NotFound:
query->bindValue(":do_not_hide", cmd.do_not_hide()); return Response::RespNameNotFound;
case Server_DatabaseInterface::ReplayWriteResult::Failed:
if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError;
return Response::RespInternalError;
} }
return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; return Response::RespInternalError;
} }
Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd,
@ -1369,19 +1332,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const
return Response::RespFunctionNotAllowed; return Response::RespFunctionNotAllowed;
} }
if (!sqlInterface->checkSql()) { if (!databaseInterface->checkSql()) {
return Response::RespInternalError; return Response::RespInternalError;
} }
QSqlQuery *query = sqlInterface->prepareQuery( switch (databaseInterface->deleteReplayAccess(cmd.game_id(), userInfo->id())) {
"delete from {prefix}_replays_access where id_player = :id_player and id_game = :id_game"); case Server_DatabaseInterface::ReplayWriteResult::Ok:
query->bindValue(":id_player", userInfo->id()); return Response::RespOk;
query->bindValue(":id_game", cmd.game_id()); case Server_DatabaseInterface::ReplayWriteResult::NotFound:
return Response::RespNameNotFound;
if (!sqlInterface->execSqlQuery(query)) { case Server_DatabaseInterface::ReplayWriteResult::Failed:
return Response::RespInternalError; return Response::RespInternalError;
} }
return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; return Response::RespInternalError;
} }
/** /**