diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Cache.cpp | 39 | ||||
-rw-r--r-- | src/Cache_p.h | 9 | ||||
-rw-r--r-- | src/ChatPage.cpp | 1 | ||||
-rw-r--r-- | src/UserSettingsPage.cpp | 33 | ||||
-rw-r--r-- | src/UserSettingsPage.h | 6 | ||||
-rw-r--r-- | src/Utils.cpp | 286 | ||||
-rw-r--r-- | src/Utils.h | 3 | ||||
-rw-r--r-- | src/ui/EventExpiry.cpp | 124 | ||||
-rw-r--r-- | src/ui/EventExpiry.h | 66 |
9 files changed, 564 insertions, 3 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp index 7a19cba4..3fe2892b 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -82,6 +82,8 @@ static constexpr auto DEVICES_DB("devices"); static constexpr auto DEVICE_KEYS_DB("device_keys"); //! room_ids that have encryption enabled. static constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); +//! Expiration progress for each room +static constexpr auto EVENT_EXPIRATION_BG_JOB_DB("event_expiration_bg_job"); //! room_id -> pickled OlmInboundGroupSession static constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); @@ -327,7 +329,9 @@ Cache::setup() megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); // What rooms are encrypted - encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + eventExpiryBgJob_ = lmdb::dbi::open(txn, EVENT_EXPIRATION_BG_JOB_DB, MDB_CREATE); + [[maybe_unused]] auto verificationDb = getVerificationDb(txn); [[maybe_unused]] auto userKeysDb = getUserKeysDb(txn); @@ -585,6 +589,39 @@ Cache::pickleSecret() } void +Cache::storeEventExpirationProgress(const std::string &room, + const std::string &expirationSettings, + const std::string &stopMarker) +{ + nlohmann::json j; + j["s"] = expirationSettings; + j["m"] = stopMarker; + + auto txn = lmdb::txn::begin(env_); + eventExpiryBgJob_.put(txn, room, j.dump()); + txn.commit(); +} + +std::string +Cache::loadEventExpirationProgress(const std::string &room, const std::string &expirationSettings) + +{ + try { + auto txn = ro_txn(env_); + std::string_view data; + if (!eventExpiryBgJob_.get(txn, room, data)) + return ""; + + auto j = nlohmann::json::parse(data); + if (j.value("s", "") == expirationSettings) + return j.value("m", ""); + } catch (...) { + return ""; + } + return ""; +} + +void Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) { nhlog::db()->info("mark room {} as encrypted", room_id); diff --git a/src/Cache_p.h b/src/Cache_p.h index 121e7e66..8d51c7c4 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -87,6 +87,13 @@ public: //! Retrieve if the room is tombstoned (closed or replaced by a different room) bool getRoomIsTombstoned(lmdb::txn &txn, lmdb::dbi &statesdb); + // for the event expiry background job + void storeEventExpirationProgress(const std::string &room, + const std::string &expirationSettings, + const std::string &stopMarker); + std::string + loadEventExpirationProgress(const std::string &room, const std::string &expirationSettings); + //! Get a specific state event template<typename T> std::optional<mtx::events::StateEvent<T>> @@ -714,6 +721,8 @@ private: lmdb::dbi encryptedRooms_; + lmdb::dbi eventExpiryBgJob_; + QString localUserId_; QString cacheDirectory_; 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 8ff8cec6..0ea42a27 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -22,6 +22,7 @@ #include <array> #include <cmath> +#include <mtx/responses/messages.hpp> #include <unordered_set> #include <variant> @@ -1466,7 +1467,7 @@ utils::updateSpaceVias() ChatPage::instance()->callFunctionOnGuiThread( [state = std::move(state), interval = e->matrix_error.retry_after]() { - QTimer::singleShot(interval, + QTimer::singleShot(interval * 3, ChatPage::instance(), [self = std::move(state)]() mutable { next(std::move(self)); @@ -1501,7 +1502,7 @@ utils::updateSpaceVias() ChatPage::instance()->callFunctionOnGuiThread( [state = std::move(state), interval = e->matrix_error.retry_after]() { - QTimer::singleShot(interval, + QTimer::singleShot(interval * 3, ChatPage::instance(), [self = std::move(state)]() mutable { next(std::move(self)); @@ -1604,3 +1605,284 @@ utils::updateSpaceVias() ApplySpaceUpdatesState::next(std::move(asus)); } + +std::atomic<bool> event_expiration_running = false; +void +utils::removeExpiredEvents() +{ + if (!UserSettings::instance()->expireEvents()) + return; + + if (event_expiration_running.exchange(true)) { + nhlog::net()->info("Event expiration still running, not starting second job."); + return; + } + + nhlog::net()->info("Remove expired events starting."); + + auto rooms = cache::roomInfo(false); + + auto us = http::client()->user_id().to_string(); + + using ExpType = + mtx::events::AccountDataEvent<mtx::events::account_data::nheko_extensions::EventExpiry>; + static auto getExpEv = [](const std::string &room = "") -> std::optional<ExpType> { + if (auto accountEvent = + cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry, room)) + if (auto ev = std::get_if<ExpType>(&*accountEvent); + ev && (ev->content.expire_after_ms || ev->content.keep_only_latest)) + return std::optional{*ev}; + return std::nullopt; + }; + + struct ApplyEventExpiration + { + std::optional<ExpType> globalExpiry; + std::vector<std::string> roomsToUpdate; + std::string filter; + + std::string currentRoom; + bool firstMessagesCall = true; + std::uint64_t currentRoomCount = 0; + + // batch token for pagination + std::string currentRoomPrevToken; + // event id of an event redacted in a previous run + std::string currentRoomStopAt; + // event id of first event redacted in the current run, hoping that the order stays the + // same. + std::string currentRoomFirstRedactedEvent; + // (evtype,state_key) tuple to keep the latest state event of each. + std::set<std::pair<std::string, std::string>> currentRoomStateEvents; + // event ids pending redaction + 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( + 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( + [state = std::move(state), + interval = e->matrix_error.retry_after]() { + // triple interval to allow other traffic as well + QTimer::singleShot(interval * 3, + ChatPage::instance(), + [self = std::move(state)]() mutable { + next(std::move(self)); + }); + }); + return; + } else { + nhlog::net()->error("Failed to redact event {} in {}: {}", + evid, + state->currentRoom, + *e); + state->currentRoomRedactionQueue.pop_back(); + next(std::move(state)); + } + } else { + nhlog::net()->info("Redacted event {} in {}", evid, state->currentRoom); + + if (state->currentRoomFirstRedactedEvent.empty()) + state->currentRoomFirstRedactedEvent = evid; + + state->currentRoomRedactionQueue.pop_back(); + next(std::move(state)); + } + }); + } else if (!state->currentRoom.empty()) { + if (state->currentRoomPrevToken.empty() && !state->firstMessagesCall) { + nhlog::net()->info("Finished room {}", state->currentRoom); + + if (!state->currentRoomFirstRedactedEvent.empty()) + cache::client()->storeEventExpirationProgress( + state->currentRoom, + nlohmann::json(state->currentExpiry).dump(), + state->currentRoomFirstRedactedEvent); + + state->currentRoom.clear(); + next(std::move(state)); + return; + } + + mtx::http::MessagesOpts opts{}; + opts.dir = mtx::http::PaginationDirection::Backwards; + opts.from = state->currentRoomPrevToken; + opts.limit = 1000; + opts.filter = state->filter; + opts.room_id = state->currentRoom; + + state->firstMessagesCall = false; + + http::client()->messages( + opts, + [state = std::move(state)](const mtx::responses::Messages &msgs, + mtx::http::RequestErr error) mutable { + if (error) { + // skip success handler + nhlog::net()->warn( + "Finished room {} with error {}", state->currentRoom, *error); + state->currentRoom.clear(); + } else if (msgs.chunk.empty()) { + state->currentRoomPrevToken.clear(); + } else { + state->currentRoomPrevToken = msgs.end; + + auto now = (uint64_t)QDateTime::currentMSecsSinceEpoch(); + auto us = http::client()->user_id().to_string(); + + for (const auto &e : msgs.chunk) { + if (std::holds_alternative< + mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e)) + continue; + + if (std::holds_alternative< + mtx::events::RoomEvent<mtx::events::msg::Redacted>>(e) || + std::holds_alternative< + mtx::events::StateEvent<mtx::events::msg::Redacted>>(e)) { + if (!state->currentRoomStopAt.empty() && + mtx::accessors::event_id(e) == state->currentRoomStopAt) { + // There is no filter to remove redacted events from + // pagination, so we try to stop early by caching what event + // we redacted last if we reached the end of a room. + nhlog::net()->info( + "Found previous redaction marker, stopping early: {}", + state->currentRoom); + state->currentRoomPrevToken.clear(); + break; + } + continue; + } + + if (std::holds_alternative< + mtx::events::StateEvent<mtx::events::msg::Redacted>>(e)) + continue; + + // synapse protects these 2 against redaction + if (std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::Create>>(e)) + continue; + + if (std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::ServerAcl>>(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; + + state->currentRoomCount++; + if (state->currentRoomCount <= state->currentExpiry.protect_latest) { + continue; + } + + if (state->currentExpiry.exclude_state_events && + 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 true; + }, + e)) + continue; + } + + if (state->currentExpiry.keep_only_latest && + state->currentRoomCount > state->currentExpiry.keep_only_latest) { + state->currentRoomRedactionQueue.push_back( + mtx::accessors::event_id(e)); + } else if (state->currentExpiry.expire_after_ms && + (state->currentExpiry.expire_after_ms + + mtx::accessors::origin_server_ts(e).toMSecsSinceEpoch()) < + now) { + state->currentRoomRedactionQueue.push_back( + mtx::accessors::event_id(e)); + } + } + } + + next(std::move(state)); + }); + } else if (!state->roomsToUpdate.empty()) { + const auto &room = state->roomsToUpdate.back(); + + auto localExp = getExpEv(room); + if (localExp) { + state->currentRoom = room; + state->currentExpiry = localExp->content; + } else if (state->globalExpiry) { + state->currentRoom = room; + state->currentExpiry = state->globalExpiry->content; + } + state->firstMessagesCall = true; + state->currentRoomCount = 0; + state->currentRoomPrevToken = ""; + state->currentRoomRedactionQueue.clear(); + state->currentRoomStateEvents.clear(); + + state->currentRoomStopAt = cache::client()->loadEventExpirationProgress( + state->currentRoom, nlohmann::json(state->currentExpiry).dump()); + + state->roomsToUpdate.pop_back(); + next(std::move(state)); + } else { + nhlog::net()->info("Finished event expiry"); + event_expiration_running = false; + } + } + }; + + auto asus = std::make_shared<ApplyEventExpiration>(); + + 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(); + + for (const auto &[roomid_, info] : rooms.toStdMap()) { + auto roomid = roomid_.toStdString(); + + if (!asus->globalExpiry && !getExpEv(roomid)) + continue; + + if (auto pl = cache::client() + ->getStateEvent<mtx::events::state::PowerLevels>(roomid) + .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) + .content; + pl.user_level(us) < pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) { + nhlog::net()->warn("Can't react events in {}, not running expiration.", roomid); + continue; + } + + asus->roomsToUpdate.push_back(roomid); + } + + nhlog::db()->info("Running expiration in {} rooms", asus->roomsToUpdate.size()); + + ApplyEventExpiration::next(std::move(asus)); +} diff --git a/src/Utils.h b/src/Utils.h index af5ea340..83f2cad1 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -339,4 +339,7 @@ roomVias(const std::string &roomid); void updateSpaceVias(); + +void +removeExpiredEvents(); } diff --git a/src/ui/EventExpiry.cpp b/src/ui/EventExpiry.cpp new file mode 100644 index 00000000..ef3f0933 --- /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 = std::uint64_t(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 = std::uint64_t(val); + else + this->event.expire_after_ms = 0; + emit protectLatestEventsChanged(); +} + +void +EventExpiry::setExpireEventsAfterCount(int val) +{ + if (val > 0) + this->event.keep_only_latest = std::uint64_t(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..378c4484 --- /dev/null +++ b/src/ui/EventExpiry.h @@ -0,0 +1,66 @@ +// 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(); +}; |