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 31593bb1e..86855d780 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -424,6 +424,66 @@ public: { return false; } + /** @brief One report category and how many reports it covers. */ + struct ReportCategoryCountRecord + { + QString category; + int count = 0; + }; + /** @brief One reported or reporting user and how many reports involve them. */ + struct ReportTopUserRecord + { + QString userName; + int count = 0; + }; + /** @brief Aggregated report queue statistics for the moderator overview. */ + struct ReportStatsRecord + { + int totalReports = 0; + int totalPending = 0; + int totalAssigned = 0; + int totalResolved = 0; + int reportsLast24h = 0; + int reportsLast7d = 0; + int reportsLast30d = 0; + int reportsThisWeek = 0; + bool hasReportsLastWeek = false; + int reportsLastWeek = 0; + bool hasAvgResolutionHours = false; + double avgResolutionHours = 0.0; + QList categoryCounts; + QList topReportedUsers; + QList topReporters; + /// False when a non-overview query failed, which keeps the result out of the cache. + bool allQueriesOk = true; + }; + virtual bool getReportStats(ReportStatsRecord & /* stats */) + { + return false; + } + /** @brief Report history and moderation counts for one user, for the moderator overview. */ + struct ReportUserInfoRecord + { + bool found = false; + bool isAdmin = false; + bool isActive = false; + qint64 registrationTime = 0; + bool hasAdminNotes = false; + QString adminNotes; + bool hasTotalReports = false; + int totalReports = 0; + bool hasTotalBans = false; + int totalBans = 0; + bool hasTotalWarns = false; + int totalWarns = 0; + bool hasLastLogin = false; + qint64 lastLogin = 0; + QList recentReports; + }; + virtual bool getReportUserInfo(const QString & /* userName */, ReportUserInfoRecord & /* info */) + { + 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 dc576a241..88e0e9ea4 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -3843,6 +3843,204 @@ bool Servatrice_DatabaseInterface::getReportComments(int reportId, QList= DATE_SUB(NOW(), INTERVAL 1 DAY)) AS last24h, " + "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)) AS last7d, " + "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)) AS last30d, " + "SUM(created_at >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)) AS this_week " + "FROM {prefix}_reports"); + if (!execSqlQuery(overviewQuery) || !overviewQuery->next()) { + return false; + } + + stats.totalReports = overviewQuery->value(0).toInt(); + stats.totalPending = overviewQuery->value(1).toInt(); + stats.totalAssigned = overviewQuery->value(2).toInt(); + stats.totalResolved = overviewQuery->value(3).toInt(); + stats.reportsLast24h = overviewQuery->value(4).toInt(); + stats.reportsLast7d = overviewQuery->value(5).toInt(); + stats.reportsLast30d = overviewQuery->value(6).toInt(); + stats.reportsThisWeek = overviewQuery->value(7).toInt(); + + stats.allQueriesOk = true; + + QSqlQuery *lastWeekQuery = + prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE created_at >= DATE_SUB(CURDATE(), " + "INTERVAL WEEKDAY(CURDATE()) + 7 DAY) " + "AND created_at < DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)"); + if (execSqlQuery(lastWeekQuery) && lastWeekQuery->next()) { + stats.hasReportsLastWeek = true; + stats.reportsLastWeek = lastWeekQuery->value(0).toInt(); + } else { + stats.allQueriesOk = false; + } + + QSqlQuery *avgResQuery = + prepareQuery("SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, resolution_time)) " + "FROM {prefix}_reports WHERE status IN ('resolved','dismissed') AND resolution_time IS NOT NULL"); + if (execSqlQuery(avgResQuery) && avgResQuery->next()) { + if (!avgResQuery->value(0).isNull()) { + stats.hasAvgResolutionHours = true; + stats.avgResolutionHours = avgResQuery->value(0).toDouble(); + } + } else { + stats.allQueriesOk = false; + } + + QSqlQuery *catQuery = + prepareQuery("SELECT category, COUNT(*) AS cnt FROM {prefix}_reports GROUP BY category ORDER BY cnt DESC " + "LIMIT 10"); + if (execSqlQuery(catQuery)) { + while (catQuery->next()) { + ReportCategoryCountRecord record; + record.category = catQuery->value(0).toString(); + record.count = catQuery->value(1).toInt(); + stats.categoryCounts.append(record); + } + } else { + stats.allQueriesOk = false; + } + + QSqlQuery *topReportedQuery = prepareQuery("SELECT reported_user_name, COUNT(*) AS cnt FROM {prefix}_reports " + "GROUP BY reported_user_name ORDER BY cnt DESC LIMIT 10"); + if (execSqlQuery(topReportedQuery)) { + while (topReportedQuery->next()) { + ReportTopUserRecord record; + record.userName = topReportedQuery->value(0).toString(); + record.count = topReportedQuery->value(1).toInt(); + stats.topReportedUsers.append(record); + } + } else { + stats.allQueriesOk = false; + } + + QSqlQuery *topReporterQuery = prepareQuery("SELECT reporter_name, COUNT(*) AS cnt FROM {prefix}_reports " + "GROUP BY reporter_name ORDER BY cnt DESC LIMIT 10"); + if (execSqlQuery(topReporterQuery)) { + while (topReporterQuery->next()) { + ReportTopUserRecord record; + record.userName = topReporterQuery->value(0).toString(); + record.count = topReporterQuery->value(1).toInt(); + stats.topReporters.append(record); + } + } else { + stats.allQueriesOk = false; + } + + return true; +} + +bool Servatrice_DatabaseInterface::getReportUserInfo(const QString &userName, ReportUserInfoRecord &info) +{ + if (!checkSql()) { + return false; + } + + QSqlQuery *userQuery = prepareQuery("SELECT u.admin, u.active, u.registrationDate, u.adminnotes " + "FROM {prefix}_users u " + "WHERE u.name = :name"); + userQuery->bindValue(":name", userName); + + if (!execSqlQuery(userQuery)) { + return false; + } + + if (!userQuery->next()) { + return true; + } + + info.found = true; + info.isAdmin = userQuery->value(0).toBool(); + info.isActive = userQuery->value(1).toBool(); + info.registrationTime = userQuery->value(2).toDateTime().toSecsSinceEpoch(); + + if (!userQuery->value(3).isNull()) { + info.hasAdminNotes = true; + info.adminNotes = userQuery->value(3).toString(); + } + + QSqlQuery *reportCountQuery = + prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE reported_user_name = :name"); + reportCountQuery->bindValue(":name", userName); + if (execSqlQuery(reportCountQuery) && reportCountQuery->next()) { + info.hasTotalReports = true; + info.totalReports = reportCountQuery->value(0).toInt(); + } + + QSqlQuery *banCountQuery = prepareQuery("SELECT COUNT(*) FROM {prefix}_bans WHERE user_name = :name"); + banCountQuery->bindValue(":name", userName); + if (execSqlQuery(banCountQuery) && banCountQuery->next()) { + info.hasTotalBans = true; + info.totalBans = banCountQuery->value(0).toInt(); + } + + QSqlQuery *warnCountQuery = prepareQuery("SELECT COUNT(*) FROM {prefix}_warnings WHERE user_name = :name"); + warnCountQuery->bindValue(":name", userName); + if (execSqlQuery(warnCountQuery) && warnCountQuery->next()) { + info.hasTotalWarns = true; + info.totalWarns = warnCountQuery->value(0).toInt(); + } + + QSqlQuery *lastLoginQuery = prepareQuery("SELECT UNIX_TIMESTAMP(a.last_login) " + "FROM {prefix}_user_analytics a " + "JOIN {prefix}_users u ON u.id = a.id " + "WHERE u.name = :name"); + lastLoginQuery->bindValue(":name", userName); + if (execSqlQuery(lastLoginQuery) && lastLoginQuery->next() && !lastLoginQuery->value(0).isNull()) { + info.hasLastLogin = true; + info.lastLogin = lastLoginQuery->value(0).toLongLong(); + } + + QSqlQuery *recentQuery = prepareQuery("SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, " + "r.category, r.description, r.created_at, r.status, " + "r.resolution_note, ru.name AS assigned_mod_name " + "FROM {prefix}_reports r " + "LEFT JOIN {prefix}_users ru ON r.assigned_to = ru.id " + "WHERE r.reported_user_name = :name " + "ORDER BY r.created_at DESC LIMIT 10"); + recentQuery->bindValue(":name", userName); + + if (execSqlQuery(recentQuery)) { + while (recentQuery->next()) { + ServerInfo_Report report; + report.set_report_id(recentQuery->value(0).toInt()); + report.set_reporter_name(recentQuery->value(1).toString().toStdString()); + report.set_reported_user_name(recentQuery->value(2).toString().toStdString()); + + if (!recentQuery->value(3).isNull()) { + report.set_game_id(recentQuery->value(3).toInt()); + } + + report.set_category(recentQuery->value(4).toString().toStdString()); + report.set_description(recentQuery->value(5).toString().toStdString()); + report.set_report_time(recentQuery->value(6).toDateTime().toSecsSinceEpoch()); + report.set_status(recentQuery->value(7).toString().toStdString()); + + if (!recentQuery->value(8).isNull()) { + report.set_resolution_note(recentQuery->value(8).toString().toStdString()); + } + + if (!recentQuery->value(9).isNull()) { + report.set_assigned_mod_name(recentQuery->value(9).toString().toStdString()); + } + + info.recentReports.append(report); + } + } + + 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 00083ecf7..f1aec807f 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -301,6 +301,8 @@ public: Server_DatabaseInterface::ReportLookupResult getReportForDetails(int reportId, int &reporterId, ServerInfo_Report &report) override; bool getReportComments(int reportId, QList &comments) override; + bool getReportStats(ReportStatsRecord &stats) override; + bool getReportUserInfo(const QString &userName, ReportUserInfoRecord &info) override; bool addReport(int reporterId, const QString &reporterName, int reportedUserId, diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 91e2dea7f..38e4c8066 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -1992,99 +1992,50 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportResolve(const Com Response::ResponseCode AbstractServerSocketInterface::cmdReportUserInfo(const Command_ReportUserInfo &cmd, ResponseContainer &rc) { - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } - QString userName = nameFromStdString(cmd.user_name()); + const QString userName = nameFromStdString(cmd.user_name()); - QSqlQuery *userQuery = sqlInterface->prepareQuery("SELECT u.admin, u.active, u.registrationDate, u.adminnotes " - "FROM {prefix}_users u " - "WHERE u.name = :name"); - userQuery->bindValue(":name", userName); - - if (!sqlInterface->execSqlQuery(userQuery)) { + Server_DatabaseInterface::ReportUserInfoRecord userReportInfo; + if (!databaseInterface->getReportUserInfo(userName, userReportInfo)) { return Response::RespInternalError; } - if (!userQuery->next()) { + if (!userReportInfo.found) { return Response::RespNameNotFound; } Response_ReportUserInfo *re = new Response_ReportUserInfo; re->set_user_name(cmd.user_name()); - re->set_is_admin(userQuery->value(0).toBool()); - re->set_is_active(userQuery->value(1).toBool()); - re->set_registration_time(userQuery->value(2).toDateTime().toSecsSinceEpoch()); + re->set_is_admin(userReportInfo.isAdmin); + re->set_is_active(userReportInfo.isActive); + re->set_registration_time(userReportInfo.registrationTime); - if (!userQuery->value(3).isNull()) { - re->set_admin_notes(userQuery->value(3).toString().toStdString()); + if (userReportInfo.hasAdminNotes) { + re->set_admin_notes(userReportInfo.adminNotes.toStdString()); } - QSqlQuery *reportCountQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE reported_user_name = :name"); - reportCountQuery->bindValue(":name", userName); - if (sqlInterface->execSqlQuery(reportCountQuery) && reportCountQuery->next()) { - re->set_total_reports(reportCountQuery->value(0).toInt()); + if (userReportInfo.hasTotalReports) { + re->set_total_reports(userReportInfo.totalReports); } - QSqlQuery *banCountQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_bans WHERE user_name = :name"); - banCountQuery->bindValue(":name", userName); - if (sqlInterface->execSqlQuery(banCountQuery) && banCountQuery->next()) { - re->set_total_bans(banCountQuery->value(0).toInt()); + if (userReportInfo.hasTotalBans) { + re->set_total_bans(userReportInfo.totalBans); } - QSqlQuery *warnCountQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_warnings WHERE user_name = :name"); - warnCountQuery->bindValue(":name", userName); - if (sqlInterface->execSqlQuery(warnCountQuery) && warnCountQuery->next()) { - re->set_total_warns(warnCountQuery->value(0).toInt()); + if (userReportInfo.hasTotalWarns) { + re->set_total_warns(userReportInfo.totalWarns); } - QSqlQuery *lastLoginQuery = sqlInterface->prepareQuery("SELECT UNIX_TIMESTAMP(a.last_login) " - "FROM {prefix}_user_analytics a " - "JOIN {prefix}_users u ON u.id = a.id " - "WHERE u.name = :name"); - lastLoginQuery->bindValue(":name", userName); - if (sqlInterface->execSqlQuery(lastLoginQuery) && lastLoginQuery->next() && !lastLoginQuery->value(0).isNull()) { - re->set_last_login(lastLoginQuery->value(0).toLongLong()); + if (userReportInfo.hasLastLogin) { + re->set_last_login(userReportInfo.lastLogin); } - QSqlQuery *recentQuery = - sqlInterface->prepareQuery("SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, " - "r.category, r.description, r.created_at, r.status, " - "r.resolution_note, ru.name AS assigned_mod_name " - "FROM {prefix}_reports r " - "LEFT JOIN {prefix}_users ru ON r.assigned_to = ru.id " - "WHERE r.reported_user_name = :name " - "ORDER BY r.created_at DESC LIMIT 10"); - recentQuery->bindValue(":name", userName); - - if (sqlInterface->execSqlQuery(recentQuery)) { - while (recentQuery->next()) { - ServerInfo_Report *info = re->add_recent_reports(); - info->set_report_id(recentQuery->value(0).toInt()); - info->set_reporter_name(recentQuery->value(1).toString().toStdString()); - info->set_reported_user_name(recentQuery->value(2).toString().toStdString()); - - if (!recentQuery->value(3).isNull()) { - info->set_game_id(recentQuery->value(3).toInt()); - } - - info->set_category(recentQuery->value(4).toString().toStdString()); - info->set_description(recentQuery->value(5).toString().toStdString()); - info->set_report_time(recentQuery->value(6).toDateTime().toSecsSinceEpoch()); - info->set_status(recentQuery->value(7).toString().toStdString()); - - if (!recentQuery->value(8).isNull()) { - info->set_resolution_note(recentQuery->value(8).toString().toStdString()); - } - - if (!recentQuery->value(9).isNull()) { - info->set_assigned_mod_name(recentQuery->value(9).toString().toStdString()); - } - } + for (const ServerInfo_Report &report : userReportInfo.recentReports) { + re->add_recent_reports()->CopyFrom(report); } rc.setResponseExtension(re); @@ -2167,7 +2118,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetServerStats(const Co Response::ResponseCode AbstractServerSocketInterface::cmdReportStats(const Command_ReportStats & /*cmd */, ResponseContainer &rc) { - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } @@ -2176,94 +2127,48 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportStats(const Comma return Response::RespOk; } - Response_ReportStats *re = new Response_ReportStats; - - QSqlQuery *overviewQuery = sqlInterface->prepareQuery( - "SELECT COUNT(*) AS total, " - "SUM(status IN ('open', 'assigned')) AS pending, " - "SUM(status = 'assigned') AS assigned, " - "SUM(status IN ('resolved', 'dismissed')) AS resolved, " - "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)) AS last24h, " - "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)) AS last7d, " - "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)) AS last30d, " - "SUM(created_at >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)) AS this_week " - "FROM {prefix}_reports"); - if (!sqlInterface->execSqlQuery(overviewQuery) || !overviewQuery->next()) { - delete re; + Server_DatabaseInterface::ReportStatsRecord stats; + if (!databaseInterface->getReportStats(stats)) { return Response::RespInternalError; } - re->set_total_reports(overviewQuery->value(0).toInt()); - re->set_total_pending(overviewQuery->value(1).toInt()); - re->set_total_assigned(overviewQuery->value(2).toInt()); - re->set_total_resolved(overviewQuery->value(3).toInt()); - re->set_reports_last_24h(overviewQuery->value(4).toInt()); - re->set_reports_last_7d(overviewQuery->value(5).toInt()); - re->set_reports_last_30d(overviewQuery->value(6).toInt()); - re->set_reports_this_week(overviewQuery->value(7).toInt()); + auto *re = new Response_ReportStats; + re->set_total_reports(stats.totalReports); + re->set_total_pending(stats.totalPending); + re->set_total_assigned(stats.totalAssigned); + re->set_total_resolved(stats.totalResolved); + re->set_reports_last_24h(stats.reportsLast24h); + re->set_reports_last_7d(stats.reportsLast7d); + re->set_reports_last_30d(stats.reportsLast30d); + re->set_reports_this_week(stats.reportsThisWeek); - bool allQueriesOk = true; - - QSqlQuery *lastWeekQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE created_at >= DATE_SUB(CURDATE(), " - "INTERVAL WEEKDAY(CURDATE()) + 7 DAY) " - "AND created_at < DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)"); - if (sqlInterface->execSqlQuery(lastWeekQuery) && lastWeekQuery->next()) { - re->set_reports_last_week(lastWeekQuery->value(0).toInt()); - } else { - allQueriesOk = false; + if (stats.hasReportsLastWeek) { + re->set_reports_last_week(stats.reportsLastWeek); } - QSqlQuery *avgResQuery = sqlInterface->prepareQuery( - "SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, resolution_time)) " - "FROM {prefix}_reports WHERE status IN ('resolved','dismissed') AND resolution_time IS NOT NULL"); - if (sqlInterface->execSqlQuery(avgResQuery) && avgResQuery->next()) { - if (!avgResQuery->value(0).isNull()) { - re->set_avg_resolution_hours(avgResQuery->value(0).toDouble()); - } - } else { - allQueriesOk = false; + if (stats.hasAvgResolutionHours) { + re->set_avg_resolution_hours(stats.avgResolutionHours); } - QSqlQuery *catQuery = sqlInterface->prepareQuery( - "SELECT category, COUNT(*) AS cnt FROM {prefix}_reports GROUP BY category ORDER BY cnt DESC LIMIT 10"); - if (sqlInterface->execSqlQuery(catQuery)) { - while (catQuery->next()) { - ReportCategoryCount *cc = re->add_category_counts(); - cc->set_category(catQuery->value(0).toString().toStdString()); - cc->set_count(catQuery->value(1).toInt()); - } - } else { - allQueriesOk = false; + for (const Server_DatabaseInterface::ReportCategoryCountRecord &count : stats.categoryCounts) { + ReportCategoryCount *cc = re->add_category_counts(); + cc->set_category(count.category.toStdString()); + cc->set_count(count.count); } - QSqlQuery *topReportedQuery = - sqlInterface->prepareQuery("SELECT reported_user_name, COUNT(*) AS cnt FROM {prefix}_reports " - "GROUP BY reported_user_name ORDER BY cnt DESC LIMIT 10"); - if (sqlInterface->execSqlQuery(topReportedQuery)) { - while (topReportedQuery->next()) { - ReportTopUser *tu = re->add_top_reported_users(); - tu->set_user_name(topReportedQuery->value(0).toString().toStdString()); - tu->set_count(topReportedQuery->value(1).toInt()); - } - } else { - allQueriesOk = false; + for (const Server_DatabaseInterface::ReportTopUserRecord &user : stats.topReportedUsers) { + ReportTopUser *tu = re->add_top_reported_users(); + tu->set_user_name(user.userName.toStdString()); + tu->set_count(user.count); } - QSqlQuery *topReporterQuery = - sqlInterface->prepareQuery("SELECT reporter_name, COUNT(*) AS cnt FROM {prefix}_reports " - "GROUP BY reporter_name ORDER BY cnt DESC LIMIT 10"); - if (sqlInterface->execSqlQuery(topReporterQuery)) { - while (topReporterQuery->next()) { - ReportTopUser *tu = re->add_top_reporters(); - tu->set_user_name(topReporterQuery->value(0).toString().toStdString()); - tu->set_count(topReporterQuery->value(1).toInt()); - } - } else { - allQueriesOk = false; + for (const Server_DatabaseInterface::ReportTopUserRecord &user : stats.topReporters) { + ReportTopUser *tu = re->add_top_reporters(); + tu->set_user_name(user.userName.toStdString()); + tu->set_count(user.count); } - if (allQueriesOk) { + if (stats.allQueriesOk) { servatrice->cacheReportStats(*re); }