Add experimental event expiration

Currently disabled by default.
This commit is contained in:
Nicolas Werner 2023-07-05 00:08:37 +02:00
parent dcb6c00708
commit ad6e4fef64
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
10 changed files with 475 additions and 32 deletions

View file

@ -387,6 +387,8 @@ set(SRC_FILES
# UI components
src/ui/HiddenEvents.cpp
src/ui/HiddenEvents.h
src/ui/EventExpiry.cpp
src/ui/EventExpiry.h
src/ui/MxcAnimatedImage.cpp
src/ui/MxcAnimatedImage.h
src/ui/MxcMediaProxy.cpp
@ -599,7 +601,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG f4425af712afc6ad704a39b93c912432bd3c1914
GIT_TAG 0a4cc9421a97bea81a8921f3f5e040f0a34278fc
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@ -763,6 +765,7 @@ set(QML_SOURCES
resources/qml/dialogs/CreateDirect.qml
resources/qml/dialogs/CreateRoom.qml
resources/qml/dialogs/HiddenEventsDialog.qml
resources/qml/dialogs/EventExpirationDialog.qml
resources/qml/dialogs/ImageOverlay.qml
resources/qml/dialogs/ImagePackEditorDialog.qml
resources/qml/dialogs/ImagePackSettingsDialog.qml

View file

@ -214,7 +214,7 @@ modules:
buildsystem: cmake-ninja
name: mtxclient
sources:
- commit: f4425af712afc6ad704a39b93c912432bd3c1914
- commit: 0a4cc9421a97bea81a8921f3f5e040f0a34278fc
#tag: v0.9.2
type: git
url: https://github.com/Nheko-Reborn/mtxclient.git

View file

@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import im.nheko
ApplicationWindow {
id: dialog
property string roomid: ""
property string roomName: ""
property var onAccepted: undefined
modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowTitleHint
width: 275
height: 330
minimumWidth: 250
minimumHeight: 220
EventExpiry {
id: eventExpiry
roomid: dialog.roomid
}
title: {
if (roomid) {
return qsTr("Event expiration for %1").arg(roomName);
}
else {
return qsTr("Event expiration");
}
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: dbb.rejected()
}
ColumnLayout {
spacing: Nheko.paddingMedium
anchors.margins: Nheko.paddingMedium
anchors.fill: parent
MatrixText {
id: promptLabel
text: {
if (roomid) {
return qsTr("You can configure when your messages will be deleted in %1. This only happens when Nheko is open and has permissions to delete messages until Matrix servers support this feature natively. In general 0 means disable.").arg(roomName);
}
else {
return qsTr("You can configure when your messages will be deleted in all rooms unless configured otherwise. This only happens when Nheko is open and has permissions to delete messages until Matrix servers support this feature natively. In general 0 means disable.");
}
}
font.pixelSize: Math.floor(fontMetrics.font.pixelSize * 1.2)
Layout.fillWidth: true
Layout.fillHeight: false
}
GridLayout {
columns: 2
rowSpacing: Nheko.paddingMedium
Layout.fillWidth: true
Layout.fillHeight: true
MatrixText {
text: qsTr("Expire events after X days")
ToolTip.text: qsTr("Automatically redacts messages after X days, unless otherwise protected. Set to 0 to disable.")
ToolTip.visible: hh1.hovered
Layout.fillWidth: true
HoverHandler {
id: hh1
}
}
SpinBox {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
from: 0
to: 1000
stepSize: 1
value: eventExpiry.expireEventsAfterDays
onValueChanged: eventExpiry.expireEventsAfterDays = value
editable: true
}
MatrixText {
text: qsTr("Only keep latest X events")
ToolTip.text: qsTr("Deletes your events in this room if there are more than X newer messages unless otherwise protected. Set to 0 to disable.")
ToolTip.visible: hh2.hovered
Layout.fillWidth: true
HoverHandler {
id: hh2
}
}
SpinBox {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
from: 0
to: 1000
stepSize: 1
value: eventExpiry.expireEventsAfterCount
onValueChanged: eventExpiry.expireEventsAfterCount = value
editable: true
}
MatrixText {
text: qsTr("Always keep latest X events")
ToolTip.text: qsTr("This prevents events to be deleted by the above 2 settings if they are the latest X messages from you in the room.")
ToolTip.visible: hh3.hovered
Layout.fillWidth: true
HoverHandler {
id: hh3
}
}
SpinBox {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
from: 0
to: 1000
stepSize: 1
value: eventExpiry.protectLatestEvents
onValueChanged: eventExpiry.protectLatestEvents = value
editable: true
}
MatrixText {
text: qsTr("Include state events")
ToolTip.text: qsTr("If this is turned on, old state events also get redacted. The latest state event of any type+key combination is excluded from redaction to not remove the room name and similar state by accident.")
ToolTip.visible: hh4.hovered
Layout.fillWidth: true
HoverHandler {
id: hh4
}
}
ToggleButton {
Layout.alignment: Qt.AlignRight
checked: eventExpiry.expireStateEvents
onToggled: eventExpiry.expireStateEvents = checked
}
}
}
footer: DialogButtonBox {
id: dbb
standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
onAccepted: {
eventExpiry.save();
dialog.close();
}
onRejected: dialog.close();
}
}

View file

@ -501,6 +501,24 @@ ApplicationWindow {
Layout.alignment: Qt.AlignRight
}
Label {
text: qsTr("Automatic event deletion")
color: palette.text
}
EventExpirationDialog {
id: eventExpirationDialog
roomid: roomSettings.roomId
roomName: roomSettings.roomName
}
Button {
text: qsTr("Configure")
ToolTip.text: qsTr("Select if your events get automatically deleted in this room.")
onClicked: eventExpirationDialog.show()
Layout.alignment: Qt.AlignRight
}
Label {
text: qsTr("GENERAL SETTINGS")
font.bold: true

View file

@ -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_)

View file

@ -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();
@ -308,6 +310,17 @@ UserSettings::setUpdateSpaceVias(bool state)
save();
}
void
UserSettings::setExpireEvents(bool state)
{
if (expireEvents_ == state)
return;
expireEvents_ = state;
emit expireEventsChanged(state);
save();
}
void
UserSettings::setMarkdown(bool state)
{
@ -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});
});
}

View file

@ -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,

View file

@ -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();

124
src/ui/EventExpiry.cpp Normal file
View file

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

67
src/ui/EventExpiry.h Normal file
View file

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