summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt16
-rw-r--r--README.md14
-rw-r--r--src/MainWindow.cpp16
-rw-r--r--src/MainWindow.h8
-rw-r--r--src/MxcImageProvider.cpp9
-rw-r--r--src/UserSettingsPage.cpp35
-rw-r--r--src/UserSettingsPage.h12
-rw-r--r--src/dbus/NhekoDBusApi.cpp166
-rw-r--r--src/dbus/NhekoDBusApi.h80
-rw-r--r--src/dbus/NhekoDBusBackend.cpp87
-rw-r--r--src/dbus/NhekoDBusBackend.h45
-rw-r--r--src/notifications/Manager.h5
-rw-r--r--src/notifications/ManagerLinux.cpp47
-rw-r--r--src/timeline/RoomlistModel.cpp13
-rw-r--r--src/timeline/RoomlistModel.h9
15 files changed, 508 insertions, 54 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f1efb47f..049e3b1e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -303,7 +303,6 @@ check_symbol_exists(backtrace_symbols_fd "execinfo.h" HAVE_BACKTRACE_SYMBOLS_FD)
 
 configure_file(cmake/nheko.h config/nheko.h)
 
-
 #
 # Declare source and header files.
 #
@@ -501,6 +500,11 @@ add_subdirectory(third_party/SingleApplication-3.3.2/)
 
 feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
 
