diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h index 813e03de5..04683bbc4 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -11,6 +11,7 @@ #include "libcockatrice/protocol/pb/serverinfo_warning.pb.h" #include "server.h" +#include #include #include #include @@ -311,6 +312,55 @@ public: { 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 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 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. */ virtual void addAuditRecord(const QString & /* user */, const QString & /* ipaddress */, diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 96d2afbfe..7fe518f1b 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -1838,6 +1838,107 @@ int Servatrice_DatabaseInterface::getDeckSharesCreatedToday(int userId) return query->value(0).toInt(); } +QList Servatrice_DatabaseInterface::getReplayList(int userId) +{ + QList 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 &files) { checkSql(); diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index fd73c1096..3eb24e06e 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -158,6 +158,10 @@ public: 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. */ bool getDeckFilesInFolder(int folderId, int userId, QList &files) override; + QList 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). */ void cleanupExpiredDeckShares(); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 4512bcef9..47365ea73 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -1252,47 +1252,24 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Comman } Response_ReplayList *re = new Response_ReplayList; - - 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()) { + for (const Server_DatabaseInterface::ReplayMatchRecord &match : databaseInterface->getReplayList(userInfo->id())) { ServerInfo_ReplayMatch *matchInfo = re->add_match_list(); - const int gameId = query1->value(0).toInt(); - matchInfo->set_game_id(gameId); - matchInfo->set_room_name(query1->value(2).toString().toStdString()); - const int timeStarted = query1->value(3).toDateTime().toSecsSinceEpoch(); - const int timeFinished = query1->value(4).toDateTime().toSecsSinceEpoch(); - matchInfo->set_time_started(timeStarted); - 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()); + matchInfo->set_game_id(match.gameId); + matchInfo->set_room_name(match.roomName.toStdString()); + matchInfo->set_time_started(match.timeStarted); + matchInfo->set_length(match.timeFinished - match.timeStarted); + matchInfo->set_game_name(match.description.toStdString()); + matchInfo->set_do_not_hide(match.doNotHide); - { - QSqlQuery *query2 = - 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 QString &playerName : match.playerNames) { + matchInfo->add_player_names(playerName.toStdString()); } - { - QSqlQuery *query3 = - sqlInterface->prepareQuery("select id, duration from {prefix}_replays where id_game = :id_game"); - query3->bindValue(":id_game", gameId); - sqlInterface->execSqlQuery(query3); - 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()); - } + for (const Server_DatabaseInterface::ReplayEntry &replay : match.replays) { + ServerInfo_Replay *replayInfo = matchInfo->add_replay_list(); + replayInfo->set_replay_id(replay.replayId); + replayInfo->set_replay_name(match.replayName.toStdString()); + replayInfo->set_duration(replay.duration); } } @@ -1307,31 +1284,18 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayDownload(const Co return Response::RespFunctionNotAllowed; } - { - QSqlQuery *query = - sqlInterface->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", cmd.replay_id()); - query->bindValue(":id_player", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - if (!query->next()) { + QByteArray data; + switch (databaseInterface->getReplayData(cmd.replay_id(), userInfo->id(), data)) { + case Server_DatabaseInterface::ReplayAccessResult::Ok: + break; + case Server_DatabaseInterface::ReplayAccessResult::AccessDenied: 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; re->set_replay_data(data.data(), data.size()); rc.setResponseExtension(re); @@ -1346,20 +1310,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayModifyMatch(const return Response::RespFunctionNotAllowed; } - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } - QSqlQuery *query = sqlInterface->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", userInfo->id()); - query->bindValue(":id_game", cmd.game_id()); - query->bindValue(":do_not_hide", cmd.do_not_hide()); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; + switch (databaseInterface->setReplayDoNotHide(cmd.game_id(), userInfo->id(), cmd.do_not_hide())) { + case Server_DatabaseInterface::ReplayWriteResult::Ok: + return Response::RespOk; + case Server_DatabaseInterface::ReplayWriteResult::NotFound: + return Response::RespNameNotFound; + case Server_DatabaseInterface::ReplayWriteResult::Failed: + return Response::RespInternalError; } - return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; + return Response::RespInternalError; } Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, @@ -1369,19 +1332,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const return Response::RespFunctionNotAllowed; } - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } - QSqlQuery *query = sqlInterface->prepareQuery( - "delete from {prefix}_replays_access where id_player = :id_player and id_game = :id_game"); - query->bindValue(":id_player", userInfo->id()); - query->bindValue(":id_game", cmd.game_id()); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; + switch (databaseInterface->deleteReplayAccess(cmd.game_id(), userInfo->id())) { + case Server_DatabaseInterface::ReplayWriteResult::Ok: + return Response::RespOk; + case Server_DatabaseInterface::ReplayWriteResult::NotFound: + return Response::RespNameNotFound; + case Server_DatabaseInterface::ReplayWriteResult::Failed: + return Response::RespInternalError; } - return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; + return Response::RespInternalError; } /**