diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp index 1fc2854ce..f18aa82b9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -29,7 +30,9 @@ #include #include #include +#include #include +#include #include class QObject; @@ -172,6 +175,7 @@ void TabCardArtRules::setupUi() addBtn = new QPushButton; removeBtn = new QPushButton; refreshBtn = new QPushButton; + clearBannedArtBtn = new QPushButton; modeBox->addItems({"ALLOW", "DENY"}); @@ -188,10 +192,14 @@ void TabCardArtRules::setupUi() form->addRow(tr("Mode:"), modeBox); form->addRow(tr("Reason:"), reasonEdit); + // The bulk clear acts on every user, so keep it visibly apart from the + // rules this tab edits. auto *buttons = new QHBoxLayout; buttons->addWidget(addBtn); buttons->addWidget(removeBtn); buttons->addWidget(refreshBtn); + buttons->addStretch(); + buttons->addWidget(clearBannedArtBtn); auto *layout = new QVBoxLayout; layout->addLayout(form); @@ -207,6 +215,8 @@ void TabCardArtRules::setupUi() connect(refreshBtn, &QPushButton::clicked, this, &TabCardArtRules::refresh); + connect(clearBannedArtBtn, &QPushButton::clicked, this, &TabCardArtRules::clearBlacklistedBanners); + retranslateUi(); } @@ -263,6 +273,7 @@ void TabCardArtRules::retranslateUi() addBtn->setText(tr("Add rule")); removeBtn->setText(tr("Remove rule")); refreshBtn->setText(tr("Refresh")); + clearBannedArtBtn->setText(tr("Clear blacklisted banners")); } void TabCardArtRules::refresh() @@ -299,4 +310,47 @@ void TabCardArtRules::removeSelected() client->sendCommand(client->prepareModeratorCommand(cmd)); refresh(); -} \ No newline at end of file +} + +void TabCardArtRules::clearBlacklistedBanners() +{ + if (!QtUtils::confirm(this, tr("Clear blacklisted banners"), + tr("Remove the banner card from every user whose current banner matches a DENY rule?\n\n" + "Users who are online keep showing their banner until they log in again."), + QMessageBox::No)) { + return; + } + + Command_ClearBlacklistedBanners cmd; + PendingCommand *pend = client->prepareModeratorCommand(cmd); + connect(pend, &PendingCommand::finished, this, &TabCardArtRules::clearBlacklistedBannersResponse); + client->sendCommand(pend); +} + +void TabCardArtRules::clearBlacklistedBannersResponse(const Response &r) +{ + switch (r.response_code()) { + case Response::RespOk: + break; + case Response::RespFunctionNotAllowed: + QMessageBox::warning(this, tr("Not supported"), + tr("This server does not support clearing blacklisted banners. It may be running an " + "older version.")); + return; + case Response::RespInternalError: + QMessageBox::critical(this, tr("Error"), + tr("The server could not clear blacklisted banners because of a database error.")); + return; + default: + QMessageBox::critical(this, tr("Error"), + tr("The server rejected the request: %1") + .arg(QString::fromStdString(Response_ResponseCode_Name(r.response_code())))); + return; + } + + const auto &resp = r.GetExtension(Response_ClearBlacklistedBanners::ext); + QMessageBox::information(this, tr("Banners cleared"), + tr("%n banner card(s) cleared.\n\n" + "Users who are online keep showing their banner until they log in again.", + "number of banners cleared", resp.cleared_count())); +} diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h index f34300179..1e0155ab7 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h @@ -78,6 +78,8 @@ private slots: void addRule(); void removeSelected(); void refresh(); + void clearBlacklistedBanners(); + void clearBlacklistedBannersResponse(const Response &r); private: AbstractClient *client; @@ -97,6 +99,7 @@ private: QPushButton *addBtn; QPushButton *removeBtn; QPushButton *refreshBtn; + QPushButton *clearBannedArtBtn; QTableView *table; CardArtRulesModel *tableModel; diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp index 09a640540..be1adeef8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp @@ -30,12 +30,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include @@ -85,12 +87,16 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli removeAvatarButton = new QPushButton; connect(removeAvatarButton, &QPushButton::clicked, this, &TabModeration::removeAvatar); + removeBannerButton = new QPushButton; + connect(removeBannerButton, &QPushButton::clicked, this, &TabModeration::removeBanner); + auto *topBar = new QHBoxLayout; topBar->addWidget(searchEdit); topBar->addWidget(investigateButton); topBar->addStretch(); topBar->addWidget(resetPasswordButton); topBar->addWidget(removeAvatarButton); + topBar->addWidget(removeBannerButton); userInfoGroup = new QGroupBox; userInfoNameLabel = new QLabel; @@ -186,6 +192,7 @@ void TabModeration::retranslateUi() investigateButton->setText(tr("Investigate")); resetPasswordButton->setText(tr("Reset Password")); removeAvatarButton->setText(tr("Remove Avatar")); + removeBannerButton->setText(tr("Remove Banner")); refreshStaffButton->setText(tr("Refresh")); userInfoGroup->setTitle(tr("User Info")); @@ -227,6 +234,7 @@ void TabModeration::clearUserData() sessionsTable->setRowCount(0); resetPasswordButton->setEnabled(false); removeAvatarButton->setEnabled(false); + removeBannerButton->setEnabled(false); } void TabModeration::investigate(const QString &userName) @@ -247,6 +255,7 @@ void TabModeration::investigateUser() currentUser = userName; resetPasswordButton->setEnabled(true); removeAvatarButton->setEnabled(true); + removeBannerButton->setEnabled(true); altsTable->setRowCount(0); sessionsTable->setRowCount(0); requestUserInfo(userName); @@ -414,13 +423,11 @@ void TabModeration::resetPassword() return; } - QMessageBox::StandardButton choice = - QMessageBox::warning(this, tr("Reset Password"), - tr("Reset the password of %1? A temporary password will be generated and shown to you. " - "The user must change it on their first login.") - .arg(currentUser), - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel); - if (choice != QMessageBox::Ok) { + if (!QtUtils::confirm(this, tr("Reset Password"), + tr("Reset the password of %1? A temporary password will be generated and shown to you. " + "The user must change it on their first login.") + .arg(currentUser), + QMessageBox::No)) { return; } @@ -451,11 +458,9 @@ void TabModeration::removeAvatar() return; } - QMessageBox::StandardButton choice = - QMessageBox::warning(this, tr("Remove Avatar"), - tr("Remove the avatar of %1? The user will have to upload a new one.").arg(currentUser), - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel); - if (choice != QMessageBox::Ok) { + if (!QtUtils::confirm(this, tr("Remove Avatar"), + tr("Remove the avatar of %1? The user will have to upload a new one.").arg(currentUser), + QMessageBox::No)) { return; } @@ -477,3 +482,67 @@ void TabModeration::removeAvatarResponse(const Response &response) QMessageBox::information(this, tr("Avatar removed"), tr("The avatar of %1 has been removed.").arg(QString::fromStdString(resp.user_name()))); } + +void TabModeration::removeBanner() +{ + if (currentUser.isEmpty()) { + return; + } + + if (!QtUtils::confirm(this, tr("Remove Banner"), + tr("Remove the banner card of %1? The user will have to choose a new one.\n\n" + "Other players stop seeing the banner the next time %1 logs in.") + .arg(currentUser), + QMessageBox::No)) { + return; + } + + Command_RemoveUserBanner cmd; + cmd.set_user_name(currentUser.toStdString()); + PendingCommand *pend = client->prepareModeratorCommand(cmd); + connect(pend, &PendingCommand::finished, this, &TabModeration::removeBannerResponse); + client->sendCommand(pend); +} + +void TabModeration::removeBannerResponse(const Response &response) +{ + const QString target = currentUser.isEmpty() ? tr("this user") : currentUser; + + switch (response.response_code()) { + case Response::RespOk: + break; + case Response::RespNameNotFound: + QMessageBox::warning(this, tr("User not found"), + tr("The server does not know a user named %1.").arg(target)); + return; + case Response::RespFunctionNotAllowed: + QMessageBox::warning(this, tr("Not supported"), + tr("This server does not support removing banner cards. It may be running an older " + "version.")); + return; + case Response::RespInternalError: + QMessageBox::critical( + this, tr("Error"), + tr("The server could not clear the banner card of %1 because of a database error.").arg(target)); + return; + default: + QMessageBox::critical( + this, tr("Error"), + tr("The server rejected the request for %1: %2") + .arg(target, QString::fromStdString(Response_ResponseCode_Name(response.response_code())))); + return; + } + + const Response_RemoveUserBanner &resp = response.GetExtension(Response_RemoveUserBanner::ext); + const QString removedUser = resp.user_name().empty() ? target : QString::fromStdString(resp.user_name()); + if (!resp.had_banner()) { + QMessageBox::information(this, tr("No banner set"), + tr("%1 has no banner card set, so there was nothing to remove.").arg(removedUser)); + return; + } + + QMessageBox::information(this, tr("Banner removed"), + tr("The banner card of %1 has been removed.\n\n" + "Other players stop seeing it the next time %1 logs in.") + .arg(removedUser)); +} diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.h b/cockatrice/src/interface/widgets/tabs/tab_moderation.h index e352e90bb..ce23b5a7a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.h +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.h @@ -20,7 +20,7 @@ class TabSupervisor; /** * Staff investigation tool. Lets moderators look up a user's account data, * alternate accounts, login sessions, and staff login activity, and offers - * the password-reset and remove-avatar actions. + * the password-reset, remove-avatar, and remove-banner actions. */ class TabModeration : public Tab { @@ -44,6 +44,8 @@ private slots: void resetPasswordResponse(const Response &response); void removeAvatar(); void removeAvatarResponse(const Response &response); + void removeBanner(); + void removeBannerResponse(const Response &response); private: void requestUserInfo(const QString &userName); @@ -60,6 +62,7 @@ private: QPushButton *investigateButton; QPushButton *resetPasswordButton; QPushButton *removeAvatarButton; + QPushButton *removeBannerButton; QGroupBox *userInfoGroup; QLabel *userInfoNameLabel; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index bd0b5bda2..059e83096 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -174,6 +174,8 @@ set(PROTO_FILES response_report_user_info.proto response_moderator_last_logins.proto response_remove_user_avatar.proto + response_remove_user_banner.proto + response_clear_blacklisted_banners.proto response_reset_user_password.proto response_user_alts.proto response_user_sessions.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index 5298d3dad..292917fa9 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -20,6 +20,8 @@ message ModeratorCommand { GET_MODERATOR_LAST_LOGINS = 1015; RESET_USER_PASSWORD = 1016; REMOVE_USER_AVATAR = 1017; + REMOVE_USER_BANNER = 1018; + CLEAR_BLACKLISTED_BANNERS = 1019; REPORT_LIST = 1200; REPORT_ASSIGN = 1201; REPORT_RESOLVE = 1202; @@ -179,3 +181,16 @@ message Command_RemoveUserAvatar { } optional string user_name = 1; } + +message Command_RemoveUserBanner { + extend ModeratorCommand { + optional Command_RemoveUserBanner ext = 1018; + } + optional string user_name = 1; +} + +message Command_ClearBlacklistedBanners { + extend ModeratorCommand { + optional Command_ClearBlacklistedBanners ext = 1019; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_clear_blacklisted_banners.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_clear_blacklisted_banners.proto new file mode 100644 index 000000000..86cbc92f3 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_clear_blacklisted_banners.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_ClearBlacklistedBanners { + extend Response { + optional Response_ClearBlacklistedBanners ext = 1222; + } + optional uint32 cleared_count = 1 [default = 0]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_banner.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_banner.proto new file mode 100644 index 000000000..bfad7333c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_banner.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_RemoveUserBanner { + extend Response { + optional Response_RemoveUserBanner ext = 1221; + } + optional string user_name = 1; + // false when the user exists but had no banner card set, so nothing was cleared + optional bool had_banner = 2 [default = true]; +} diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 04f6047fa..b5c45065c 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -2077,6 +2077,108 @@ bool Servatrice_DatabaseInterface::removeUserAvatar(const QString &userName) return query->numRowsAffected() > 0; } +Servatrice_DatabaseInterface::RemoveUserBannerResult +Servatrice_DatabaseInterface::removeUserBanner(const QString &userName) +{ + if (!checkSql()) { + return RemoveUserBannerResult::DatabaseError; + } + + // The UPDATE alone cannot tell a missing user from a user without a banner, + // so read the row first and let the moderator know which case they hit. + QSqlQuery *lookupQuery = prepareQuery("SELECT card_art_params FROM {prefix}_users WHERE name = :user_name"); + lookupQuery->bindValue(":user_name", userName); + if (!execSqlQuery(lookupQuery)) { + return RemoveUserBannerResult::DatabaseError; + } + if (!lookupQuery->next()) { + return RemoveUserBannerResult::NoSuchUser; + } + if (lookupQuery->value(0).toString().isEmpty()) { + return RemoveUserBannerResult::NothingToRemove; + } + + QSqlQuery *query = prepareQuery("UPDATE {prefix}_users SET card_art_params = NULL WHERE name = :user_name"); + query->bindValue(":user_name", userName); + if (!execSqlQuery(query)) { + return RemoveUserBannerResult::DatabaseError; + } + return RemoveUserBannerResult::Removed; +} + +// isCardArtAllowed() compares against the rule table with the database's +// case-insensitive collation, so the sweep has to fold case the same way or it +// leaves behind banners the server already treats as blacklisted. +static QPair bannerKey(const QString &cardName, const QString &cardProviderId) +{ + return {cardName.toCaseFolded(), cardProviderId.toCaseFolded()}; +} + +int Servatrice_DatabaseInterface::clearBlacklistedBanners() +{ + if (!checkSql()) { + return -1; + } + + QSqlQuery *ruleQuery = prepareQuery("SELECT card_name, card_provider_id FROM {prefix}_card_art_name_rules " + "WHERE mode = 'DENY'"); + if (!execSqlQuery(ruleQuery)) { + return -1; + } + + QSet> denied; + while (ruleQuery->next()) { + denied.insert(bannerKey(ruleQuery->value(0).toString(), ruleQuery->value(1).toString())); + } + if (denied.isEmpty()) { + return 0; + } + + QSqlQuery *userQuery = + prepareQuery("SELECT id, card_art_params FROM {prefix}_users WHERE card_art_params IS NOT NULL"); + if (!execSqlQuery(userQuery)) { + return -1; + } + + QList matchingIds; + while (userQuery->next()) { + const QJsonDocument doc = QJsonDocument::fromJson(userQuery->value(1).toString().toUtf8()); + if (!doc.isObject()) { + continue; + } + const QJsonObject obj = doc.object(); + if (denied.contains(bannerKey(obj.value("card_name").toString(), obj.value("card_provider_id").toString()))) { + matchingIds.append(userQuery->value(0)); + } + } + + if (matchingIds.isEmpty()) { + return 0; + } + + // Chunk the UPDATE so very large user tables do not exceed driver placeholder limits. + int cleared = 0; + constexpr int BATCH_SIZE = 500; + for (int i = 0; i < matchingIds.size(); i += BATCH_SIZE) { + const QList batch = matchingIds.mid(i, BATCH_SIZE); + QStringList placeholders; + placeholders.reserve(batch.size()); + for (int j = 0; j < batch.size(); ++j) { + placeholders.append(QString(":id%1").arg(j)); + } + QSqlQuery *clearQuery = prepareQuery( + QString("UPDATE {prefix}_users SET card_art_params = NULL WHERE id IN (%1)").arg(placeholders.join(","))); + for (int j = 0; j < batch.size(); ++j) { + clearQuery->bindValue(placeholders.at(j), batch.at(j)); + } + if (!execSqlQuery(clearQuery)) { + return -1; + } + cleared += clearQuery->numRowsAffected(); + } + return cleared; +} + QList Servatrice_DatabaseInterface::getMessageLogHistory(const QString &user, const QString &ipaddress, const QString &gamename, diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 822accada..6b38384e4 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -231,6 +231,18 @@ public: }; UptimeSnapshot getLatestUptimeSnapshot(int serverId); bool removeUserAvatar(const QString &userName); + + // Outcome of removeUserBanner(), so the caller can tell a missing user apart + // from a user who simply has no banner card set. + enum class RemoveUserBannerResult + { + Removed, + NothingToRemove, + NoSuchUser, + DatabaseError, + }; + RemoveUserBannerResult removeUserBanner(const QString &userName); + int clearBlacklistedBanners(); bool isCardArtAllowed(const QString &cardName, const QString &cardProviderId) override; bool addForgotPassword(const QString &user); bool removeForgotPassword(const QString &user) override; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index fc911950e..2cb4df8ba 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -126,6 +126,7 @@ #include #include #include +#include #include #include #include @@ -140,6 +141,7 @@ #include #include #include +#include #include #include #include @@ -484,6 +486,10 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo return cmdGetModeratorLastLogins(cmd.GetExtension(Command_GetModeratorLastLogins::ext), rc); case ModeratorCommand::REMOVE_USER_AVATAR: return cmdRemoveUserAvatar(cmd.GetExtension(Command_RemoveUserAvatar::ext), rc); + case ModeratorCommand::REMOVE_USER_BANNER: + return cmdRemoveUserBanner(cmd.GetExtension(Command_RemoveUserBanner::ext), rc); + case ModeratorCommand::CLEAR_BLACKLISTED_BANNERS: + return cmdClearBlacklistedBanners(cmd.GetExtension(Command_ClearBlacklistedBanners::ext), rc); default: return Response::RespFunctionNotAllowed; } @@ -2691,6 +2697,67 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRemoveUserAvatar(const return Response::RespOk; } +Response::ResponseCode AbstractServerSocketInterface::cmdRemoveUserBanner(const Command_RemoveUserBanner &cmd, + ResponseContainer &rc) +{ + if (!sqlInterface->checkSql()) { + return Response::RespInternalError; + } + + const QString userName = nameFromStdString(cmd.user_name()).simplified(); + if (userName.isEmpty()) { + return Response::RespContextError; + } + + bool hadBanner = true; + switch (sqlInterface->removeUserBanner(userName)) { + case Servatrice_DatabaseInterface::RemoveUserBannerResult::Removed: + break; + case Servatrice_DatabaseInterface::RemoveUserBannerResult::NothingToRemove: + hadBanner = false; + break; + case Servatrice_DatabaseInterface::RemoveUserBannerResult::NoSuchUser: + return Response::RespNameNotFound; + case Servatrice_DatabaseInterface::RemoveUserBannerResult::DatabaseError: + default: + return Response::RespInternalError; + } + + if (hadBanner) { + sqlInterface->addAuditRecord(userName, this->getAddress(), QString::fromStdString(userInfo->clientid()), + "REMOVE_USER_BANNER", "Moderator removed user banner card", true); + } + + Response_RemoveUserBanner *re = new Response_RemoveUserBanner; + re->set_user_name(userName.toStdString()); + re->set_had_banner(hadBanner); + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode +AbstractServerSocketInterface::cmdClearBlacklistedBanners(const Command_ClearBlacklistedBanners & /* cmd */, + ResponseContainer &rc) +{ + if (!sqlInterface->checkSql()) { + return Response::RespInternalError; + } + + const int cleared = sqlInterface->clearBlacklistedBanners(); + if (cleared < 0) { + return Response::RespInternalError; + } + + sqlInterface->addAuditRecord(QString::fromStdString(userInfo->name()), this->getAddress(), + QString::fromStdString(userInfo->clientid()), "CLEAR_BLACKLISTED_BANNERS", + QString("Moderator cleared %1 blacklisted banner(s)").arg(cleared), true); + + Response_ClearBlacklistedBanners *re = new Response_ClearBlacklistedBanners; + re->set_cleared_count(static_cast(cleared)); + rc.setResponseExtension(re); + return Response::RespOk; +} + void AbstractServerSocketInterface::sendPendingReportNotifications(ResponseContainer &rc) { if (!sqlInterface->checkSql()) { diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 71854513f..cd37286c6 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -73,6 +73,7 @@ class Command_AccountPassword; class Command_Activate; class Command_AddCardArtRule; class Command_AdjustMod; +class Command_ClearBlacklistedBanners; class Command_ForceActivateUser; class Command_ForgotPasswordChallenge; class Command_ForgotPasswordRequest; @@ -90,6 +91,7 @@ class Command_ListCardArtRules; class Command_Register; class Command_RemoveCardArtRule; class Command_RemoveUserAvatar; +class Command_RemoveUserBanner; class Command_Report; class Command_ReportAddComment; class Command_ReportAssign; @@ -236,6 +238,9 @@ private: Response::ResponseCode cmdGetModeratorLastLogins(const Command_GetModeratorLastLogins &cmd, ResponseContainer &rc); Response::ResponseCode cmdResetUserPassword(const Command_ResetUserPassword &cmd, ResponseContainer &rc); Response::ResponseCode cmdRemoveUserAvatar(const Command_RemoveUserAvatar &cmd, ResponseContainer &rc); + Response::ResponseCode cmdRemoveUserBanner(const Command_RemoveUserBanner &cmd, ResponseContainer &rc); + Response::ResponseCode cmdClearBlacklistedBanners(const Command_ClearBlacklistedBanners &cmd, + ResponseContainer &rc); Response::ResponseCode cmdGetServerStats(const Command_GetServerStats &cmd, ResponseContainer &rc);