+# this must be defined here to make the moc work properly
+if (NOT APPLE AND NOT WIN32)
+	add_compile_definitions(NHEKO_DBUS_SYS)
+endif()
+
 qt5_wrap_cpp(MOC_HEADERS
 	# Dialogs
 	src/dialogs/FallbackAuth.h
@@ -599,7 +603,15 @@ elseif (WIN32)
 
 	set(SRC_FILES ${SRC_FILES} src/notifications/ManagerWin.cpp src/wintoastlib.cpp)
 else ()
-	set(SRC_FILES ${SRC_FILES} src/notifications/ManagerLinux.cpp)
+	set(SRC_FILES ${SRC_FILES}
+        src/dbus/NhekoDBusApi.cpp
+        src/dbus/NhekoDBusBackend.cpp
+        src/notifications/ManagerLinux.cpp
+	)
+    qt5_wrap_cpp(MOC_HEADERS
+        src/dbus/NhekoDBusApi.h
+        src/dbus/NhekoDBusBackend.h
+	)
 endif ()
 
 set(NHEKO_DEPS
diff --git a/README.md b/README.md
index 0a89097d..06180735 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ Specifically there is support for:
 - Room switcher (ctrl-K).
 - Light, Dark & System themes.
 - Creating separate profiles (command line only, use `-p name`).
+- D-Bus API to allow integration with third-party plugins (does not support Windows or macOS).
 
 ## Installation
 
@@ -156,6 +157,12 @@ with [Chocolatey](https://chocolatey.org/):
 choco install nheko-reborn
 ```
 
+#### D-Bus plugins
+
+nheko does not provide binaries for any D-Bus plugins. However, we do provide the following list of known plugins:
+
+- [nheko-krunner](https://github.com/LorenDB/nheko-krunner)
+
 ### FAQ
 
 ---
@@ -409,7 +416,12 @@ Also copy the respective cmark.dll to the binary dir from `build/cmark-build/src
 
 ### Contributing
 
-See [CONTRIBUTING](.github/CONTRIBUTING.md)
+See [CONTRIBUTING](.github/CONTRIBUTING.md).
+
+### Using the D-Bus API
+
+Currently, there is no documentation for the D-Bus API, so if you'd like to make use of it, come ask
+for support in [#nheko:nheko.im](https://matrix.to/#/#nheko:nheko.im).
 
 ### Screens
 
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 7235f93d..ffc3c6c1 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -54,6 +54,10 @@
 #include "ui/UIA.h"
 #include "voip/WebRTCSession.h"
 
+#ifdef NHEKO_DBUS_SYS
+#include "dbus/NhekoDBusApi.h"
+#endif
+
 Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
 Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
 Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
@@ -282,6 +286,18 @@ MainWindow::registerQmlTypes()
         engine()->addImageProvider(QStringLiteral("jdenticon"), new JdenticonProvider());
 
     QObject::connect(engine(), &QQmlEngine::quit, &QGuiApplication::quit);
+
+#ifdef NHEKO_DBUS_SYS
+    if (UserSettings::instance()->exposeDBusApi()) {
+        if (QDBusConnection::sessionBus().isConnected() &&
+            QDBusConnection::sessionBus().registerService(NHEKO_DBUS_SERVICE_NAME)) {
+            nheko::dbus::init();
+            nhlog::ui()->info("Initialized D-Bus");
+            dbusAvailable_ = true;
+        } else
+            nhlog::ui()->warn("Could not connect to D-Bus!");
+    }
+#endif
 }
 
 void
diff --git a/src/MainWindow.h b/src/MainWindow.h
index e8c6fafd..3b1ff6f5 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -54,6 +54,10 @@ public:
     //! Show the chat page and start communicating with the given access token.
     void showChatPage();
 
+#ifdef NHEKO_DBUS_SYS
+    bool dbusAvailable() const { return dbusAvailable_; }
+#endif
+
 protected:
     void closeEvent(QCloseEvent *event);
     bool event(QEvent *event) override;
@@ -96,4 +100,8 @@ private:
     TrayIcon *trayIcon_;
 
     MxcImageProvider *imgProvider = nullptr;
+
+#ifdef NHEKO_DBUS_SYS
+    bool dbusAvailable_{false};
+#endif
 };
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index 0a91dde3..6098f0c4 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -10,6 +10,7 @@
 #include <mtxclient/crypto/client.hpp>
 
 #include <QByteArray>
+#include <QCache>
 #include <QDir>
 #include <QFileInfo>
 #include <QPainter>
@@ -103,6 +104,12 @@ MxcImageProvider::download(const QString &id,
                            bool crop,
                            double radius)
 {
+    if (id.isEmpty()) {
+        nhlog::net()->warn("Attempted to download image with empty ID");
+        then(id, QSize{}, QImage{}, QString{});
+        return;
+    }
+
     std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
     auto temp = infos.find("mxc://" + id);
     if (temp != infos.end())
@@ -264,6 +271,7 @@ MxcImageProvider::download(const QString &id,
                       image.setText(QStringLiteral("original filename"),
                                     QString::fromStdString(originalFilename));
                       image.setText(QStringLiteral("mxc url"), "mxc://" + id);
+
                       then(id, requestedSize, image, fileInfo.absoluteFilePath());
                       return;
                   }
@@ -276,6 +284,7 @@ MxcImageProvider::download(const QString &id,
                   image.setText(QStringLiteral("original filename"),
                                 QString::fromStdString(originalFilename));
                   image.setText(QStringLiteral("mxc url"), "mxc://" + id);
+
                   then(id, requestedSize, image, fileInfo.absoluteFilePath());
               });
         } catch (std::exception &e) {
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 636bf75f..932c3beb 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -90,6 +90,8 @@ UserSettings::load(std::optional<QString> profile)
     privacyScreen_     = settings.value(QStringLiteral("user/privacy_screen"), false).toBool();
     privacyScreenTimeout_ =
       settings.value(QStringLiteral("user/privacy_screen_timeout"), 0).toInt();
+    exposeDBusApi_ = settings.value(QStringLiteral("user/expose_dbus_api"), false).toBool();
+
     mobileMode_ = settings.value(QStringLiteral("user/mobile_mode"), false).toBool();
     emojiFont_  = settings.value(QStringLiteral("user/emoji_font_family"), "emoji").toString();
     baseFontSize_ =
@@ -248,6 +250,17 @@ UserSettings::setCollapsedSpaces(QList<QStringList> spaces)
 }
 
 void
+UserSettings::setExposeDBusApi(bool state)
+{
+    if (exposeDBusApi_ == state)
+        return;
+
+    exposeDBusApi_ = state;
+    emit exposeDBusApiChanged(state);
+    save();
+}
+
+void
 UserSettings::setMarkdown(bool state)
 {
     if (state == markdown_)
@@ -788,6 +801,7 @@ UserSettings::save()
     settings.setValue(QStringLiteral("use_identicon"), useIdenticon_);
     settings.setValue(QStringLiteral("open_image_external"), openImageExternal_);
     settings.setValue(QStringLiteral("open_video_external"), openVideoExternal_);
+    settings.setValue(QStringLiteral("expose_dbus_api"), exposeDBusApi_);
 
     settings.endGroup(); // user
 
@@ -972,6 +986,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return tr("User signing key");
         case MasterKey:
             return tr("Master signing key");
+        case ExposeDBusApi:
+            return tr("Expose room information via D-Bus");
         }
     } else if (role == Value) {
         switch (index.row()) {
@@ -1091,6 +1107,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
               .has_value();
         case MasterKey:
             return cache::secret(mtx::secret_storage::secrets::cross_signing_master).has_value();
+        case ExposeDBusApi:
+            return i->exposeDBusApi();
         }
     } else if (role == Description) {
         switch (index.row()) {
@@ -1235,6 +1253,12 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
               "Your most important key. You don't need to have it cached, since not caching "
               "it makes it less likely it can be stolen and it is only needed to rotate your "
               "other signing keys.");
+        case ExposeDBusApi:
+            return tr("Allow third-party plugins and applications to load information about rooms "
+                      "you are in via D-Bus. "
+                      "This can have useful applications, but it also could be used for nefarious "
+                      "purposes. Enable at your own risk.\n\n"
+                      "This setting will take effect upon restart.");
         }
     } else if (role == Type) {
         switch (index.row()) {
@@ -1279,6 +1303,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
         case OnlyShareKeysWithVerifiedUsers:
         case ShareKeysWithTrustedUsers:
         case UseOnlineKeyBackup:
+        case ExposeDBusApi:
             return Toggle;
         case Profile:
         case UserId:
@@ -1711,6 +1736,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
             } else
                 return false;
         }
