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 86855d780..1f3351ac2 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -484,6 +484,72 @@ public: { return false; } + /** @brief Outcome of a report state change. */ + enum class ReportUpdateResult + { + Ok, + InvalidState, + Failed + }; + /** @brief Outcome of claiming an open report for a moderator. */ + enum class ReportAssignResult + { + Ok, + NotFound, + NotOpen, + Failed + }; + virtual ReportAssignResult assignReport(int /* reportId */, int /* moderatorId */) + { + return ReportAssignResult::Failed; + } + virtual ReportUpdateResult + resolveReport(int /* reportId */, const QString & /* status */, const QString & /* note */, int /* moderatorId */) + { + return ReportUpdateResult::Failed; + } + virtual ReportUpdateResult reopenReport(int /* reportId */) + { + return ReportUpdateResult::Failed; + } + /** @brief The user name behind a database user id; false when the lookup fails or finds no row. */ + virtual bool getUserNameById(int /* userId */, QString & /* userName */) + { + return false; + } + /** @brief The reporter and reported user of a report, for the resolve notification. */ + virtual bool + getReportNotificationTarget(int /* reportId */, int & /* reporterId */, QString & /* reportedUserName */) + { + return false; + } + /** @brief Comments the author wrote in the last hour, for the hourly cap. */ + virtual int countReportCommentsInLastHour(int /* authorId */) + { + return 0; + } + /** @brief One report's reporter, assignee and status, for the comment checks. */ + struct ReportCommentTargetRecord + { + bool found = false; + int reporterId = 0; + QString reporterName; + int assignedToId = 0; + QString status; + }; + virtual bool getReportCommentTarget(int /* reportId */, ReportCommentTargetRecord & /* target */) + { + return false; + } + virtual bool addReportComment(int /* reportId */, + const QString & /* authorName */, + int /* authorId */, + const QString & /* commentText */, + bool /* isModerator */, + qlonglong & /* commentId */) + { + return false; + } /** @brief One resolved or dismissed report whose reporter has not been told yet. */ struct PendingReportNotification { diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 88e0e9ea4..c60d6feae 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -4041,6 +4041,203 @@ bool Servatrice_DatabaseInterface::getReportUserInfo(const QString &userName, Re return true; } +Server_DatabaseInterface::ReportAssignResult Servatrice_DatabaseInterface::assignReport(int reportId, int moderatorId) +{ + if (!checkSql()) { + return ReportAssignResult::Failed; + } + + QSqlQuery *lookupQuery = prepareQuery("SELECT status FROM {prefix}_reports WHERE id = :id"); + lookupQuery->bindValue(":id", reportId); + + if (!execSqlQuery(lookupQuery)) { + return ReportAssignResult::Failed; + } + + if (!lookupQuery->next()) { + return ReportAssignResult::NotFound; + } + + if (lookupQuery->value(0).toString() != "open") { + return ReportAssignResult::NotOpen; + } + + QSqlQuery *query = prepareQuery("UPDATE {prefix}_reports " + "SET status = 'assigned', assigned_to = :mod_id " + "WHERE id = :id AND status = 'open'"); + + query->bindValue(":mod_id", moderatorId); + query->bindValue(":id", reportId); + + if (!execSqlQuery(query)) { + return ReportAssignResult::Failed; + } + + if (query->numRowsAffected() == 0) { + // The report was taken by another moderator between the lookup and this update. + return ReportAssignResult::NotOpen; + } + + return ReportAssignResult::Ok; +} + +Server_DatabaseInterface::ReportUpdateResult +Servatrice_DatabaseInterface::resolveReport(int reportId, const QString &status, const QString ¬e, int moderatorId) +{ + if (!checkSql()) { + return ReportUpdateResult::Failed; + } + + QSqlQuery *query = prepareQuery("UPDATE {prefix}_reports " + "SET status = :status, resolution_note = :note, " + "resolution_time = NOW(), resolved_by = :mod_id " + "WHERE id = :id AND status IN ('open', 'assigned')"); + + query->bindValue(":status", status); + query->bindValue(":note", note); + query->bindValue(":mod_id", moderatorId); + query->bindValue(":id", reportId); + + if (!execSqlQuery(query)) { + return ReportUpdateResult::Failed; + } + + if (query->numRowsAffected() == 0) { + return ReportUpdateResult::InvalidState; + } + + return ReportUpdateResult::Ok; +} + +Server_DatabaseInterface::ReportUpdateResult Servatrice_DatabaseInterface::reopenReport(int reportId) +{ + if (!checkSql()) { + return ReportUpdateResult::Failed; + } + + QSqlQuery *query = prepareQuery("UPDATE {prefix}_reports " + "SET status = 'open', resolution_note = NULL, " + "resolution_time = NULL, resolved_by = NULL, assigned_to = NULL, " + "notified = 0 " + "WHERE id = :id AND status IN ('resolved', 'dismissed')"); + + query->bindValue(":id", reportId); + + if (!execSqlQuery(query)) { + return ReportUpdateResult::Failed; + } + + if (query->numRowsAffected() == 0) { + return ReportUpdateResult::InvalidState; + } + + return ReportUpdateResult::Ok; +} + +bool Servatrice_DatabaseInterface::getUserNameById(int userId, QString &userName) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); + query->bindValue(":id", userId); + if (!execSqlQuery(query) || !query->next()) { + return false; + } + + userName = query->value(0).toString(); + return true; +} + +bool Servatrice_DatabaseInterface::getReportNotificationTarget(int reportId, int &reporterId, QString &reportedUserName) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = prepareQuery("SELECT reporter_id, reported_user_name FROM {prefix}_reports WHERE id = :id"); + query->bindValue(":id", reportId); + if (!execSqlQuery(query) || !query->next()) { + return false; + } + + reporterId = query->value(0).toInt(); + reportedUserName = query->value(1).toString(); + return true; +} + +int Servatrice_DatabaseInterface::countReportCommentsInLastHour(int authorId) +{ + if (!checkSql()) { + return 0; + } + + QSqlQuery *query = prepareQuery("SELECT COUNT(*) FROM {prefix}_report_comments WHERE author_id = :id " + "AND created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)"); + query->bindValue(":id", authorId); + if (!execSqlQuery(query) || !query->next()) { + return 0; + } + + return query->value(0).toInt(); +} + +bool Servatrice_DatabaseInterface::getReportCommentTarget(int reportId, ReportCommentTargetRecord &target) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = + prepareQuery("SELECT reporter_id, reporter_name, assigned_to, status FROM {prefix}_reports WHERE id = :id"); + query->bindValue(":id", reportId); + + if (!execSqlQuery(query)) { + return false; + } + + if (!query->next()) { + return true; + } + + target.found = true; + target.reporterId = query->value(0).toInt(); + target.reporterName = query->value(1).toString(); + target.assignedToId = query->value(2).toInt(); + target.status = query->value(3).toString(); + return true; +} + +bool Servatrice_DatabaseInterface::addReportComment(int reportId, + const QString &authorName, + int authorId, + const QString &commentText, + bool isModerator, + qlonglong &commentId) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *query = + prepareQuery("INSERT INTO {prefix}_report_comments (report_id, author_name, author_id, " + "comment_text, created_at, is_moderator) " + "VALUES (:report_id, :author_name, :author_id, :comment_text, NOW(), :is_moderator)"); + query->bindValue(":report_id", reportId); + query->bindValue(":author_name", authorName); + query->bindValue(":author_id", authorId); + query->bindValue(":comment_text", commentText); + query->bindValue(":is_moderator", isModerator); + + if (!execSqlQuery(query)) { + return false; + } + + commentId = query->lastInsertId().toLongLong(); + return true; +} + int Servatrice_DatabaseInterface::countReportsSubmittedInLastDay(int reporterId) { if (!checkSql()) { diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index f1aec807f..076ab6523 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -303,6 +303,20 @@ public: bool getReportComments(int reportId, QList &comments) override; bool getReportStats(ReportStatsRecord &stats) override; bool getReportUserInfo(const QString &userName, ReportUserInfoRecord &info) override; + Server_DatabaseInterface::ReportAssignResult assignReport(int reportId, int moderatorId) override; + Server_DatabaseInterface::ReportUpdateResult + resolveReport(int reportId, const QString &status, const QString ¬e, int moderatorId) override; + Server_DatabaseInterface::ReportUpdateResult reopenReport(int reportId) override; + bool getUserNameById(int userId, QString &userName) override; + bool getReportNotificationTarget(int reportId, int &reporterId, QString &reportedUserName) override; + int countReportCommentsInLastHour(int authorId) override; + bool getReportCommentTarget(int reportId, ReportCommentTargetRecord &target) override; + bool addReportComment(int reportId, + const QString &authorName, + int authorId, + const QString &commentText, + bool isModerator, + qlonglong &commentId) override; bool addReport(int reporterId, const QString &reporterName, int reportedUserId, diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 38e4c8066..801a40de8 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -1849,42 +1849,22 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman Response::ResponseCode AbstractServerSocketInterface::cmdReportAssign(const Command_ReportAssign &cmd, ResponseContainer & /*rc*/) { - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } - int reportId = cmd.report_id(); - int modId = userInfo->id(); + const int reportId = cmd.report_id(); + const int modId = userInfo->id(); - QSqlQuery *lookupQuery = sqlInterface->prepareQuery("SELECT status FROM {prefix}_reports WHERE id = :id"); - lookupQuery->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(lookupQuery)) { - return Response::RespInternalError; - } - - if (!lookupQuery->next()) { - return Response::RespNameNotFound; - } - - if (lookupQuery->value(0).toString() != "open") { - return Response::RespInvalidData; - } - - QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports " - "SET status = 'assigned', assigned_to = :mod_id " - "WHERE id = :id AND status = 'open'"); - - query->bindValue(":mod_id", modId); - query->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - if (query->numRowsAffected() == 0) { - // The report was taken by another moderator between the lookup and this update. - return Response::RespInvalidData; + switch (databaseInterface->assignReport(reportId, modId)) { + case Server_DatabaseInterface::ReportAssignResult::Ok: + break; + case Server_DatabaseInterface::ReportAssignResult::NotFound: + return Response::RespNameNotFound; + case Server_DatabaseInterface::ReportAssignResult::NotOpen: + return Response::RespInvalidData; + case Server_DatabaseInterface::ReportAssignResult::Failed: + return Response::RespInternalError; } return Response::RespOk; @@ -1893,32 +1873,23 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportAssign(const Comm Response::ResponseCode AbstractServerSocketInterface::cmdReportResolve(const Command_ReportResolve &cmd, ResponseContainer & /*rc*/) { - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } - int reportId = cmd.report_id(); - QString note = textFromStdString(cmd.resolution_note()); - bool dismissed = cmd.dismissed(); + const int reportId = cmd.report_id(); + const QString note = textFromStdString(cmd.resolution_note()); + const bool dismissed = cmd.dismissed(); - QString newStatus = dismissed ? "dismissed" : "resolved"; + const QString newStatus = dismissed ? "dismissed" : "resolved"; - QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports " - "SET status = :status, resolution_note = :note, " - "resolution_time = NOW(), resolved_by = :mod_id " - "WHERE id = :id AND status IN ('open', 'assigned')"); - - query->bindValue(":status", newStatus); - query->bindValue(":note", note); - query->bindValue(":mod_id", userInfo->id()); - query->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - if (query->numRowsAffected() == 0) { - return Response::RespInvalidData; + switch (databaseInterface->resolveReport(reportId, newStatus, note, userInfo->id())) { + case Server_DatabaseInterface::ReportUpdateResult::Ok: + break; + case Server_DatabaseInterface::ReportUpdateResult::InvalidState: + return Response::RespInvalidData; + case Server_DatabaseInterface::ReportUpdateResult::Failed: + return Response::RespInternalError; } databaseInterface->addAuditRecord(QString::number(reportId), this->getAddress(), @@ -1929,21 +1900,11 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportResolve(const Com // Notifying the reporter is best-effort: the resolve already succeeded. If any of the lookup // queries below fail, the report is simply left with notified = 0 and the notification is // delivered later via sendPendingReportNotifications. - QSqlQuery *lookupQuery = - sqlInterface->prepareQuery("SELECT reporter_id, reported_user_name FROM {prefix}_reports WHERE id = :id"); - lookupQuery->bindValue(":id", reportId); - - QString reporterName; + int reporterId = -1; QString reportedUser; - if (sqlInterface->execSqlQuery(lookupQuery) && lookupQuery->next()) { - int reporterId = lookupQuery->value(0).toInt(); - reportedUser = lookupQuery->value(1).toString(); - - QSqlQuery *nameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); - nameQuery->bindValue(":id", reporterId); - if (sqlInterface->execSqlQuery(nameQuery) && nameQuery->next()) { - reporterName = nameQuery->value(0).toString(); - } + QString reporterName; + if (databaseInterface->getReportNotificationTarget(reportId, reporterId, reportedUser)) { + databaseInterface->getUserNameById(reporterId, reporterName); } const QString ownName = QString::fromStdString(userInfo->name()); @@ -1951,10 +1912,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportResolve(const Com if (reporterName == ownName) { // Resolving your own report: no self-notification needed, but mark it as notified // so it is not delivered on the next login via sendPendingReportNotifications. - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", reportId); - sqlInterface->execSqlQuery(notifiedQuery); + databaseInterface->markReportNotified(reportId); } else { QReadLocker clientsLocker(&servatrice->clientsLock); AbstractServerSocketInterface *reporter = @@ -1978,10 +1936,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportResolve(const Com // Only mark the report as notified if the reporter is still connected; otherwise the // notification is delivered on their next login via sendPendingReportNotifications. - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", reportId); - sqlInterface->execSqlQuery(notifiedQuery); + databaseInterface->markReportNotified(reportId); } } } @@ -2472,27 +2427,20 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Comm Response::ResponseCode AbstractServerSocketInterface::cmdReportReopen(const Command_ReportReopen &cmd, ResponseContainer & /*rc*/) { - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } - int reportId = cmd.report_id(); - QString note = textFromStdString(cmd.reopen_note()); + const int reportId = cmd.report_id(); + const QString note = textFromStdString(cmd.reopen_note()); - QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports " - "SET status = 'open', resolution_note = NULL, " - "resolution_time = NULL, resolved_by = NULL, assigned_to = NULL, " - "notified = 0 " - "WHERE id = :id AND status IN ('resolved', 'dismissed')"); - - query->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - if (query->numRowsAffected() == 0) { - return Response::RespInvalidData; + switch (databaseInterface->reopenReport(reportId)) { + case Server_DatabaseInterface::ReportUpdateResult::Ok: + break; + case Server_DatabaseInterface::ReportUpdateResult::InvalidState: + return Response::RespInvalidData; + case Server_DatabaseInterface::ReportUpdateResult::Failed: + return Response::RespInternalError; } QString auditDetails = QString("Report #%1 reopened").arg(reportId); @@ -2555,89 +2503,58 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportAddComment(const return Response::RespFunctionNotAllowed; } - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } - int reportId = cmd.report_id(); - QString commentText = textFromStdString(cmd.comment()).trimmed(); + const int reportId = cmd.report_id(); + const QString commentText = textFromStdString(cmd.comment()).trimmed(); if (commentText.isEmpty()) { return Response::RespInvalidData; } const int maxCommentsPerHour = settingsCache->value("reporting/max_comments_per_hour", 30).toInt(); - if (maxCommentsPerHour > 0) { - QSqlQuery *countQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_report_comments WHERE author_id = :id " - "AND created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)"); - countQuery->bindValue(":id", userInfo->id()); - if (sqlInterface->execSqlQuery(countQuery) && countQuery->next() && - countQuery->value(0).toInt() >= maxCommentsPerHour) { - return Response::RespTooManyRequests; - } + if (maxCommentsPerHour > 0 && + databaseInterface->countReportCommentsInLastHour(userInfo->id()) >= maxCommentsPerHour) { + return Response::RespTooManyRequests; } - QSqlQuery *checkQuery = sqlInterface->prepareQuery( - "SELECT reporter_id, reporter_name, assigned_to, status FROM {prefix}_reports WHERE id = :id"); - checkQuery->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(checkQuery)) { + Server_DatabaseInterface::ReportCommentTargetRecord target; + if (!databaseInterface->getReportCommentTarget(reportId, target)) { return Response::RespInternalError; } - if (!checkQuery->next()) { + if (!target.found) { return Response::RespNameNotFound; } - int reporterId = checkQuery->value(0).toInt(); - QString reporterName = checkQuery->value(1).toString(); - int assignedToId = checkQuery->value(2).toInt(); - - bool isMod = userInfo->user_level() & ServerInfo_User::IsModerator; + const bool isMod = userInfo->user_level() & ServerInfo_User::IsModerator; // Only the reporter (by id) or a moderator may comment on a report. - if (reporterId != userInfo->id() && !isMod) { + if (target.reporterId != userInfo->id() && !isMod) { return Response::RespAccessDenied; } - QString reportStatus = checkQuery->value(3).toString(); - if (reportStatus == "resolved" || reportStatus == "dismissed") { + if (target.status == "resolved" || target.status == "dismissed") { return Response::RespInvalidData; } - QSqlQuery *insertQuery = sqlInterface->prepareQuery( - "INSERT INTO {prefix}_report_comments (report_id, author_name, author_id, comment_text, created_at, " - "is_moderator) " - "VALUES (:report_id, :author_name, :author_id, :comment_text, NOW(), :is_moderator)"); - insertQuery->bindValue(":report_id", reportId); - insertQuery->bindValue(":author_name", QString::fromStdString(userInfo->name())); - insertQuery->bindValue(":author_id", userInfo->id()); - insertQuery->bindValue(":comment_text", commentText); - insertQuery->bindValue(":is_moderator", isMod); - - if (!sqlInterface->execSqlQuery(insertQuery)) { + qlonglong commentId = 0; + if (!databaseInterface->addReportComment(reportId, QString::fromStdString(userInfo->name()), userInfo->id(), + commentText, isMod, commentId)) { return Response::RespInternalError; } - const qlonglong commentId = insertQuery->lastInsertId().toLongLong(); - QStringList recipients; if (isMod) { // A moderator comment reaches both the reporter and (if assigned) the assigned moderator. - recipients.append(reporterName); - if (assignedToId > 0) { - QSqlQuery *modNameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); - modNameQuery->bindValue(":id", assignedToId); - if (sqlInterface->execSqlQuery(modNameQuery) && modNameQuery->next()) { - recipients.append(modNameQuery->value(0).toString()); - } - } - } else if (assignedToId > 0) { - QSqlQuery *modNameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); - modNameQuery->bindValue(":id", assignedToId); - if (sqlInterface->execSqlQuery(modNameQuery) && modNameQuery->next()) { - recipients.append(modNameQuery->value(0).toString()); + recipients.append(target.reporterName); + } + if (target.assignedToId > 0) { + QString modName; + if (databaseInterface->getUserNameById(target.assignedToId, modName)) { + recipients.append(modName); } } @@ -2670,10 +2587,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportAddComment(const } if (allNotified) { - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_report_comments SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", commentId); - sqlInterface->execSqlQuery(notifiedQuery); + databaseInterface->markReportCommentNotified(commentId); } return Response::RespOk;