From a8810ad0164615787807f547124f7b2a8cbb556f Mon Sep 17 00:00:00 2001
From: Marcel <MTRNord@users.noreply.github.com>
Date: Sun, 10 Apr 2022 22:44:15 +0200
Subject: [PATCH] Add specific powerlevel messages (#852)

fixes #136
---
 src/timeline/TimelineModel.cpp | 265 ++++++++++++++++++++++++++++++++-
 1 file changed, 261 insertions(+), 4 deletions(-)

diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 3dd51112e..070d226f2 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -1934,11 +1934,268 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
     if (!event)
         return QString();
 
-    QString user = QString::fromStdString(event->sender);
-    QString name = utils::replaceEmoji(displayName(user));
+    mtx::events::StateEvent<mtx::events::state::PowerLevels> *prevEvent = nullptr;
+    if (!event->unsigned_data.replaces_state.empty()) {
+        auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id);
+        if (tempPrevEvent) {
+            prevEvent =
+              std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(tempPrevEvent);
+        }
+    }
 
-    // TODO: power levels rendering is actually a bit complex. work on this later.
-    return tr("%1 has changed the room's permissions.").arg(name);
+    QString user        = QString::fromStdString(event->sender);
+    QString sender_name = utils::replaceEmoji(displayName(user));
+    // Get the rooms levels for redactions and powerlevel changes to determine "Administrator" and
+    // "Moderator" powerlevels.
+    auto administrator_power_level = event->content.state_level("m.room.power_levels");
+    auto moderator_power_level     = event->content.redact;
+    auto default_powerlevel        = event->content.users_default;
+    if (!prevEvent)
+        return tr("%1 has changed the room's permissions.").arg(sender_name);
+
+    auto calc_affected = [&event,
+                          &prevEvent](int64_t newPowerlevelSetting) -> std::pair<QStringList, int> {
+        QStringList affected{};
+        auto numberOfAffected = 0;
+        // We do only compare to people with explicit PL. Usually others are not going to be
+        // affected either way and this is cheaper to iterate over.
+        for (auto const &[mxid, currentPowerlevel] : event->content.users) {
+            if (currentPowerlevel == newPowerlevelSetting &&
+                prevEvent->content.user_level(mxid) < newPowerlevelSetting) {
+                numberOfAffected++;
+                if (numberOfAffected <= 2) {
+                    affected.push_back(QString::fromStdString(mxid));
+                }
+            }
+        }
+        return {affected, numberOfAffected};
+    };
+
+    QStringList resultingMessage{};
+    // These affect only a few people. Therefor we can print who is affected.
+    if (event->content.kick != prevEvent->content.kick) {
+        auto default_message = tr("%1 has changed the room's kick powerlevel from %2 to %3.")
+                                 .arg(sender_name)
+                                 .arg(prevEvent->content.kick)
+                                 .arg(event->content.kick);
+
+        // We only calculate affected users if we change to a level above the default users PL
+        // to not accidentally have a DoS vector
+        if (event->content.kick > default_powerlevel) {
+            auto [affected, number_of_affected] = calc_affected(event->content.kick);
+
+            if (number_of_affected != 0) {
+                auto true_affected_rest = number_of_affected - affected.size();
+                if (number_of_affected > 1) {
+                    resultingMessage.append(
+                      default_message + QStringLiteral(" ") +
+                      tr("%n member(s) can now kick room members.", nullptr, true_affected_rest));
+                } else if (number_of_affected == 1) {
+                    resultingMessage.append(
+                      default_message + QStringLiteral(" ") +
+                      tr("%1 can now kick room members.")
+                        .arg(utils::replaceEmoji(displayName(affected.at(0)))));
+                }
+            } else {
+                resultingMessage.append(default_message);
+            }
+        } else {
+            resultingMessage.append(default_message);
+        }
+    }
+
+    if (event->content.redact != prevEvent->content.redact) {
+        auto default_message = tr("%1 has changed the room's redact powerlevel from %2 to %3.")
+                                 .arg(sender_name)
+                                 .arg(prevEvent->content.redact)
+                                 .arg(event->content.redact);
+
+        // We only calculate affected users if we change to a level above the default users PL
+        // to not accidentally have a DoS vector
+        if (event->content.redact > default_powerlevel) {
+            auto [affected, number_of_affected] = calc_affected(event->content.redact);
+
+            if (number_of_affected != 0) {
+                auto true_affected_rest = number_of_affected - affected.size();
+                if (number_of_affected > 1) {
+                    resultingMessage.append(
+                      default_message + QStringLiteral(" ") +
+                      tr("%n member(s) can now redact room messages.", nullptr, true_affected_rest));
+                } else if (number_of_affected == 1) {
+                    resultingMessage.append(
+                      default_message + QStringLiteral(" ") +
+                      tr("%1 can now redact room messages.")
+                        .arg(utils::replaceEmoji(displayName(affected.at(0)))));
+                }
+            } else {
+                resultingMessage.append(default_message);
+            }
+        } else {
+            resultingMessage.append(default_message);
+        }
+    }
+
+    if (event->content.ban != prevEvent->content.ban) {
+        auto default_message = tr("%1 has changed the room's ban powerlevel from %2 to %3.")
+                                 .arg(sender_name)
+                                 .arg(prevEvent->content.ban)
+                                 .arg(event->content.ban);
+
+        // We only calculate affected users if we change to a level above the default users PL
+        // to not accidentally have a DoS vector
+        if (event->content.ban > default_powerlevel) {
+            auto [affected, number_of_affected] = calc_affected(event->content.ban);
+
+            if (number_of_affected != 0) {
+                auto true_affected_rest = number_of_affected - affected.size();
+                if (number_of_affected > 1) {
+                    resultingMessage.append(
+                      default_message + QStringLiteral(" ") +
+                      tr("%n member(s) can now ban room members.", nullptr, true_affected_rest));
+                } else if (number_of_affected == 1) {
+                    resultingMessage.append(
+                      default_message + QStringLiteral(" ") +
+                      tr("%1 can now ban room members.")
+                        .arg(utils::replaceEmoji(displayName(affected.at(0)))));
+                }
+            } else {
+                resultingMessage.append(default_message);
+            }
+        } else {
+            resultingMessage.append(default_message);
+        }
+    }
+
+    if (event->content.state_default != prevEvent->content.state_default) {
+        auto default_message =
+          tr("%1 has changed the room's state_default powerlevel from %2 to %3.")
+            .arg(sender_name)
+            .arg(prevEvent->content.state_default)
+            .arg(event->content.state_default);
+
+        // We only calculate affected users if we change to a level above the default users PL
+        // to not accidentally have a DoS vector
+        if (event->content.state_default > default_powerlevel) {
+            auto [affected, number_of_affected] = calc_affected(event->content.kick);
+
+            if (number_of_affected != 0) {
+                auto true_affected_rest = number_of_affected - affected.size();
+                if (number_of_affected > 1) {
+                    resultingMessage.append(
+                      default_message + QStringLiteral(" ") +
+                      tr("%n member(s) can now send state events.", nullptr, true_affected_rest));
+                } else if (number_of_affected == 1) {
+                    resultingMessage.append(
+                      default_message + QStringLiteral(" ") +
+                      tr("%1 can now send state events.")
+                        .arg(utils::replaceEmoji(displayName(affected.at(0)))));
+                }
+            } else {
+                resultingMessage.append(default_message);
+            }
+        } else {
+            resultingMessage.append(default_message);
+        }
+    }
+
+    // These affect potentially the whole room. We there for do not calculate who gets affected
+    // by this to prevent huge lists of people.
+    if (event->content.invite != prevEvent->content.invite) {
+        resultingMessage.append(tr("%1 has changed the room's invite powerlevel from %2 to %3.")
+                                  .arg(sender_name,
+                                       QString::number(prevEvent->content.invite),
+                                       QString::number(event->content.invite)));
+    }
+
+    if (event->content.events_default != prevEvent->content.events_default) {
+        if ((event->content.events_default > default_powerlevel) &&
+            prevEvent->content.events_default <= default_powerlevel) {
+            resultingMessage.append(
+              tr("%1 has changed the room's events_default powerlevel from %2 to %3. New "
+                 "users can now not send any events.")
+                .arg(sender_name,
+                     QString::number(prevEvent->content.events_default),
+                     QString::number(event->content.events_default)));
+        } else if ((event->content.events_default < prevEvent->content.events_default) &&
+                   (event->content.events_default < default_powerlevel) &&
+                   (prevEvent->content.events_default > default_powerlevel)) {
+            resultingMessage.append(
+              tr("%1 has changed the room's events_default powerlevel from %2 to %3. New "
+                 "users can now send events that are not otherwise restricted.")
+                .arg(sender_name,
+                     QString::number(prevEvent->content.events_default),
+                     QString::number(event->content.events_default)));
+        } else {
+            resultingMessage.append(
+              tr("%1 has changed the room's events_default powerlevel from %2 to %3.")
+                .arg(sender_name,
+                     QString::number(prevEvent->content.events_default),
+                     QString::number(event->content.events_default)));
+        }
+    }
+
+    // Compare if a Powerlevel of a user changed
+    for (auto const &[mxid, powerlevel] : event->content.users) {
+        auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid)));
+        if (prevEvent->content.user_level(mxid) != powerlevel) {
+            if (powerlevel >= administrator_power_level) {
+                resultingMessage.append(tr("%1 has made %2 an administrator of this room.")
+                                          .arg(sender_name, nameOfChangedUser));
+            } else if (powerlevel >= moderator_power_level &&
+                       powerlevel > prevEvent->content.user_level(mxid)) {
+                resultingMessage.append(tr("%1 has made %2 a moderator of this room.")
+                                          .arg(sender_name, nameOfChangedUser));
+            } else if (powerlevel >= moderator_power_level &&
+                       powerlevel < prevEvent->content.user_level(mxid)) {
+                resultingMessage.append(tr("%1 has downgraded %2 to moderator of this room.")
+                                          .arg(sender_name, nameOfChangedUser));
+            } else {
+                resultingMessage.append(tr("%1 has changed the powerlevel of %2 from %3 to %4.")
+                                          .arg(sender_name,
+                                               nameOfChangedUser,
+                                               QString::number(prevEvent->content.user_level(mxid)),
+                                               QString::number(powerlevel)));
+            }
+        }
+    }
+
+    // Handle added/removed/changed event type
+    for (auto const &[event_type, powerlevel] : event->content.events) {
+        auto prev_not_present =
+          prevEvent->content.events.find(event_type) == prevEvent->content.events.end();
+
+        if (prev_not_present || prevEvent->content.events.at(event_type) != powerlevel) {
+            if (powerlevel >= administrator_power_level) {
+                resultingMessage.append(tr("%1 allowed only administrators to send \"%2\".")
+                                          .arg(sender_name, QString::fromStdString(event_type)));
+            } else if (powerlevel >= moderator_power_level) {
+                resultingMessage.append(tr("%1 allowed only moderators to send \"%2\".")
+                                          .arg(sender_name, QString::fromStdString(event_type)));
+            } else if (powerlevel == default_powerlevel) {
+                resultingMessage.append(tr("%1 allowed everyone to send \"%2\".")
+                                          .arg(sender_name, QString::fromStdString(event_type)));
+            } else if (prev_not_present) {
+                resultingMessage.append(
+                  tr("%1 has changed the powerlevel of event type \"%2\" from the default to %3.")
+                    .arg(sender_name,
+                         QString::fromStdString(event_type),
+                         QString::number(powerlevel)));
+            } else {
+                resultingMessage.append(
+                  tr("%1 has changed the powerlevel of event type \"%2\" from %3 to %4.")
+                    .arg(sender_name,
+                         QString::fromStdString(event_type),
+                         QString::number(prevEvent->content.events.at(event_type)),
+                         QString::number(powerlevel)));
+            }
+        }
+    }
+
+    if (!resultingMessage.isEmpty()) {
+        return resultingMessage.join("<br/>");
+    } else {
+        return tr("%1 has changed the room's permissions.").arg(sender_name);
+    }
 }
 
 QVariantMap
-- 
GitLab