+        case ExposeDBusApi: {
+            if (value.userType() == QMetaType::Bool) {
+                i->setExposeDBusApi(value.toBool());
+                return true;
+            } else
+                return false;
+        }
         }
     }
     return false;
@@ -1940,4 +1972,7 @@ UserSettingsModel::UserSettingsModel(QObject *p)
     connect(MainWindow::instance(), &MainWindow::secretsChanged, this, [this]() {
         emit dataChanged(index(OnlineBackupKey), index(MasterKey), {Value, Good});
     });
+    connect(s.get(), &UserSettings::exposeDBusApiChanged, this, [this] {
+        emit dataChanged(index(ExposeDBusApi), index(ExposeDBusApi), {Value});
+    });
 }
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index c34bf1fc..bcc45cdc 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -115,6 +115,8 @@ class UserSettings : public QObject
                  recentReactionsChanged)
     Q_PROPERTY(QStringList hiddenWidgets READ hiddenWidgets WRITE setHiddenWidgets NOTIFY
                  hiddenWidgetsChanged)
+    Q_PROPERTY(
+      bool exposeDBusApi READ exposeDBusApi WRITE setExposeDBusApi NOTIFY exposeDBusApiChanged)
 
     UserSettings();
 
@@ -191,6 +193,7 @@ public:
     void setOpenImageExternal(bool state);
     void setOpenVideoExternal(bool state);
     void setCollapsedSpaces(QList<QStringList> spaces);
+    void setExposeDBusApi(bool state);
 
     QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
     bool messageHoverHighlight() const { return messageHoverHighlight_; }
@@ -255,6 +258,7 @@ public:
     bool openImageExternal() const { return openImageExternal_; }
     bool openVideoExternal() const { return openVideoExternal_; }
     QList<QStringList> collapsedSpaces() const { return collapsedSpaces_; }
+    bool exposeDBusApi() const { return exposeDBusApi_; }
 
 signals:
     void groupViewStateChanged(bool state);
@@ -310,6 +314,7 @@ signals:
     void hiddenPinsChanged();
     void hiddenWidgetsChanged();
     void recentReactionsChanged();
+    void exposeDBusApiChanged(bool state);
 
 private:
     // Default to system theme if QT_QPA_PLATFORMTHEME var is set.
@@ -373,6 +378,7 @@ private:
     bool useIdenticon_;
     bool openImageExternal_;
     bool openVideoExternal_;
+    bool exposeDBusApi_;
 
     QSettings settings;
 
@@ -398,6 +404,9 @@ class UserSettingsModel : public QAbstractListModel
         UseIdenticon,
         PrivacyScreen,
         PrivacyScreenTimeout,
+#ifdef NHEKO_DBUS_SYS
+        ExposeDBusApi,
+#endif
 
         TimelineSection,
         TimelineMaxWidth,
@@ -458,6 +467,9 @@ class UserSettingsModel : public QAbstractListModel
 #ifdef Q_OS_MAC
         ScaleFactor,
 #endif
+#ifndef NHEKO_DBUS_SYS
+        ExposeDBusApi,
+#endif
     };
 
 public:
