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