summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2023-07-05 00:08:37 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2023-07-05 00:08:37 +0200
commitad6e4fef6407b9e39ab8ee329b4e8c12376c8494 (patch)
treebfa2634aabea625a1479817700c35d846273371b /src
parentAdd some event expiration function (diff)
downloadnheko-ad6e4fef6407b9e39ab8ee329b4e8c12376c8494.tar.xz
Add experimental event expiration
Currently disabled by default.
Diffstat (limited to 'src')
-rw-r--r--src/ChatPage.cpp1
-rw-r--r--src/UserSettingsPage.cpp33
-rw-r--r--src/UserSettingsPage.h6
-rw-r--r--src/Utils.cpp84
-rw-r--r--src/ui/EventExpiry.cpp124
-rw-r--r--src/ui/EventExpiry.h67
6 files changed, 285 insertions, 30 deletions
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index c305a54a..4686b0f5 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -87,6 +87,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent)
               if (lastSpacesUpdate < QDateTime::currentDateTime().addSecs(-20 * 60)) {
                   lastSpacesUpdate = QDateTime::currentDateTime();
                   utils::updateSpaceVias();
+                  utils::removeExpiredEvents();
               }
 
               if (!isConnected_)
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index ea7f22c4..7c30f877 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -101,6 +101,8 @@ UserSettings::load(std::optional<QString> profile)
     exposeDBusApi_ = settings.value(QStringLiteral("user/expose_dbus_api"), false).toBool();
     updateSpaceVias_ =
       settings.value(QStringLiteral("user/space_background_maintenance"), true).toBool();
+    expireEvents_ =
+      settings.value(QStringLiteral("user/expired_events_background_maintenance"), false).toBool();
 
     mobileMode_ = settings.value(QStringLiteral("user/mobile_mode"), false).toBool();
     emojiFont_  = settings.value(QStringLiteral("user/emoji_font_family"), "emoji").toString();
@@ -309,6 +311,17 @@ UserSettings::setUpdateSpaceVias(bool state)
 }
 
 void
+UserSettings::setExpireEvents(bool state)
+{
+    if (expireEvents_ == state)
+        return;
+
+    expireEvents_ = state;
+    emit expireEventsChanged(state);
+    save();
+}
+
+void
 UserSettings::setMarkdown(bool state)
 {
     if (state == markdown_)
@@ -924,6 +937,7 @@ UserSettings::save()
     settings.setValue(QStringLiteral("open_video_external"), openVideoExternal_);
     settings.setValue(QStringLiteral("expose_dbus_api"), exposeDBusApi_);
     settings.setValue(QStringLiteral("space_background_maintenance"), updateSpaceVias_);
+    settings.setValue(QStringLiteral("expired_events_background_maintenance"), expireEvents_);
 
     settings.endGroup(); // user
 
@@ -1129,6 +1143,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return tr("Expose room information via D-Bus");
         case UpdateSpaceVias:
             return tr("Periodically update community routing information");
+        case ExpireEvents:
+            return tr("Periodically delete expired events");
         }
     } else if (role == Value) {
         switch (index.row()) {
@@ -1266,6 +1282,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return i->exposeDBusApi();
         case UpdateSpaceVias:
             return i->updateSpaceVias();
+        case ExpireEvents:
+            return i->expireEvents();
         }
     } else if (role == Description) {
         switch (index.row()) {
@@ -1449,6 +1467,10 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
               "information about what servers participate in a room to community members. Since "
               "the room participants can change over time, this needs to be updated from time to "
               "time. This setting enables a background job to do that automatically.");
+        case ExpireEvents:
+            return tr("Regularly redact expired events as specified in the event expiration "
+                      "configuration. Since this is currently not executed server side, you need "
+                      "to have one client running this regularly.");
         }
     } else if (role == Type) {
         switch (index.row()) {
@@ -1499,6 +1521,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
         case UseOnlineKeyBackup:
         case ExposeDBusApi:
         case UpdateSpaceVias:
+        case ExpireEvents:
         case SpaceNotifications:
         case FancyEffects:
         case ReducedMotion:
@@ -1994,6 +2017,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
             } else
                 return false;
         }
+        case ExpireEvents: {
+            if (value.userType() == QMetaType::Bool) {
+                i->setExpireEvents(value.toBool());
+                return true;
+            } else
+                return false;
+        }
         }
     }
     return false;
@@ -2249,4 +2279,7 @@ UserSettingsModel::UserSettingsModel(QObject *p)
     connect(s.get(), &UserSettings::updateSpaceViasChanged, this, [this] {
         emit dataChanged(index(UpdateSpaceVias), index(UpdateSpaceVias), {Value});
     });