diff --git a/src/dbus/NhekoDBusApi.cpp b/src/dbus/NhekoDBusApi.cpp
new file mode 100644
index 00000000..edc3fa8a
--- /dev/null
+++ b/src/dbus/NhekoDBusApi.cpp
@@ -0,0 +1,166 @@
+// SPDX-FileCopyrightText: 2010 David Sansome <me@davidsansome.com>
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "NhekoDBusApi.h"
+
+#include <QDBusMetaType>
+
+namespace nheko::dbus {
+void
+init()
+{
+    qDBusRegisterMetaType<RoomInfoItem>();
+    qDBusRegisterMetaType<QVector<RoomInfoItem>>();
+    qDBusRegisterMetaType<QImage>();
+    qDBusRegisterMetaType<QVersionNumber>();
+}
+
+bool
+apiVersionIsCompatible(const QVersionNumber &clientAppVersion)
+{
+    if (clientAppVersion.majorVersion() != nheko::dbus::apiVersion.majorVersion())
+        return false;
+    if (clientAppVersion.minorVersion() > nheko::dbus::apiVersion.minorVersion())
+        return false;
+    if (clientAppVersion.minorVersion() == nheko::dbus::apiVersion.minorVersion() &&
+        clientAppVersion.microVersion() < nheko::dbus::apiVersion.microVersion())
+        return false;
+
+    return true;
+}
+
+RoomInfoItem::RoomInfoItem(const QString &roomId,
+                           const QString &alias,
+                           const QString &title,
+                           const QImage &image,
+                           const int unreadNotifications,
+                           QObject *parent)
+  : QObject{parent}
+  , roomId_{roomId}
+  , alias_{alias}
+  , roomName_{title}
+  , image_{image}
+  , unreadNotifications_{unreadNotifications}
+{}
+
+RoomInfoItem::RoomInfoItem(const RoomInfoItem &other)
+  : QObject{other.parent()}
+  , roomId_{other.roomId_}
+  , alias_{other.alias_}
+  , roomName_{other.roomName_}
+  , image_{other.image_}
+  , unreadNotifications_{other.unreadNotifications_}
+{}
+
+RoomInfoItem &
+RoomInfoItem::operator=(const RoomInfoItem &other)
+{
+    roomId_              = other.roomId_;
+    alias_               = other.alias_;
+    roomName_            = other.roomName_;
+    image_               = other.image_;
+    unreadNotifications_ = other.unreadNotifications_;
+    return *this;
+}
+
+QDBusArgument &
+operator<<(QDBusArgument &arg, const RoomInfoItem &item)
+{
+    arg.beginStructure();
+    arg << item.roomId_ << item.alias_ << item.roomName_ << item.image_
+        << item.unreadNotifications_;
+    arg.endStructure();
+    return arg;
+}
+
+const QDBusArgument &
+operator>>(const QDBusArgument &arg, RoomInfoItem &item)
+{
+    arg.beginStructure();
+    arg >> item.roomId_ >> item.alias_ >> item.roomName_ >> item.image_ >>
+      item.unreadNotifications_;
+    if (item.image_.isNull())
+        item.image_ = QImage{QStringLiteral(":/icons/ui/speech-bubbles.svg")};
+
+    arg.endStructure();
+    return arg;
+}
+} // nheko::dbus
+
+/**
+ * Automatic marshaling of a QImage for org.freedesktop.Notifications.Notify
+ *
+ * This function is heavily based on a function from the Clementine project (see
+ * http://www.clementine-player.org) and licensed under the GNU General Public
+ * License, version 3 or later.
+ *
+ * SPDX-FileCopyrightText: 2010 David Sansome <me@davidsansome.com>
+ */
+QDBusArgument &
+operator<<(QDBusArgument &arg, const QImage &image)
+{
+    if (image.isNull()) {
+        arg.beginStructure();
+        arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray();
+        arg.endStructure();
+        return arg;
+    }
+
+    QImage i = image.height() > 100 || image.width() > 100
+                 ? image.scaledToHeight(100, Qt::SmoothTransformation)
+                 : image;
+    i        = std::move(i).convertToFormat(QImage::Format_RGBA8888);
+
+    arg.beginStructure();
+    arg << i.width();
+    arg << i.height();
+    arg << i.bytesPerLine();
+    arg << i.hasAlphaChannel();
+    int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3);
+    arg << i.depth() / channels;
+    arg << channels;
+    arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes());
+    arg.endStructure();
+
+    return arg;
+}
+
+// This function, however, was merely reverse-engineered from the above function
+// and is not from the Clementine project.
+const QDBusArgument &
+operator>>(const QDBusArgument &arg, QImage &image)
+{
+    // garbage is used as a sort of /dev/null
+    int width, height, garbage;
+    QByteArray bits;
+
+    arg.beginStructure();
+    arg >> width >> height >> garbage >> garbage >> garbage >> garbage >> bits;
+    arg.endStructure();
+
+    image = QImage(reinterpret_cast<uchar *>(bits.data()), width, height, QImage::Format_RGBA8888);
+
+    return arg;
+}
+
+QDBusArgument &
+operator<<(QDBusArgument &arg, const QVersionNumber &v)
+{
+    arg.beginStructure();
+    arg << v.toString();
+    arg.endStructure();
+    return arg;
+}
+
+const QDBusArgument &
+operator>>(const QDBusArgument &arg, QVersionNumber &v)
+{
+    arg.beginStructure();
+    QString temp;
+    arg >> temp;
+    v = QVersionNumber::fromString(temp);
+    arg.endStructure();
+    return arg;
+}
diff --git a/src/dbus/NhekoDBusApi.h b/src/dbus/NhekoDBusApi.h
new file mode 100644
index 00000000..47cc108a
--- /dev/null
+++ b/src/dbus/NhekoDBusApi.h
@@ -0,0 +1,80 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef NHEKODBUSAPI_H
+#define NHEKODBUSAPI_H
+
+#include <QDBusArgument>
+#include <QIcon>
+#include <QObject>
+#include <QVersionNumber>
+
+namespace nheko::dbus {
+
+//! Registers all necessary classes with D-Bus. Call this before using any nheko D-Bus classes.
+void
+init();
+
+//! The nheko D-Bus API version provided by this file. The API version number follows semantic
+//! versioning as defined by https://semver.org.
+const QVersionNumber apiVersion{0, 0, 1};
+
+//! Compare the installed Nheko API to the version that your client app targets to see if they
+//! are compatible.
+bool
+apiVersionIsCompatible(const QVersionNumber &clientAppVersion);
+
+class RoomInfoItem : public QObject
+{
+    Q_OBJECT
+
+public:
+    RoomInfoItem(const QString &roomId         = QString{},
+                 const QString &alias          = QString{},
+                 const QString &title          = QString{},
+                 const QImage &image           = QImage{},
+                 const int unreadNotifications = 0,
+                 QObject *parent               = nullptr);
+
+    RoomInfoItem(const RoomInfoItem &other);
+
+    const QString &roomId() const { return roomId_; }
+    const QString &alias() const { return alias_; }
+    const QString &roomName() const { return roomName_; }
+    const QImage &image() const { return image_; }
+    int unreadNotifications() const { return unreadNotifications_; }
+
+    RoomInfoItem &operator=(const RoomInfoItem &other);
+    friend QDBusArgument &operator<<(QDBusArgument &arg, const nheko::dbus::RoomInfoItem &item);
+    friend const QDBusArgument &
+    operator>>(const QDBusArgument &arg, nheko::dbus::RoomInfoItem &item);
+
+private:
+    QString roomId_;
+    QString alias_;
+    QString roomName_;
+    QImage image_;
+    int unreadNotifications_;
+};
+
+QDBusArgument &
+operator<<(QDBusArgument &arg, const RoomInfoItem &item);
+const QDBusArgument &
+operator>>(const QDBusArgument &arg, RoomInfoItem &item);
+} // nheko::dbus
+Q_DECLARE_METATYPE(nheko::dbus::RoomInfoItem)
+
+QDBusArgument &
+operator<<(QDBusArgument &arg, const QImage &image);
+const QDBusArgument &
+operator>>(const QDBusArgument &arg, QImage &);
+
+QDBusArgument &
+operator<<(QDBusArgument &arg, const QVersionNumber &v);
+const QDBusArgument &
+operator>>(const QDBusArgument &arg, QVersionNumber &v);
+
+#define NHEKO_DBUS_SERVICE_NAME "io.github.Nheko-Reborn.nheko"
+
+#endif // NHEKODBUSAPI_H
diff --git a/src/dbus/NhekoDBusBackend.cpp b/src/dbus/NhekoDBusBackend.cpp
new file mode 100644
index 00000000..3645aea6
--- /dev/null
+++ b/src/dbus/NhekoDBusBackend.cpp
@@ -0,0 +1,87 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "NhekoDBusBackend.h"
+
+#include "Cache_p.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "MainWindow.h"
+#include "MxcImageProvider.h"
+#include "timeline/RoomlistModel.h"
+
+#include <QDBusConnection>
+
+NhekoDBusBackend::NhekoDBusBackend(RoomlistModel *parent)
+  : QObject{parent}
+  , m_parent{parent}
+{}
+
+QVector<nheko::dbus::RoomInfoItem>
+NhekoDBusBackend::getRooms(const QDBusMessage &message)
+{
+    const auto roomListModel = m_parent->models;
+    QSharedPointer<QVector<nheko::dbus::RoomInfoItem>> model{
+      new QVector<nheko::dbus::RoomInfoItem>};
+
+    for (const auto &room : roomListModel) {
+        MainWindow::instance()->imageProvider()->download(
+          room->roomAvatarUrl().remove("mxc://"),
+          {96, 96},
+          [message, room, model, roomListModel](
+            const QString &, const QSize &, const QImage &image, const QString &) {
+              const auto aliases = cache::client()->getRoomAliases(room->roomId().toStdString());
+              QString alias;
+              if (aliases.has_value()) {
+                  const auto &val = aliases.value();
+                  if (!val.alias.empty())
+                      alias = QString::fromStdString(val.alias);
+                  else if (val.alt_aliases.size() > 0)
+                      alias = QString::fromStdString(val.alt_aliases.front());
+              }
+
+              model->push_back(nheko::dbus::RoomInfoItem{
+                room->roomId(), room->roomName(), alias, image, room->notificationCount()});
+
+              if (model->length() == roomListModel.size()) {
+                  auto reply = message.createReply();
+                  nhlog::ui()->debug("Sending {} rooms over D-Bus...", model->size());
+                  reply << QVariant::fromValue(*model);
+                  QDBusConnection::sessionBus().send(reply);
+                  nhlog::ui()->debug("Rooms successfully sent to D-Bus.");
+              }
+          },
+          true);
+    }
+
+    return {};
+}
+
+void
+NhekoDBusBackend::activateRoom(const QString &alias) const
+{
+    bringWindowToTop();
+    m_parent->setCurrentRoom(alias);
+}
+
+void
+NhekoDBusBackend::joinRoom(const QString &alias) const
+{
+    bringWindowToTop();
+    ChatPage::instance()->joinRoom(alias);
+}
+
+void
+NhekoDBusBackend::startDirectChat(const QString &userId) const
+{
+    bringWindowToTop();
+    ChatPage::instance()->startChat(userId);
+}
+
+void
+NhekoDBusBackend::bringWindowToTop() const
+{
+    MainWindow::instance()->show();
+    MainWindow::instance()->raise();
+}
diff --git a/src/dbus/NhekoDBusBackend.h b/src/dbus/NhekoDBusBackend.h
new file mode 100644
index 00000000..02fd87d5
--- /dev/null
+++ b/src/dbus/NhekoDBusBackend.h
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef NHEKODBUSBACKEND_H
+#define NHEKODBUSBACKEND_H
+
+#include <QDBusMessage>
+#include <QObject>
+
+#include "NhekoDBusApi.h"
+#include "config/nheko.h"
+
+class RoomlistModel;
+
+class NhekoDBusBackend : public QObject
+{
+    Q_OBJECT
+    Q_CLASSINFO("D-Bus Interface", "im.nheko.Nheko")
+
+public:
+    NhekoDBusBackend(RoomlistModel *parent);
+
+public slots:
+    //! Get the nheko D-Bus API version.
+    Q_SCRIPTABLE QVersionNumber apiVersion() const { return nheko::dbus::apiVersion; }
+    //! Get the nheko version.
+    Q_SCRIPTABLE QString nhekoVersionString() const { return nheko::version; }
+    //! Call this function to get a list of all joined rooms.
+    Q_SCRIPTABLE QVector<nheko::dbus::RoomInfoItem> getRooms(const QDBusMessage &message);
+    //! Activates a currently joined room.
+    Q_SCRIPTABLE void activateRoom(const QString &alias) const;
+    //! Joins a room. It is your responsibility to ask for confirmation (if desired).
+    Q_SCRIPTABLE void joinRoom(const QString &alias) const;
+    //! Starts or activates a direct chat. It is your responsibility to ask for confirmation (if
+    //! desired).
+    Q_SCRIPTABLE void startDirectChat(const QString &userId) const;
+
+private:
+    void bringWindowToTop() const;
+
+    RoomlistModel *m_parent;
+};
+
+#endif // NHEKODBUSBACKEND_H
diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h
index 2a399a27..0a5f4caa 100644
--- a/src/notifications/Manager.h
+++ b/src/notifications/Manager.h
@@ -11,11 +11,6 @@
 
 #include <mtx/responses/notifications.hpp>
 
