[Server/Client/Protocol] Let moderators clear banner cards (#7546)

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-10-25 13:19:20 +01:00 • committed by GitHub
parent b4e24f958a
commit 93309e01a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 366 additions and 14 deletions

View file

@ -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<QString, QString> 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<QPair<QString, QString>> 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<QVariant> 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<QVariant> 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<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory(const QString &user,
const QString &ipaddress,
const QString &gamename,