From ba037cbf04708f615e3dac54733851d5c3d485fd Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:16:24 +0100 Subject: [PATCH] [Server] Move deck share storage into the database interface (#7837) The five deck-share commands reached through Servatrice_DatabaseInterface for share lookups, and cmdDeckShareCreate still issued the per-user rate-limit count and the folder deck listing inline. Move DeckShareItemRecord and DeckShareSummaryRecord to Server_DatabaseInterface, declare the share operations plus getDeckSharesCreatedToday and getDeckFilesInFolder there with failing defaults, and switch the commands to the injected interface. The MySQL statements and every response code stay as they were, including the -1-on-failure sentinel for the count. --- .../server/remote/server_database_interface.h | 63 ++++++++++++++++++ .../src/servatrice_database_interface.cpp | 30 +++++++++ .../src/servatrice_database_interface.h | 38 ++++------- servatrice/src/serversocketinterface.cpp | 65 ++++++++----------- 4 files changed, 131 insertions(+), 65 deletions(-) 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 7d4d4c7fa..813e03de5 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -248,6 +248,69 @@ public: { return DeckStorageResult::Failed; } + /** @brief Metadata of a single deck inside a temporary deck share bundle. */ + struct DeckShareItemRecord + { + int id = -1; ///< Database id, used for downloads. + QString name; ///< Deck name. + QStringList tags; ///< Deck tags. + QString bannerCard; ///< Banner card name (deck image). + QString gameFormat; ///< Game format the deck was built for. + QString colorIdentity; ///< Color identity, e.g. "WUBRG". + QString content; ///< Deck content (native format); empty in list queries. + }; + /** @brief Summary of a share bundle owned by a user. */ + struct DeckShareSummaryRecord + { + int id = -1; ///< Database id, used for revocation. + QString name; ///< Share name. + qint64 creationTime = 0; ///< Unix timestamp at which the share was created. + qint64 expiresAt = 0; ///< Unix timestamp at which the share expires. + int itemCount = 0; ///< Number of decks in the bundle. + }; + /** @brief One deck file in a folder, as the share builder lists it. */ + struct DeckFileListItem + { + int id = -1; + QString colorIdentity; + }; + virtual bool createDeckShare(const QString & /* token */, + const QString & /* name */, + int /* userId */, + const QList & /* items */, + int /* expiryDays */, + qint64 & /* expiresAt */) + { + return false; + } + /** @brief Share bundles the user created in the last day, or -1 when the read failed. */ + virtual int getDeckSharesCreatedToday(int /* userId */) + { + return -1; + } + virtual bool getDeckSharesForUser(int /* userId */, QList & /* shares */) + { + return false; + } + virtual bool deleteDeckShare(int /* shareId */, int /* userId */) + { + return false; + } + virtual bool getDeckShareList(const QString & /* token */, + QString & /* name */, + qint64 & /* expiresAt */, + QList & /* items */) + { + return false; + } + virtual bool getDeckShareItem(const QString & /* token */, int /* itemId */, QString & /* content */) + { + return false; + } + virtual bool getDeckFilesInFolder(int /* folderId */, int /* userId */, QList & /* files */) + { + return false; + } /** @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 103c9304e..96d2afbfe 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -1825,6 +1825,36 @@ void Servatrice_DatabaseInterface::cleanupExpiredDeckShares() execSqlQuery(query); } +int Servatrice_DatabaseInterface::getDeckSharesCreatedToday(int userId) +{ + checkSql(); + + QSqlQuery *query = prepareQuery("select count(*) from {prefix}_deck_share where created_by = :created_by and " + "created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)"); + query->bindValue(":created_by", userId); + if (!execSqlQuery(query) || !query->next()) { + return -1; + } + return query->value(0).toInt(); +} + +bool Servatrice_DatabaseInterface::getDeckFilesInFolder(int folderId, int userId, QList &files) +{ + checkSql(); + + QSqlQuery *query = prepareQuery("select id, color_identity from {prefix}_decklist_files where id_folder = " + ":id_folder and id_user = :id_user"); + query->bindValue(":id_folder", folderId); + query->bindValue(":id_user", userId); + if (!execSqlQuery(query)) { + return false; + } + while (query->next()) { + files.append(DeckFileListItem{.id = query->value(0).toInt(), .colorIdentity = query->value(1).toString()}); + } + return true; +} + bool Servatrice_DatabaseInterface::getDeckSharesForUser(int userId, QList &shares) { checkSql(); diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index c535aa61e..fd73c1096 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -26,28 +26,6 @@ class QSqlQuery; class Servatrice; -/** @brief Metadata of a single deck inside a temporary deck share bundle. */ -struct DeckShareItemRecord -{ - int id = -1; ///< Database id, used for downloads. - QString name; ///< Deck name. - QStringList tags; ///< Deck tags. - QString bannerCard; ///< Banner card name (deck image). - QString gameFormat; ///< Game format the deck was built for. - QString colorIdentity; ///< Color identity, e.g. "WUBRG". - QString content; ///< Deck content (native format); empty in list queries. -}; - -/** @brief Summary of a share bundle owned by a user. */ -struct DeckShareSummaryRecord -{ - int id = -1; ///< Database id, used for revocation. - QString name; ///< Share name. - qint64 creationTime = 0; ///< Unix timestamp at which the share was created. - qint64 expiresAt = 0; ///< Unix timestamp at which the share expires. - int itemCount = 0; ///< Number of decks in the bundle. -}; - class Servatrice_DatabaseInterface : public Server_DatabaseInterface { Q_OBJECT @@ -156,24 +134,30 @@ public: int userId, const QList &items, int expiryDays, - qint64 &expiresAt); + qint64 &expiresAt) override; + int getDeckSharesCreatedToday(int userId) override; /** @brief Lists the share bundles created by a user, newest first. */ - bool getDeckSharesForUser(int userId, QList &shares); + bool getDeckSharesForUser(int userId, QList &shares) override; /** * @brief Deletes one of a user's own share bundles (cascades to its items). * @return false if no such bundle belongs to the user. */ - bool deleteDeckShare(int shareId, int userId); + bool deleteDeckShare(int shareId, int userId) override; /** * @brief Looks up a valid (non-expired) share bundle by token. * @return false if the token is unknown or expired. */ - bool getDeckShareList(const QString &token, QString &name, qint64 &expiresAt, QList &items); + bool getDeckShareList(const QString &token, + QString &name, + qint64 &expiresAt, + QList &items) override; /** * @brief Fetches the content of one item of a valid share bundle. * @return false if the token is unknown/expired or the item does not belong to the bundle. */ - bool getDeckShareItem(const QString &token, int itemId, QString &content); + 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; /** @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 8f6f99a4b..4512bcef9 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -1024,9 +1024,9 @@ QString generateShareToken() } /** @brief Extracts the share metadata for a deck, materializing its content. */ -DeckShareItemRecord makeShareItemFromDeck(const DeckList &deck, const QString &colorIdentity) +Server_DatabaseInterface::DeckShareItemRecord makeShareItemFromDeck(const DeckList &deck, const QString &colorIdentity) { - DeckShareItemRecord item; + Server_DatabaseInterface::DeckShareItemRecord item; item.name = deck.getName(); if (item.name.isEmpty()) { item.name = "Unnamed deck"; @@ -1047,7 +1047,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const C return Response::RespFunctionNotAllowed; } - sqlInterface->checkSql(); + databaseInterface->checkSql(); const int maxItems = servatrice->getDeckShareMaxDecksPerShare(); if (maxItems > 0 && cmd.items_size() > maxItems) { @@ -1058,19 +1058,16 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const C // word-of-mouth leak of public decks. const int maxSharesPerDay = servatrice->getDeckShareMaxSharesPerDay(); if (maxSharesPerDay > 0) { - QSqlQuery *countQuery = sqlInterface->prepareQuery("select count(*) from {prefix}_deck_share where " - "created_by = :created_by and created_at >= " - "DATE_SUB(NOW(), INTERVAL 1 DAY)"); - countQuery->bindValue(":created_by", userInfo->id()); - if (!sqlInterface->execSqlQuery(countQuery) || !countQuery->next()) { + const int createdToday = databaseInterface->getDeckSharesCreatedToday(userInfo->id()); + if (createdToday < 0) { return Response::RespContextError; } - if (countQuery->value(0).toInt() >= maxSharesPerDay) { + if (createdToday >= maxSharesPerDay) { return Response::RespTooManyRequests; } } - QList items; + QList items; if (cmd.items_size() > 0) { for (const DeckShareItem &shareItem : cmd.items()) { if (shareItem.has_deck_list()) { @@ -1082,7 +1079,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const C } else if (shareItem.has_deck_id()) { DeckList *deck; try { - deck = sqlInterface->getDeckFromDatabase(shareItem.deck_id(), userInfo->id()); + deck = databaseInterface->getDeckFromDatabase(shareItem.deck_id(), userInfo->id()); } catch (Response::ResponseCode &r) { return r; } @@ -1100,26 +1097,18 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const C // Drain the deck list before resolving each deck: getDeckFromDatabase // issues its own query on the same cached statement set. - QSqlQuery *query = - sqlInterface->prepareQuery("select id, color_identity from {prefix}_decklist_files where id_folder = " - ":id_folder and id_user = :id_user"); - query->bindValue(":id_folder", folderId); - query->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { + QList deckRows; + if (!databaseInterface->getDeckFilesInFolder(folderId, userInfo->id(), deckRows)) { return Response::RespContextError; } - QList> deckRows; - while (query->next()) { - deckRows.append({query->value(0).toInt(), query->value(1).toString()}); - } - for (const auto &[deckId, colorIdentity] : deckRows) { + for (const Server_DatabaseInterface::DeckFileListItem &deckRow : deckRows) { DeckList *deck; try { - deck = sqlInterface->getDeckFromDatabase(deckId, userInfo->id()); + deck = databaseInterface->getDeckFromDatabase(deckRow.id, userInfo->id()); } catch (Response::ResponseCode &r) { return r; } - items.append(makeShareItemFromDeck(*deck, colorIdentity)); + items.append(makeShareItemFromDeck(*deck, deckRow.colorIdentity)); delete deck; } } else { @@ -1137,8 +1126,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const C const QString token = generateShareToken(); qint64 expiresAt = 0; - if (!sqlInterface->createDeckShare(token, shareName, userInfo->id(), items, servatrice->getDeckShareExpiryDays(), - expiresAt)) { + if (!databaseInterface->createDeckShare(token, shareName, userInfo->id(), items, + servatrice->getDeckShareExpiryDays(), expiresAt)) { return Response::RespInvalidData; } @@ -1158,15 +1147,15 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareListMine(const return Response::RespFunctionNotAllowed; } - sqlInterface->checkSql(); + databaseInterface->checkSql(); - QList shares; - if (!sqlInterface->getDeckSharesForUser(userInfo->id(), shares)) { + QList shares; + if (!databaseInterface->getDeckSharesForUser(userInfo->id(), shares)) { return Response::RespContextError; } Response_DeckShareListMine *re = new Response_DeckShareListMine; - for (const DeckShareSummaryRecord &share : shares) { + for (const Server_DatabaseInterface::DeckShareSummaryRecord &share : shares) { ServerInfo_DeckShareSummary *summary = re->add_shares(); summary->set_id(share.id); summary->set_name(share.name.toStdString()); @@ -1190,9 +1179,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareRemove(const C return Response::RespInvalidData; } - sqlInterface->checkSql(); + databaseInterface->checkSql(); - if (!sqlInterface->deleteDeckShare(cmd.share_id(), userInfo->id())) { + if (!databaseInterface->deleteDeckShare(cmd.share_id(), userInfo->id())) { return Response::RespNameNotFound; } @@ -1206,19 +1195,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareList(const Com return Response::RespFunctionNotAllowed; } - sqlInterface->checkSql(); + databaseInterface->checkSql(); QString name; qint64 expiresAt = 0; - QList items; - if (!sqlInterface->getDeckShareList(nameFromStdString(cmd.token()), name, expiresAt, items)) { + QList items; + if (!databaseInterface->getDeckShareList(nameFromStdString(cmd.token()), name, expiresAt, items)) { return Response::RespNameNotFound; } Response_DeckShareList *re = new Response_DeckShareList; re->set_name(name.toStdString()); re->set_expires_at(expiresAt); - for (const DeckShareItemRecord &item : items) { + for (const Server_DatabaseInterface::DeckShareItemRecord &item : items) { ServerInfo_DeckShareItem *itemInfo = re->add_items(); itemInfo->set_id(item.id); itemInfo->set_name(item.name.toStdString()); @@ -1241,10 +1230,10 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareDownload(const return Response::RespFunctionNotAllowed; } - sqlInterface->checkSql(); + databaseInterface->checkSql(); QString content; - if (!sqlInterface->getDeckShareItem(nameFromStdString(cmd.token()), cmd.item_id(), content)) { + if (!databaseInterface->getDeckShareItem(nameFromStdString(cmd.token()), cmd.item_id(), content)) { return Response::RespNameNotFound; }