-// convenience definition
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
-#define NHEKO_DBUS_SYS
-#endif
-
 #if defined(NHEKO_DBUS_SYS)
 #include <QtDBus/QDBusArgument>
 #include <QtDBus/QDBusInterface>
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index 76f290a8..225a6533 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -27,6 +27,7 @@
 #include "MxcImageProvider.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
+#include "dbus/NhekoDBusApi.h"
 
 NotificationsManager::NotificationsManager(QObject *parent)
   : QObject(parent)
@@ -269,49 +270,3 @@ NotificationsManager::notificationClosed(uint id, uint reason)
     Q_UNUSED(reason);
     notificationIds.remove(id);
 }
-
-/**
- * Automatic marshaling of a QImage for org.freedesktop.Notifications.Notify
- *
- * This function is from the Clementine project (see
- * http://www.clementine-player.org) and licensed under the GNU General Public
- * License, version 3 or later.
- *
- * SPDX-FileCopyrightText: 2010 David Sansome <me@davidsansome.com>
- */
-QDBusArgument &
-operator<<(QDBusArgument &arg, const QImage &image)
-{
-    if (image.isNull()) {
-        arg.beginStructure();
-        arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray();
-        arg.endStructure();
-        return arg;
-    }
-
-    QImage i = image.height() > 100 || image.width() > 100
-                 ? image.scaledToHeight(100, Qt::SmoothTransformation)
-                 : image;
-    i        = std::move(i).convertToFormat(QImage::Format_RGBA8888);
-
-    arg.beginStructure();
-    arg << i.width();
-    arg << i.height();
-    arg << i.bytesPerLine();
-    arg << i.hasAlphaChannel();
-    int channels = i.hasAlphaChannel() ? 4 : 3;
-    arg << i.depth() / channels;
-    arg << channels;
-    arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes());
-    arg.endStructure();
-
-    return arg;
-}
-
-const QDBusArgument &
-operator>>(const QDBusArgument &arg, QImage &)
-{
-    // This is needed to link but shouldn't be called.
-    Q_ASSERT(0);
-    return arg;
-}
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 31e5a33d..ea4f6fa8 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -15,6 +15,10 @@
 #include "TimelineViewManager.h"
 #include "UserSettingsPage.h"
 