+    connect(s.get(), &UserSettings::expireEventsChanged, this, [this] {
+        emit dataChanged(index(ExpireEvents), index(ExpireEvents), {Value});
+    });
 }
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 34dae2ea..4e2691e5 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -128,6 +128,7 @@ class UserSettings final : public QObject
       bool exposeDBusApi READ exposeDBusApi WRITE setExposeDBusApi NOTIFY exposeDBusApiChanged)
     Q_PROPERTY(bool updateSpaceVias READ updateSpaceVias WRITE setUpdateSpaceVias NOTIFY
                  updateSpaceViasChanged)
+    Q_PROPERTY(bool expireEvents READ expireEvents WRITE setExpireEvents NOTIFY expireEventsChanged)
 
     UserSettings();
 
@@ -233,6 +234,7 @@ public:
     void setCollapsedSpaces(QList<QStringList> spaces);
     void setExposeDBusApi(bool state);
     void setUpdateSpaceVias(bool state);
+    void setExpireEvents(bool state);
 
     QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
     bool messageHoverHighlight() const { return messageHoverHighlight_; }
@@ -308,6 +310,7 @@ public:
     QList<QStringList> collapsedSpaces() const { return collapsedSpaces_; }
     bool exposeDBusApi() const { return exposeDBusApi_; }
     bool updateSpaceVias() const { return updateSpaceVias_; }
+    bool expireEvents() const { return expireEvents_; }
 
 signals:
     void groupViewStateChanged(bool state);
@@ -372,6 +375,7 @@ signals:
     void recentReactionsChanged();
     void exposeDBusApiChanged(bool state);
     void updateSpaceViasChanged(bool state);
+    void expireEventsChanged(bool state);
 
 private:
     // Default to system theme if QT_QPA_PLATFORMTHEME var is set.
@@ -446,6 +450,7 @@ private:
     bool openVideoExternal_;
     bool exposeDBusApi_;
     bool updateSpaceVias_;
+    bool expireEvents_;
 
     QSettings settings;
 
@@ -478,6 +483,7 @@ class UserSettingsModel : public QAbstractListModel
         ExposeDBusApi,
 #endif
         UpdateSpaceVias,
+        ExpireEvents,
 
         AccessibilitySection,
         ReducedMotion,
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 7a412db0..663609fe 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -1610,8 +1610,7 @@ std::atomic<bool> event_expiration_running = false;
 void
 utils::removeExpiredEvents()
 {
-    // TODO(Nico): Add its own toggle...
-    if (!UserSettings::instance()->updateSpaceVias())
+    if (!UserSettings::instance()->expireEvents())
         return;
 
     if (event_expiration_running.exchange(true)) {
@@ -1645,18 +1644,20 @@ utils::removeExpiredEvents()
         std::string currentRoom;
         std::uint64_t currentRoomCount = 0;
         std::string currentRoomPrevToken;
+        std::set<std::pair<std::string, std::string>> currentRoomStateEvents;
         std::vector<std::string> currentRoomRedactionQueue;
         mtx::events::account_data::nheko_extensions::EventExpiry currentExpiry;
 
         static void next(std::shared_ptr<ApplyEventExpiration> state)
         {
             if (!state->currentRoomRedactionQueue.empty()) {
+                auto evid = state->currentRoomRedactionQueue.back();
+                auto room = state->currentRoom;
                 http::client()->redact_event(
-                  state->currentRoom,
-                  state->currentRoomRedactionQueue.back(),
-                  [state = std::move(state)](const mtx::responses::EventId &,
-                                             mtx::http::RequestErr e) mutable {
-                      const auto &event_id = state->currentRoomRedactionQueue.back();
+                  room,
+                  evid,
+                  [state = std::move(state), evid](const mtx::responses::EventId &,
+                                                   mtx::http::RequestErr e) mutable {
                       if (e) {
                           if (e->status_code == 429 && e->matrix_error.retry_after.count() != 0) {
                               ChatPage::instance()->callFunctionOnGuiThread(
@@ -1669,17 +1670,19 @@ utils::removeExpiredEvents()
                                                        });
                                 });
                               return;
+                          } else {
+                              nhlog::net()->error("Failed to redact event {} in {}: {}",
+                                                  evid,
+                                                  state->currentRoom,
+                                                  *e);
+                              state->currentRoomRedactionQueue.pop_back();
+                              next(std::move(state));
                           }
-
-                          nhlog::net()->error("Failed to redact event {} in {}: {}",
-                                              event_id,
-                                              state->currentRoom,
-                                              *e);
+                      } else {
+                          nhlog::net()->info("Redacted event {} in {}", evid, state->currentRoom);
+                          state->currentRoomRedactionQueue.pop_back();
+                          next(std::move(state));
                       }
-                      nhlog::net()->info(
-                        "Redacted event {} in {}: {}", event_id, state->currentRoom, *e);
-                      state->currentRoomRedactionQueue.pop_back();
-                      next(std::move(state));
                   });
             } else if (!state->currentRoom.empty()) {
                 mtx::http::MessagesOpts opts{};
@@ -1687,6 +1690,7 @@ utils::removeExpiredEvents()
                 opts.from  = state->currentRoomPrevToken;
                 opts.limit = 1000;
                 opts.filter = state->filter;
+                opts.room_id = state->currentRoom;
 
                 http::client()->messages(
                   opts,
@@ -1708,6 +1712,19 @@ utils::removeExpiredEvents()
                                     mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e))
                                   continue;
 
+                              if (std::holds_alternative<
+                                    mtx::events::RoomEvent<mtx::events::msg::Redacted>>(e))
+                                  continue;
+
+                              if (std::holds_alternative<
+                                    mtx::events::StateEvent<mtx::events::msg::Redacted>>(e))
+                                  continue;
+
+                              // skip events we don't know to protect us from mistakes.
+                              if (std::holds_alternative<
+                                    mtx::events::RoomEvent<mtx::events::Unknown>>(e))
+                                  continue;
+
                               if (mtx::accessors::sender(e) != us)
                                   continue;
 
@@ -1720,6 +1737,21 @@ utils::removeExpiredEvents()
                                   mtx::accessors::is_state_event(e))
                                   continue;
 
+                              if (mtx::accessors::is_state_event(e)) {
+                                  // skip the first state event of a type
+                                  if (std::visit(
+                                        [&state](const auto &se) {
+                                            if constexpr (requires { se.state_key; })
+                                                return state->currentRoomStateEvents
+                                                  .emplace(to_string(se.type), se.state_key)
+                                                  .second;
+                                            else
+                                                return false;
+                                        },
+                                        e))
+                                      continue;
+                              }
+
                               if (state->currentExpiry.keep_only_latest &&
                                   state->currentRoomCount > state->currentExpiry.keep_only_latest) {
                                   state->currentRoomRedactionQueue.push_back(
@@ -1738,6 +1770,7 @@ utils::removeExpiredEvents()
                           state->currentRoom.clear();
                           state->currentRoomCount = 0;
                           state->currentRoomPrevToken.clear();
+                          state->currentRoomStateEvents.clear();
                       }
 
                       next(std::move(state));
@@ -1764,20 +1797,11 @@ utils::removeExpiredEvents()
 
     auto asus = std::make_shared<ApplyEventExpiration>();
 
-    asus->filter =
-      nlohmann::json{
-        "room",
-        nlohmann::json::object({
-          {
-            "timeline",
-            nlohmann::json::object({
-              {"senders", nlohmann::json::array({us})},
-              {"not_types", nlohmann::json::array({"m.room.redaction"})},
-            }),
-          },
-        }),
-      }
-        .dump();
+    nlohmann::json filter;
+    filter["timeline"]["senders"]   = nlohmann::json::array({us});
+    filter["timeline"]["not_types"] = nlohmann::json::array({"m.room.redaction"});
+
+    asus->filter = filter.dump();
 
     asus->globalExpiry = getExpEv();
 
diff --git a/src/ui/EventExpiry.cpp b/src/ui/EventExpiry.cpp
new file mode 100644
index 00000000..ca149dc3
--- /dev/null
+++ b/src/ui/EventExpiry.cpp
@@ -0,0 +1,124 @@
+// SPDX-FileCopyrightText: Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "EventExpiry.h"
+
+#include "Cache_p.h"
+#include "MainWindow.h"
+#include "MatrixClient.h"
+#include "timeline/TimelineModel.h"
+
+void
+EventExpiry::load()
+{
+    using namespace mtx::events;
+
+    this->event = {};
+
+    if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry, "")) {
+        auto h = std::get<
+          mtx::events::AccountDataEvent<mtx::events::account_data::nheko_extensions::EventExpiry>>(
+          *temp);
+        this->event = std::move(h.content);
+    }
+
+    if (!roomid_.isEmpty()) {
+        if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry,
+                                                        roomid_.toStdString())) {
+            auto h      = std::get<mtx::events::AccountDataEvent<
+              mtx::events::account_data::nheko_extensions::EventExpiry>>(*temp);
+            this->event = std::move(h.content);
+        }
+    }
+
+    emit expireEventsAfterDaysChanged();
+    emit expireEventsAfterCountChanged();
+    emit protectLatestEventsChanged();
+    emit expireStateEventsChanged();
+}
+
+void
+EventExpiry::save()
+{
+    if (roomid_.isEmpty())
+        http::client()->put_account_data(event, [](mtx::http::RequestErr e) {
+            if (e) {
+                nhlog::net()->error("Failed to set hidden events: {}", *e);
+                MainWindow::instance()->showNotification(
+                  tr("Failed to set hidden events: %1")
+                    .arg(QString::fromStdString(e->matrix_error.error)));
+            }
+        });
+    else
+        http::client()->put_room_account_data(
+          roomid_.toStdString(), event, [](mtx::http::RequestErr e) {
+              if (e) {
+                  nhlog::net()->error("Failed to set hidden events: {}", *e);
+                  MainWindow::instance()->showNotification(
+                    tr("Failed to set hidden events: %1")
+                      .arg(QString::fromStdString(e->matrix_error.error)));
+              }
+          });
+}
+
+int
+EventExpiry::expireEventsAfterDays() const
+{
+    return event.expire_after_ms / (1000 * 60 * 60 * 24);
+}
+
+int
+EventExpiry::expireEventsAfterCount() const
+{
+    return event.keep_only_latest;
+}
+
+int
+EventExpiry::protectLatestEvents() const
+{
+    return event.protect_latest;
+}
+
+bool
+EventExpiry::expireStateEvents() const
+{
+    return !event.exclude_state_events;
+}
+
+void
+EventExpiry::setExpireEventsAfterDays(int val)
+{
+    if (val > 0)
+        this->event.expire_after_ms = val * (1000 * 60 * 60 * 24);
+    else
+        this->event.expire_after_ms = 0;
+    emit expireEventsAfterDaysChanged();
+}
+
+void
+EventExpiry::setProtectLatestEvents(int val)
+{
+    if (val > 0)
+        this->event.protect_latest = val;
+    else
+        this->event.expire_after_ms = 0;
+    emit protectLatestEventsChanged();
+}
+
+void
+EventExpiry::setExpireEventsAfterCount(int val)
+{
+    if (val > 0)
+        this->event.keep_only_latest = val;
+    else
+        this->event.keep_only_latest = 0;
+    emit expireEventsAfterCountChanged();
+}
+
+void
+EventExpiry::setExpireStateEvents(bool val)
+{
+    this->event.exclude_state_events = !val;
+    emit expireEventsAfterCountChanged();
+}
diff --git a/src/ui/EventExpiry.h b/src/ui/EventExpiry.h
new file mode 100644
index 00000000..aa144dc3
--- /dev/null
+++ b/src/ui/EventExpiry.h
@@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+#include <QQmlEngine>
+#include <QString>
+#include <QVariantList>
+
+#include <mtx/events/nheko_extensions/event_expiry.hpp>
+
+class EventExpiry : public QObject
+{
+    Q_OBJECT
+    QML_ELEMENT
+    Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged REQUIRED)
+    Q_PROPERTY(int expireEventsAfterDays READ expireEventsAfterDays WRITE setExpireEventsAfterDays
+                 NOTIFY expireEventsAfterDaysChanged)
+    Q_PROPERTY(bool expireStateEvents READ expireStateEvents WRITE setExpireStateEvents NOTIFY
+                 expireStateEventsChanged)
+    Q_PROPERTY(int expireEventsAfterCount READ expireEventsAfterCount WRITE
+                 setExpireEventsAfterCount NOTIFY expireEventsAfterCountChanged)
+    Q_PROPERTY(int protectLatestEvents READ protectLatestEvents WRITE setProtectLatestEvents NOTIFY
+                 protectLatestEventsChanged)
+public:
+    explicit EventExpiry(QObject *p = nullptr)
+      : QObject(p)
+    {
+    }
+
+    Q_INVOKABLE void save();
+
+    [[nodiscard]] QString roomid() const { return roomid_; }
+    void setRoomid(const QString &r)
+    {
+        roomid_ = r;
+        emit roomidChanged();
+
+        load();
+    }
+
+    [[nodiscard]] int expireEventsAfterDays() const;
+    [[nodiscard]] int expireEventsAfterCount() const;
+    [[nodiscard]] int protectLatestEvents() const;
+    [[nodiscard]] bool expireStateEvents() const;
+    void setExpireEventsAfterDays(int);
+    void setExpireEventsAfterCount(int);
+    void setProtectLatestEvents(int);
+    void setExpireStateEvents(bool);
+
+signals:
+    void roomidChanged();
+
+    void expireEventsAfterDaysChanged();
+    void expireEventsAfterCountChanged();
+    void protectLatestEventsChanged();
+    void expireStateEventsChanged();
+
+private:
+    QString roomid_;
+    mtx::events::account_data::nheko_extensions::EventExpiry event = {};
+
+    void load();
+};
+