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();
+};
+
|