+#ifdef NHEKO_DBUS_SYS
+#include <QDBusConnection>
+#endif
+
 RoomlistModel::RoomlistModel(TimelineViewManager *parent)
   : QAbstractListModel(parent)
   , manager(parent)
@@ -604,6 +608,15 @@ RoomlistModel::initializeRooms()
     nhlog::db()->info("Restored {} rooms from cache", rowCount());
 
     endResetModel();
+
+#ifdef NHEKO_DBUS_SYS
+    if (MainWindow::instance()->dbusAvailable()) {
+        dbusInterface_ = new NhekoDBusBackend{this};
+        if (!QDBusConnection::sessionBus().registerObject(
+              "/", dbusInterface_, QDBusConnection::ExportScriptableSlots))
+            nhlog::ui()->warn("Failed to register rooms with D-Bus");
+    }
+#endif
 }
 
 void
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 73ccd929..9546d434 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -17,6 +17,10 @@
 
 #include "TimelineModel.h"
 
+#ifdef NHEKO_DBUS_SYS
+#include "dbus/NhekoDBusBackend.h"
+#endif
+
 class TimelineViewManager;
 
 class RoomPreview
@@ -138,6 +142,11 @@ private:
 
     std::map<QString, std::vector<QString>> directChatToUser;
 
+#ifdef NHEKO_DBUS_SYS
+    NhekoDBusBackend *dbusInterface_;
+    friend class NhekoDBusBackend;
+#endif
+
     friend class FilteredRoomlistModel;
 };