From ab4373d025a892b7cdd1385f3a4c98c5c60ff232 Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Sun, 24 Aug 2025 19:40:44 -0700
Subject: [PATCH] Implement replay sharing (#6066)
* new protos
* implement commands on server
* add buttons
* icons
* run formatter
* Message on get replay code failure
* Add new commands to switch statement
* Better failure messages
* Fix permission check query
* Change hash method
* Prevent adding duplicate replays
* Clean up TabReplay ui
* Copy over replay name
* base64 encode the hash
* Shorten hash
* Better failure messages
* change icon back to search icon
* check hash before checking if user already has access
* update share icon
* Update label text
---
cockatrice/cockatrice.qrc | 1 +
cockatrice/resources/icons/share.svg | 25 ++++
cockatrice/src/client/tabs/tab_replays.cpp | 139 ++++++++++++++++++++-
cockatrice/src/client/tabs/tab_replays.h | 9 +-
common/pb/CMakeLists.txt | 3 +
common/pb/command_replay_get_code.proto | 9 ++
common/pb/command_replay_submit_code.proto | 9 ++
common/pb/response.proto | 1 +
common/pb/response_replay_get_code.proto | 9 ++
common/pb/session_commands.proto | 2 +
servatrice/src/serversocketinterface.cpp | 137 ++++++++++++++++++++
servatrice/src/serversocketinterface.h | 5 +
12 files changed, 342 insertions(+), 7 deletions(-)
create mode 100644 cockatrice/resources/icons/share.svg
create mode 100644 common/pb/command_replay_get_code.proto
create mode 100644 common/pb/command_replay_submit_code.proto
create mode 100644 common/pb/response_replay_get_code.proto
diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc
index 72ca1d83f..67cde480e 100644
--- a/cockatrice/cockatrice.qrc
+++ b/cockatrice/cockatrice.qrc
@@ -33,6 +33,7 @@
resources/icons/scales.svg
resources/icons/search.svg
resources/icons/settings.svg
+ resources/icons/share.svg
resources/icons/spectator.svg
resources/icons/swap.svg
resources/icons/sync.svg
diff --git a/cockatrice/resources/icons/share.svg b/cockatrice/resources/icons/share.svg
new file mode 100644
index 000000000..a84f55847
--- /dev/null
+++ b/cockatrice/resources/icons/share.svg
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/cockatrice/src/client/tabs/tab_replays.cpp b/cockatrice/src/client/tabs/tab_replays.cpp
index 59f19a9c4..e08cd35fb 100644
--- a/cockatrice/src/client/tabs/tab_replays.cpp
+++ b/cockatrice/src/client/tabs/tab_replays.cpp
@@ -6,15 +6,19 @@
#include "../../settings/cache_settings.h"
#include "pb/command_replay_delete_match.pb.h"
#include "pb/command_replay_download.pb.h"
+#include "pb/command_replay_get_code.pb.h"
#include "pb/command_replay_modify_match.pb.h"
+#include "pb/command_replay_submit_code.pb.h"
#include "pb/event_replay_added.pb.h"
#include "pb/game_replay.pb.h"
#include "pb/response.pb.h"
#include "pb/response_replay_download.pb.h"
+#include "pb/response_replay_get_code.pb.h"
#include "tab_game.h"
#include
#include
+#include
#include
#include
#include
@@ -129,13 +133,26 @@ QGroupBox *TabReplays::createRightLayout()
serverDirView = new RemoteReplayList_TreeWidget(client);
// Right side layout
- QToolBar *toolBar = new QToolBar;
+ /* put an invisible dummy QToolBar in the leftmost column so that the main toolbar is centered.
+ * Really ugly workaround, but I couldn't figure out the proper way to make it centered */
+ QToolBar *dummyToolBar = new QToolBar(this);
+ QSizePolicy sizePolicy = dummyToolBar->sizePolicy();
+ sizePolicy.setRetainSizeWhenHidden(true);
+ dummyToolBar->setSizePolicy(sizePolicy);
+ dummyToolBar->setVisible(false);
+
+ QToolBar *toolBar = new QToolBar(this);
toolBar->setOrientation(Qt::Horizontal);
toolBar->setIconSize(QSize(32, 32));
- QHBoxLayout *toolBarLayout = new QHBoxLayout;
- toolBarLayout->addStretch();
- toolBarLayout->addWidget(toolBar);
- toolBarLayout->addStretch();
+
+ QToolBar *rightmostToolBar = new QToolBar(this);
+ rightmostToolBar->setOrientation(Qt::Horizontal);
+ rightmostToolBar->setIconSize(QSize(32, 32));
+
+ QGridLayout *toolBarLayout = new QGridLayout;
+ toolBarLayout->addWidget(dummyToolBar, 0, 0, Qt::AlignLeft);
+ toolBarLayout->addWidget(toolBar, 0, 1, Qt::AlignHCenter);
+ toolBarLayout->addWidget(rightmostToolBar, 0, 2, Qt::AlignRight);
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(serverDirView);
@@ -157,12 +174,22 @@ QGroupBox *TabReplays::createRightLayout()
aDeleteRemoteReplay = new QAction(this);
aDeleteRemoteReplay->setIcon(QPixmap("theme:icons/remove_row"));
connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay);
+ aGetReplayCode = new QAction(this);
+ aGetReplayCode->setIcon(QPixmap("theme:icons/share"));
+ connect(aGetReplayCode, &QAction::triggered, this, &TabReplays::actGetReplayCode);
+
+ aSubmitReplayCode = new QAction(this);
+ aSubmitReplayCode->setIcon(QPixmap("theme:icons/search"));
+ connect(aSubmitReplayCode, &QAction::triggered, this, &TabReplays::actSubmitReplayCode);
// Add actions to toolbars
toolBar->addAction(aOpenRemoteReplay);
toolBar->addAction(aDownload);
toolBar->addAction(aKeep);
toolBar->addAction(aDeleteRemoteReplay);
+ toolBar->addAction(aGetReplayCode);
+
+ rightmostToolBar->addAction(aSubmitReplayCode);
return groupBox;
}
@@ -181,6 +208,9 @@ void TabReplays::retranslateUi()
aDownload->setText(tr("Download replay"));
aKeep->setText(tr("Toggle expiration lock"));
aDeleteRemoteReplay->setText(tr("Delete"));
+ aGetReplayCode->setText(tr("Get replay share code"));
+
+ aSubmitReplayCode->setText(tr("Look up replay by share code"));
}
void TabReplays::handleConnected(const ServerInfo_User &userInfo)
@@ -204,6 +234,8 @@ void TabReplays::setRemoteEnabled(bool enabled)
aDownload->setEnabled(enabled);
aKeep->setEnabled(enabled);
aDeleteRemoteReplay->setEnabled(enabled);
+ aGetReplayCode->setEnabled(enabled);
+ aSubmitReplayCode->setEnabled(enabled);
if (enabled) {
serverDirView->refreshTree();
@@ -480,13 +512,108 @@ void TabReplays::deleteRemoteReplayFinished(const Response &r, const CommandCont
serverDirView->removeMatchInfo(cmd.game_id());
}
+void TabReplays::actGetReplayCode()
+{
+ const auto curRights = serverDirView->getSelectedReplayMatches();
+ if (curRights.isEmpty()) {
+ return;
+ }
+
+ for (const auto curRight : curRights) {
+ Command_ReplayGetCode cmd;
+ cmd.set_game_id(curRight->game_id());
+
+ PendingCommand *pend = client->prepareSessionCommand(cmd);
+ connect(pend, &PendingCommand::finished, this, &TabReplays::getReplayCodeFinished);
+ client->sendCommand(pend);
+ }
+}
+
+void TabReplays::getReplayCodeFinished(const Response &r, const CommandContainer & /*commandContainer*/)
+{
+ if (r.response_code() == Response::RespFunctionNotAllowed) {
+ QMessageBox msgBox;
+ msgBox.setIcon(QMessageBox::Warning);
+ msgBox.setText(tr("Failed to get code"));
+ msgBox.setInformativeText(
+ tr("Either this server does not support replay sharing, or does not permit replay sharing for you."));
+ msgBox.exec();
+ return;
+ }
+
+ if (r.response_code() != Response::RespOk) {
+ QMessageBox::warning(this, tr("Failed"), tr("Could not get replay code"));
+ return;
+ }
+
+ const Response_ReplayGetCode &resp = r.GetExtension(Response_ReplayGetCode::ext);
+ QString code = QString::fromStdString(resp.replay_code());
+
+ QMessageBox msgBox;
+ msgBox.setText(tr("Replay Share Code"));
+ msgBox.setInformativeText(
+ tr("Others can use this code to add the replay to their list of remote replays:\n%1").arg(code));
+ msgBox.setStandardButtons(QMessageBox::Ok);
+ QPushButton *copyToClipboardButton = msgBox.addButton(tr("Copy to clipboard"), QMessageBox::ActionRole);
+ connect(copyToClipboardButton, &QPushButton::clicked, this, [code] { QApplication::clipboard()->setText(code); });
+ msgBox.setDefaultButton(copyToClipboardButton);
+ msgBox.exec();
+}
+
+void TabReplays::actSubmitReplayCode()
+{
+ bool ok;
+ QString code = QInputDialog::getText(this, tr("Look up replay by share code"), tr("Replay share code"),
+ QLineEdit::Normal, "", &ok);
+
+ if (!ok) {
+ return;
+ }
+
+ Command_ReplaySubmitCode cmd;
+ cmd.set_replay_code(code.toStdString());
+
+ PendingCommand *pend = client->prepareSessionCommand(cmd);
+ connect(pend, &PendingCommand::finished, this, &TabReplays::submitReplayCodeFinished);
+ client->sendCommand(pend);
+}
+
+void TabReplays::submitReplayCodeFinished(const Response &r, const CommandContainer & /*commandContainer*/)
+{
+ switch (r.response_code()) {
+ case Response::RespOk: {
+ QMessageBox msgBox;
+ msgBox.setIcon(QMessageBox::Information);
+ msgBox.setText(tr("Replay code found"));
+ msgBox.setInformativeText(tr("Replay was added, or you already had access to it."));
+ msgBox.exec();
+ break;
+ }
+ case Response::RespNameNotFound:
+ QMessageBox::warning(this, tr("Failed"), tr("Replay code not found"));
+ break;
+ case Response::RespFunctionNotAllowed: {
+ QMessageBox msgBox;
+ msgBox.setIcon(QMessageBox::Warning);
+ msgBox.setText(tr("Failed to submit code"));
+ msgBox.setInformativeText(
+ tr("Either this server does not support replay sharing, or does not permit replay sharing for you."));
+ msgBox.exec();
+ break;
+ }
+ default:
+ QMessageBox::warning(this, tr("Failed"), tr("Unexpected error"));
+ break;
+ }
+}
+
void TabReplays::replayAddedEventReceived(const Event_ReplayAdded &event)
{
if (event.has_match_info()) {
// 99.9% of events will have match info (Normal Workflow)
serverDirView->addMatchInfo(event.match_info());
} else {
- // When a Moderator force adds a replay, we need to refresh their view
+ // When a Moderator force adds a replay or a user submits a replay code, we need to refresh their view
serverDirView->refreshTree();
}
}
diff --git a/cockatrice/src/client/tabs/tab_replays.h b/cockatrice/src/client/tabs/tab_replays.h
index dafdcb5ae..af794535a 100644
--- a/cockatrice/src/client/tabs/tab_replays.h
+++ b/cockatrice/src/client/tabs/tab_replays.h
@@ -28,7 +28,8 @@ private:
QAction *aOpenLocalReplay, *aRenameLocal, *aNewLocalFolder, *aDeleteLocalReplay;
QAction *aOpenReplaysFolder;
- QAction *aOpenRemoteReplay, *aDownload, *aKeep, *aDeleteRemoteReplay;
+ QAction *aOpenRemoteReplay, *aDownload, *aKeep, *aDeleteRemoteReplay, *aGetReplayCode;
+ QAction *aSubmitReplayCode;
QGroupBox *createLeftLayout();
QGroupBox *createRightLayout();
@@ -62,6 +63,12 @@ private slots:
void actDeleteRemoteReplay();
void deleteRemoteReplayFinished(const Response &r, const CommandContainer &commandContainer);
+ void actGetReplayCode();
+ void getReplayCodeFinished(const Response &r, const CommandContainer &commandContainer);
+
+ void actSubmitReplayCode();
+ void submitReplayCodeFinished(const Response &r, const CommandContainer &commandContainer);
+
void replayAddedEventReceived(const Event_ReplayAdded &event);
signals:
void openReplay(GameReplay *replay);
diff --git a/common/pb/CMakeLists.txt b/common/pb/CMakeLists.txt
index e8af9ef46..43775a810 100644
--- a/common/pb/CMakeLists.txt
+++ b/common/pb/CMakeLists.txt
@@ -37,6 +37,8 @@ set(PROTO_FILES
command_replay_list.proto
command_replay_download.proto
command_replay_modify_match.proto
+ command_replay_get_code.proto
+ command_replay_submit_code.proto
command_reveal_cards.proto
command_reverse_turn.proto
command_roll_die.proto
@@ -130,6 +132,7 @@ set(PROTO_FILES
response_password_salt.proto
response_register.proto
response_replay_download.proto
+ response_replay_get_code.proto
response_replay_list.proto
response_viewlog_history.proto
response_warn_history.proto
diff --git a/common/pb/command_replay_get_code.proto b/common/pb/command_replay_get_code.proto
new file mode 100644
index 000000000..e4573153c
--- /dev/null
+++ b/common/pb/command_replay_get_code.proto
@@ -0,0 +1,9 @@
+syntax = "proto2";
+import "session_commands.proto";
+
+message Command_ReplayGetCode {
+ extend SessionCommand {
+ optional Command_ReplayGetCode ext = 1104;
+ }
+ optional sint32 game_id = 1 [default = -1];
+}
diff --git a/common/pb/command_replay_submit_code.proto b/common/pb/command_replay_submit_code.proto
new file mode 100644
index 000000000..73c5d0ba0
--- /dev/null
+++ b/common/pb/command_replay_submit_code.proto
@@ -0,0 +1,9 @@
+syntax = "proto2";
+import "session_commands.proto";
+
+message Command_ReplaySubmitCode {
+ extend SessionCommand {
+ optional Command_ReplaySubmitCode ext = 1105;
+ }
+ optional string replay_code = 1;
+}
diff --git a/common/pb/response.proto b/common/pb/response.proto
index 18f4249c4..a63819a8e 100644
--- a/common/pb/response.proto
+++ b/common/pb/response.proto
@@ -65,6 +65,7 @@ message Response {
GET_ADMIN_NOTES = 1018;
REPLAY_LIST = 1100;
REPLAY_DOWNLOAD = 1101;
+ REPLAY_GET_CODE = 1102;
}
required uint64 cmd_id = 1;
optional ResponseCode response_code = 2;
diff --git a/common/pb/response_replay_get_code.proto b/common/pb/response_replay_get_code.proto
new file mode 100644
index 000000000..5bb1b511a
--- /dev/null
+++ b/common/pb/response_replay_get_code.proto
@@ -0,0 +1,9 @@
+syntax = "proto2";
+import "response.proto";
+
+message Response_ReplayGetCode {
+ extend Response {
+ optional Response_ReplayGetCode ext = 1102;
+ }
+ optional string replay_code = 1;
+}
diff --git a/common/pb/session_commands.proto b/common/pb/session_commands.proto
index 64b3cd872..cecf87370 100644
--- a/common/pb/session_commands.proto
+++ b/common/pb/session_commands.proto
@@ -31,6 +31,8 @@ message SessionCommand {
REPLAY_DOWNLOAD = 1101;
REPLAY_MODIFY_MATCH = 1102;
REPLAY_DELETE_MATCH = 1103;
+ REPLAY_GET_CODE = 1104;
+ REPLAY_SUBMIT_CODE = 1105;
}
extensions 100 to max;
}
diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp
index bccefc438..f0d4771a7 100644
--- a/servatrice/src/serversocketinterface.cpp
+++ b/servatrice/src/serversocketinterface.cpp
@@ -31,13 +31,16 @@
#include "pb/command_deck_upload.pb.h"
#include "pb/command_replay_delete_match.pb.h"
#include "pb/command_replay_download.pb.h"
+#include "pb/command_replay_get_code.pb.h"
#include "pb/command_replay_list.pb.h"
#include "pb/command_replay_modify_match.pb.h"
+#include "pb/command_replay_submit_code.pb.h"
#include "pb/commands.pb.h"
#include "pb/event_add_to_list.pb.h"
#include "pb/event_connection_closed.pb.h"
#include "pb/event_notify_user.pb.h"
#include "pb/event_remove_from_list.pb.h"
+#include "pb/event_replay_added.pb.h"
#include "pb/event_server_identification.pb.h"
#include "pb/event_server_message.pb.h"
#include "pb/event_user_message.pb.h"
@@ -50,6 +53,7 @@
#include "pb/response_password_salt.pb.h"
#include "pb/response_register.pb.h"
#include "pb/response_replay_download.pb.h"
+#include "pb/response_replay_get_code.pb.h"
#include "pb/response_replay_list.pb.h"
#include "pb/response_viewlog_history.pb.h"
#include "pb/response_warn_history.pb.h"
@@ -179,6 +183,10 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm
return cmdReplayModifyMatch(cmd.GetExtension(Command_ReplayModifyMatch::ext), rc);
case SessionCommand::REPLAY_DELETE_MATCH:
return cmdReplayDeleteMatch(cmd.GetExtension(Command_ReplayDeleteMatch::ext), rc);
+ case SessionCommand::REPLAY_GET_CODE:
+ return cmdReplayGetCode(cmd.GetExtension(Command_ReplayGetCode::ext), rc);
+ case SessionCommand::REPLAY_SUBMIT_CODE:
+ return cmdReplaySubmitCode(cmd.GetExtension(Command_ReplaySubmitCode::ext), rc);
case SessionCommand::REGISTER:
return cmdRegisterAccount(cmd.GetExtension(Command_Register::ext), rc);
break;
@@ -735,6 +743,135 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const
return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound;
}
+/**
+ * Generates a hash for the given replay folder, used for auth when replay sharing.
+ * This is a separate function in case we change the hash implementation in the future.
+ *
+ * Currently, we append together the first 128 bytes of the first 3 replays in the game.
+ * Then we md5 hash it, base64 encode it, and truncate the result to 10 characters.
+ *
+ * @param gameId The replay match to hash
+ * @return The hash as a QString. Returns an empty string if failed
+ */
+QString AbstractServerSocketInterface::createHashForReplay(int gameId)
+{
+ QSqlQuery *query =
+ sqlInterface->prepareQuery("select replay from {prefix}_replays where id_game = :id_game limit 3");
+ query->bindValue(":id_game", gameId);
+
+ if (!sqlInterface->execSqlQuery(query))
+ return "";
+
+ QByteArray replaysBytes;
+ while (query->next()) {
+ QByteArray replay = query->value(0).toByteArray();
+ replay.truncate(128);
+ replaysBytes.append(replay);
+ }
+
+ auto hash =
+ QCryptographicHash::hash(replaysBytes, QCryptographicHash::Md5).toBase64(QByteArray::OmitTrailingEquals);
+ hash.truncate(10);
+ return hash;
+}
+
+Response::ResponseCode AbstractServerSocketInterface::cmdReplayGetCode(const Command_ReplayGetCode &cmd,
+ ResponseContainer &rc)
+{
+ if (authState != PasswordRight)
+ return Response::RespFunctionNotAllowed;
+
+ // Check that user has access to replay match
+ {
+ QSqlQuery *query = sqlInterface->prepareQuery(
+ "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player");
+ query->bindValue(":id_game", cmd.game_id());
+ query->bindValue(":id_player", userInfo->id());
+ if (!sqlInterface->execSqlQuery(query))
+ return Response::RespInternalError;
+ if (!query->next())
+ return Response::RespAccessDenied;
+ }
+
+ QString hash = createHashForReplay(cmd.game_id());
+ if (hash.isEmpty()) {
+ return Response::RespInternalError;
+ }
+
+ // code is of the form -
+ QString code = QString(QString::number(cmd.game_id()) + "-" + hash);
+
+ Response_ReplayGetCode *re = new Response_ReplayGetCode;
+ re->set_replay_code(code.toStdString());
+ rc.setResponseExtension(re);
+
+ return Response::RespOk;
+}
+
+Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd,
+ ResponseContainer & /*rc*/)
+{
+ // code is of the form -
+ QString code = QString::fromStdString(cmd.replay_code());
+ QStringList split = code.split("-");
+ if (split.size() != 2) {
+ // always return the same error response if code is incorrect, to not leak info to user
+ return Response::RespNameNotFound;
+ }
+ QString gameId = split[0];
+ QString hash = split[1];
+
+ // Determine if the replay actually exists (and grab the replay name while at it)
+ auto *replayExistsQuery =
+ sqlInterface->prepareQuery("select replay_name from {prefix}_replays_access where id_game = :id_game limit 1");
+ replayExistsQuery->bindValue(":id_game", gameId);
+ if (!sqlInterface->execSqlQuery(replayExistsQuery)) {
+ return Response::RespInternalError;
+ }
+ if (!replayExistsQuery->next()) {
+ return Response::RespNameNotFound;
+ }
+
+ const auto &replayName = replayExistsQuery->value(0).toString();
+
+ // Check if hash is correct
+ if (hash != createHashForReplay(gameId.toInt())) {
+ return Response::RespNameNotFound;
+ }
+
+ // Determine if user already has access to replay
+ auto *alreadyAccessQuery = sqlInterface->prepareQuery(
+ "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player");
+ alreadyAccessQuery->bindValue(":id_game", gameId);
+ alreadyAccessQuery->bindValue(":id_player", userInfo->id());
+ if (!sqlInterface->execSqlQuery(alreadyAccessQuery)) {
+ return Response::RespInternalError;
+ }
+ if (alreadyAccessQuery->next()) {
+ return Response::RespOk;
+ }
+
+ // Grant the User access to the replay
+ auto *grantReplayAccessQuery =
+ sqlInterface->prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) "
+ "values(:idgame, :idplayer, :replayname, 0)");
+ grantReplayAccessQuery->bindValue(":idgame", gameId);
+ grantReplayAccessQuery->bindValue(":idplayer", userInfo->id());
+ grantReplayAccessQuery->bindValue(":replayname", replayName);
+
+ if (!sqlInterface->execSqlQuery(grantReplayAccessQuery)) {
+ return Response::RespInternalError;
+ }
+
+ // update user's view
+ Event_ReplayAdded event;
+ SessionEvent *se = prepareSessionEvent(event);
+ sendProtocolItem(*se);
+ delete se;
+
+ return Response::RespOk;
+}
+
// MODERATOR FUNCTIONS.
// May be called by admins and moderators. Permission is checked by the calling function.
Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Command_ViewLogHistory &cmd,
diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h
index 88d0fc549..54a852fd7 100644
--- a/servatrice/src/serversocketinterface.h
+++ b/servatrice/src/serversocketinterface.h
@@ -44,6 +44,8 @@ class Command_ReplayList;
class Command_ReplayDownload;
class Command_ReplayModifyMatch;
class Command_ReplayDeleteMatch;
+class Command_ReplayGetCode;
+class Command_ReplaySubmitCode;
class Command_BanFromServer;
class Command_UpdateServerMessage;
@@ -97,6 +99,9 @@ private:
Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc);
Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc);
Response::ResponseCode cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, ResponseContainer &rc);
+ QString createHashForReplay(int gameId);
+ Response::ResponseCode cmdReplayGetCode(const Command_ReplayGetCode &cmd, ResponseContainer &rc);
+ Response::ResponseCode cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, ResponseContainer &rc);
Response::ResponseCode cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer &rc);
Response::ResponseCode cmdWarnUser(const Command_WarnUser &cmd, ResponseContainer &rc);
Response::ResponseCode cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc);