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 098a40535..709ad4bef 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -391,6 +391,65 @@ public: { return ReplayWriteResult::Failed; } + /** @brief One resolved or dismissed report whose reporter has not been told yet. */ + struct PendingReportNotification + { + int reportId = 0; + QString reportedUserName; + QString status; + QString resolutionNote; + }; + /** @brief One unannounced comment on a report the user reported, was assigned, or moderates. */ + struct PendingReportCommentNotification + { + qlonglong commentId = 0; + int reportId = 0; + QString authorName; + QString commentText; + }; + /** @brief Reports the reporter submitted in the last day, for the daily cap. */ + virtual int countReportsSubmittedInLastDay(int /* reporterId */) + { + return 0; + } + /** @brief Resolves a user name for the report's reported-user column, -1 when unknown. */ + virtual int getReportedUserId(const QString & /* userName */) + { + return -1; + } + /** @brief Stores a new open report, returning whether the insert succeeded. */ + virtual bool addReport(int /* reporterId */, + const QString & /* reporterName */, + int /* reportedUserId */, + const QString & /* reportedUserName */, + int /* gameId */, + int /* roomId */, + const QString & /* category */, + const QString & /* description */, + const QString & /* chatLog */) + { + return false; + } + /** @brief Collects the reporter's resolved or dismissed reports that were not announced yet. */ + virtual bool getPendingReportNotifications(int /* reporterId */, + QList & /* notifications */) + { + return false; + } + /** @brief Marks a report notification as delivered so it is not announced again. */ + virtual void markReportNotified(int /* reportId */) + { + } + /** @brief Collects comments on the user's reports that were not announced yet. */ + virtual bool getPendingReportCommentNotifications(int /* userId */, + QList & /* notifications */) + { + return false; + } + /** @brief Marks a comment notification as delivered so it is not announced again. */ + virtual void markReportCommentNotified(qlonglong /* commentId */) + { + } /** @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 a0b967cd1..93b75777c 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -3562,6 +3562,148 @@ bool Servatrice_DatabaseInterface::userEmailMatches(const QString &userName, con return validateTableColumnStringData("{prefix}_users", "email", userName, email); } +int Servatrice_DatabaseInterface::countReportsSubmittedInLastDay(int reporterId) +{ + if (!checkSql()) { + return 0; + } + + QSqlQuery *query = prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE reporter_id = :id " + "AND created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)"); + query->bindValue(":id", reporterId); + if (!execSqlQuery(query) || !query->next()) { + return 0; + } + + return query->value(0).toInt(); +} + +int Servatrice_DatabaseInterface::getReportedUserId(const QString &userName) +{ + if (!checkSql()) { + return -1; + } + + QSqlQuery *query = prepareQuery("SELECT id FROM {prefix}_users WHERE name = :name"); + query->bindValue(":name", userName); + if (!execSqlQuery(query) || !query->next()) { + return -1; + } + + return query->value(0).toInt(); +} + +bool Servatrice_DatabaseInterface::addReport(int reporterId, + const QString &reporterName, + int reportedUserId, + const QString &reportedUserName, + int gameId, + int roomId, + const QString &category, + const QString &description, + const QString &chatLog) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = prepareQuery("insert into {prefix}_reports " + "(reporter_id, reporter_name, reported_user_id, reported_user_name, " + "game_id, room_id, category, description, chat_log, created_at, status) " + "values " + "(:reporter_id, :reporter_name, :reported_user_id, :reported_user_name, " + ":game_id, :room_id, :category, :description, :chat_log, NOW(), 'open')"); + + query->bindValue(":reporter_id", reporterId); + query->bindValue(":reporter_name", reporterName); + query->bindValue(":reported_user_id", reportedUserId); + query->bindValue(":reported_user_name", reportedUserName); + query->bindValue(":game_id", gameId > 0 ? gameId : QVariant()); + query->bindValue(":room_id", roomId > 0 ? roomId : QVariant()); + query->bindValue(":category", category); + query->bindValue(":description", description); + query->bindValue(":chat_log", chatLog.isEmpty() ? QVariant() : chatLog); + + return execSqlQuery(query); +} + +bool Servatrice_DatabaseInterface::getPendingReportNotifications( + int reporterId, + QList ¬ifications) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = prepareQuery("SELECT id, reported_user_name, status, resolution_note " + "FROM {prefix}_reports " + "WHERE reporter_id = :reporter_id AND notified = 0 " + "AND status IN ('resolved', 'dismissed')"); + query->bindValue(":reporter_id", reporterId); + if (!execSqlQuery(query)) { + return false; + } + + while (query->next()) { + PendingReportNotification notification; + notification.reportId = query->value(0).toInt(); + notification.reportedUserName = query->value(1).toString(); + notification.status = query->value(2).toString(); + notification.resolutionNote = query->value(3).toString(); + notifications.append(notification); + } + + return true; +} + +void Servatrice_DatabaseInterface::markReportNotified(int reportId) +{ + QSqlQuery *query = prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); + query->bindValue(":id", reportId); + execSqlQuery(query); +} + +bool Servatrice_DatabaseInterface::getPendingReportCommentNotifications( + int userId, + QList ¬ifications) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = prepareQuery("SELECT c.id, c.report_id, c.author_name, c.comment_text " + "FROM {prefix}_report_comments c " + "JOIN {prefix}_reports r ON r.id = c.report_id " + "WHERE c.notified = 0 " + "AND c.author_id != :user_id " + "AND ((c.is_moderator = 1 AND r.reporter_id = :user_id) " + "OR (c.is_moderator = 0 AND r.assigned_to = :user_id) " + "OR (c.is_moderator = 1 AND r.assigned_to = :user_id)) " + "ORDER BY c.created_at ASC"); + query->bindValue(":user_id", userId); + if (!execSqlQuery(query)) { + return false; + } + + while (query->next()) { + PendingReportCommentNotification notification; + notification.commentId = query->value(0).toLongLong(); + notification.reportId = query->value(1).toInt(); + notification.authorName = query->value(2).toString(); + notification.commentText = query->value(3).toString(); + notifications.append(notification); + } + + return true; +} + +void Servatrice_DatabaseInterface::markReportCommentNotified(qlonglong commentId) +{ + QSqlQuery *query = prepareQuery("UPDATE {prefix}_report_comments SET notified = 1 WHERE id = :id"); + query->bindValue(":id", commentId); + execSqlQuery(query); +} + void Servatrice_DatabaseInterface::addAuditRecord(const QString &user, const QString &ipaddress, const QString &clientid, diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 1d9e53279..bdaa72a62 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -289,6 +289,25 @@ public: const QString &action, const QString &details, bool results) override; + int countReportsSubmittedInLastDay(int reporterId) override; + int getReportedUserId(const QString &userName) override; + bool addReport(int reporterId, + const QString &reporterName, + int reportedUserId, + const QString &reportedUserName, + int gameId, + int roomId, + const QString &category, + const QString &description, + const QString &chatLog) override; + bool + getPendingReportNotifications(int reporterId, + QList ¬ifications) override; + void markReportNotified(int reportId) override; + bool getPendingReportCommentNotifications( + int userId, + QList ¬ifications) override; + void markReportCommentNotified(qlonglong commentId) override; }; #endif diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 788a6f7c7..f0e0928a1 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -2582,77 +2582,45 @@ AbstractServerSocketInterface::cmdClearBlacklistedBanners(const Command_ClearBla void AbstractServerSocketInterface::sendPendingReportNotifications(ResponseContainer &rc) { - if (!sqlInterface->checkSql()) { + QList reportNotifications; + if (!databaseInterface->getPendingReportNotifications(userInfo->id(), reportNotifications)) { return; } - QSqlQuery *query = sqlInterface->prepareQuery("SELECT id, reported_user_name, status, resolution_note " - "FROM {prefix}_reports " - "WHERE reporter_id = :reporter_id AND notified = 0 " - "AND status IN ('resolved', 'dismissed')"); - query->bindValue(":reporter_id", userInfo->id()); - - if (!sqlInterface->execSqlQuery(query)) { - return; - } - - while (query->next()) { - const int reportId = query->value(0).toInt(); - const QString reportedUser = query->value(1).toString(); - const bool dismissed = query->value(2).toString() == "dismissed"; - const QString note = query->value(3).toString(); + for (const Server_DatabaseInterface::PendingReportNotification ¬ification : reportNotifications) { + const bool dismissed = notification.status == "dismissed"; Event_NotifyUser event; event.set_type(Event_NotifyUser::REPORT_RESOLVED); event.set_custom_title(dismissed ? tr("Report Dismissed").toStdString() : tr("Report Resolved").toStdString()); - QString content = dismissed ? tr("Your report about %1 has been dismissed.").arg(reportedUser) - : tr("Your report about %1 has been resolved.").arg(reportedUser); - if (!note.isEmpty()) { - content += "\n" + tr("Note: %1").arg(note); + QString content = dismissed ? tr("Your report about %1 has been dismissed.").arg(notification.reportedUserName) + : tr("Your report about %1 has been resolved.").arg(notification.reportedUserName); + if (!notification.resolutionNote.isEmpty()) { + content += "\n" + tr("Note: %1").arg(notification.resolutionNote); } event.set_custom_content(content.toStdString()); rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", reportId); - sqlInterface->execSqlQuery(notifiedQuery); + databaseInterface->markReportNotified(notification.reportId); } - QSqlQuery *commentQuery = sqlInterface->prepareQuery("SELECT c.id, c.report_id, c.author_name, c.comment_text " - "FROM {prefix}_report_comments c " - "JOIN {prefix}_reports r ON r.id = c.report_id " - "WHERE c.notified = 0 " - "AND c.author_id != :user_id " - "AND ((c.is_moderator = 1 AND r.reporter_id = :user_id) " - "OR (c.is_moderator = 0 AND r.assigned_to = :user_id) " - "OR (c.is_moderator = 1 AND r.assigned_to = :user_id)) " - "ORDER BY c.created_at ASC"); - commentQuery->bindValue(":user_id", userInfo->id()); - - if (!sqlInterface->execSqlQuery(commentQuery)) { + QList commentNotifications; + if (!databaseInterface->getPendingReportCommentNotifications(userInfo->id(), commentNotifications)) { return; } - while (commentQuery->next()) { - const qlonglong commentId = commentQuery->value(0).toLongLong(); - const int reportId = commentQuery->value(1).toInt(); - const QString authorName = commentQuery->value(2).toString(); - const QString commentText = commentQuery->value(3).toString(); - + for (const Server_DatabaseInterface::PendingReportCommentNotification ¬ification : commentNotifications) { Event_NotifyUser event; event.set_type(Event_NotifyUser::REPORT_COMMENT); - event.set_custom_title(tr("New Comment on Report #%1").arg(reportId).toStdString()); - event.set_custom_content(tr("%1 commented:\n%2").arg(authorName, commentText).toStdString()); + event.set_custom_title(tr("New Comment on Report #%1").arg(notification.reportId).toStdString()); + event.set_custom_content( + tr("%1 commented:\n%2").arg(notification.authorName, notification.commentText).toStdString()); rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_report_comments SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", commentId); - sqlInterface->execSqlQuery(notifiedQuery); + databaseInterface->markReportCommentNotified(notification.commentId); } } @@ -3874,7 +3842,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReport(const Command_Re return Response::RespFunctionNotAllowed; } - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } @@ -3897,24 +3865,11 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReport(const Command_Re } const int maxReportsPerDay = settingsCache->value("reporting/max_reports_per_day", 10).toInt(); - if (maxReportsPerDay > 0) { - QSqlQuery *countQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE reporter_id = :id " - "AND created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)"); - countQuery->bindValue(":id", userInfo->id()); - if (sqlInterface->execSqlQuery(countQuery) && countQuery->next() && - countQuery->value(0).toInt() >= maxReportsPerDay) { - return Response::RespTooManyRequests; - } - } - - int reportedUserId = -1; - QSqlQuery *lookupQuery = sqlInterface->prepareQuery("SELECT id FROM {prefix}_users WHERE name = :name"); - lookupQuery->bindValue(":name", reportedUser); - if (sqlInterface->execSqlQuery(lookupQuery) && lookupQuery->next()) { - reportedUserId = lookupQuery->value(0).toInt(); + if (maxReportsPerDay > 0 && databaseInterface->countReportsSubmittedInLastDay(userInfo->id()) >= maxReportsPerDay) { + return Response::RespTooManyRequests; } + const int reportedUserId = databaseInterface->getReportedUserId(reportedUser); if (reportedUserId == -1) { return Response::RespNameNotFound; } @@ -3933,28 +3888,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReport(const Command_Re } } - QSqlQuery *query = - sqlInterface->prepareQuery("insert into {prefix}_reports " - "(reporter_id, reporter_name, reported_user_id, reported_user_name, " - "game_id, room_id, category, description, chat_log, created_at, status) " - "values " - "(:reporter_id, :reporter_name, :reported_user_id, :reported_user_name, " - ":game_id, :room_id, :category, :description, :chat_log, NOW(), 'open')"); - - query->bindValue(":reporter_id", userInfo->id()); - query->bindValue(":reporter_name", reporterName); - query->bindValue(":reported_user_id", reportedUserId); - query->bindValue(":reported_user_name", reportedUser); - - query->bindValue(":game_id", gameId > 0 ? gameId : QVariant()); - - query->bindValue(":room_id", roomId > 0 ? roomId : QVariant()); - - query->bindValue(":category", category); - query->bindValue(":description", description); - query->bindValue(":chat_log", chatLog.isEmpty() ? QVariant() : chatLog); - - if (!sqlInterface->execSqlQuery(query)) { + if (!databaseInterface->addReport(userInfo->id(), reporterName, reportedUserId, reportedUser, gameId, roomId, + category, description, chatLog)) { return Response::RespInternalError; }