From 39f9b7d90adbdbc9eb6186d93bb6bfd0564c152c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 10 Jan 2021 18:36:06 +0100 Subject: [PATCH] Handle matrix scheme Link opening only works on Linux for now. See https://github.com/matrix-org/matrix-doc/pull/2312 --- resources/nheko.desktop | 1 + src/Cache.cpp | 28 ++++++++ src/Cache_p.h | 1 + src/ChatPage.cpp | 140 ++++++++++++++++++++++++++++++++++++++++ src/ChatPage.h | 4 ++ src/main.cpp | 52 +++++++++++---- src/ui/UserProfile.cpp | 7 +- 7 files changed, 216 insertions(+), 17 deletions(-) diff --git a/resources/nheko.desktop b/resources/nheko.desktop index 16e04926..4404e460 100644 --- a/resources/nheko.desktop +++ b/resources/nheko.desktop @@ -8,3 +8,4 @@ Type=Application Categories=Network;InstantMessaging;Qt; StartupWMClass=nheko Terminal=false +MimeType=x-scheme-handler/matrix; diff --git a/src/Cache.cpp b/src/Cache.cpp index 04046346..17b55144 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -2221,6 +2221,34 @@ Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) return QString("1"); } +std::optional +Cache::getRoomAliases(const std::string &roomid) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto statesdb = getStatesDb(txn, roomid); + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCanonicalAlias)), event); + + if (res) { + try { + StateEvent msg = + json::parse(std::string_view(event.data(), event.size())); + + return msg.content; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", + e.what()); + } + } + + return std::nullopt; +} + QString Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { diff --git a/src/Cache_p.h b/src/Cache_p.h index 059c1461..e2ce1668 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -81,6 +81,7 @@ public: std::vector joinedRooms(); QMap roomInfo(bool withInvites = true); + std::optional getRoomAliases(const std::string &roomid); std::map invites(); //! Calculate & return the name of the room. diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 238c9362..33c993ae 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -918,6 +918,8 @@ ChatPage::joinRoom(const QString &room) } catch (const lmdb::error &e) { emit showNotification(tr("Failed to remove invite: %1").arg(e.what())); } + + room_list_->highlightSelectedRoom(QString::fromStdString(room_id)); }); } @@ -1268,3 +1270,141 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio cache::storeSecret(secretName, decrypted); } } + +void +ChatPage::startChat(QString userid) +{ + auto joined_rooms = cache::joinedRooms(); + auto room_infos = cache::getRoomInfo(joined_rooms); + + for (std::string room_id : joined_rooms) { + if (room_infos[QString::fromStdString(room_id)].member_count == 2) { + auto room_members = cache::roomMembers(room_id); + if (std::find(room_members.begin(), + room_members.end(), + (userid).toStdString()) != room_members.end()) { + room_list_->highlightSelectedRoom(QString::fromStdString(room_id)); + return; + } + } + } + + mtx::requests::CreateRoom req; + req.preset = mtx::requests::Preset::PrivateChat; + req.visibility = mtx::requests::Visibility::Private; + if (utils::localUser() != userid) + req.invite = {userid.toStdString()}; + emit ChatPage::instance()->createRoom(req); +} + +static QString +mxidFromSegments(QStringRef sigil, QStringRef mxid) +{ + if (mxid.isEmpty()) + return ""; + + auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8()); + + if (sigil == "user") { + return "@" + mxid_; + } else if (sigil == "roomid") { + return "!" + mxid_; + } else if (sigil == "room") { + return "#" + mxid_; + } else if (sigil == "group") { + return "+" + mxid_; + } else { + return ""; + } +} + +void +ChatPage::handleMatrixUri(const QByteArray &uri) +{ + nhlog::ui()->info("Received uri! {}", uri.toStdString()); + QUrl uri_{QString::fromUtf8(uri)}; + + if (uri_.scheme() != "matrix") + return; + + auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded); + if (tempPath.startsWith('/')) + tempPath.remove(0, 1); + auto segments = tempPath.splitRef('/'); + + if (segments.size() != 2 && segments.size() != 4) + return; + + auto sigil1 = segments[0]; + auto mxid1 = mxidFromSegments(sigil1, segments[1]); + if (mxid1.isEmpty()) + return; + + QString mxid2; + if (segments.size() == 4 && segments[2] == "event") { + if (segments[3].isEmpty()) + return; + else + mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8()); + } + + std::vector vias; + QString action; + + for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) { + nhlog::ui()->info("item: {}", item.toStdString()); + + if (item.startsWith("action=")) { + action = item.remove("action="); + } else if (item.startsWith("via=")) { + vias.push_back( + QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString()); + } + } + + if (sigil1 == "user") { + if (action.isEmpty()) { + view_manager_->activeTimeline()->openUserProfile(mxid1); + } else if (action == "chat") { + this->startChat(mxid1); + } + } else if (sigil1 == "roomid") { + auto joined_rooms = cache::joinedRooms(); + auto targetRoomId = mxid1.toStdString(); + + for (auto roomid : joined_rooms) { + if (roomid == targetRoomId) { + room_list_->highlightSelectedRoom(mxid1); + break; + } + } + + if (action == "join") { + joinRoom(mxid1); + } + } else if (sigil1 == "room") { + auto joined_rooms = cache::joinedRooms(); + auto targetRoomAlias = mxid1.toStdString(); + + for (auto roomid : joined_rooms) { + auto aliases = cache::client()->getRoomAliases(roomid); + if (aliases) { + if (aliases->alias == targetRoomAlias) { + room_list_->highlightSelectedRoom( + QString::fromStdString(roomid)); + break; + } + } + } + + if (action == "join") { + joinRoom(mxid1); + } + } +} + +void +ChatPage::handleMatrixUri(const QUrl &uri) +{ + handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8()); +} diff --git a/src/ChatPage.h b/src/ChatPage.h index 45a4ff63..004bb3e8 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -110,6 +110,10 @@ public: mtx::presence::PresenceState currentPresence() const; public slots: + void handleMatrixUri(const QByteArray &uri); + void handleMatrixUri(const QUrl &uri); + + void startChat(QString userid); void leaveRoom(const QString &room_id); void createRoom(const mtx::requests::CreateRoom &req); void joinRoom(const QString &room); diff --git a/src/main.cpp b/src/main.cpp index a60c66c4..7a417ae2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -33,6 +34,7 @@ #include #include +#include "ChatPage.h" #include "Config.h" #include "Logging.h" #include "MainWindow.h" @@ -128,34 +130,43 @@ main(int argc, char *argv[]) // This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name // parsed before the SingleApplication userdata is set. QString userdata{""}; + QString matrixUri; for (int i = 0; i < argc; ++i) { - if (QString{argv[i]}.startsWith("--profile=")) { - QString q{argv[i]}; - q.remove("--profile="); - userdata = q; - } else if (QString{argv[i]}.startsWith("--p=")) { - QString q{argv[i]}; - q.remove("-p="); - userdata = q; - } else if (QString{argv[i]} == "--profile" || QString{argv[i]} == "-p") { + QString arg{argv[i]}; + if (arg.startsWith("--profile=")) { + arg.remove("--profile="); + userdata = arg; + } else if (arg.startsWith("--p=")) { + arg.remove("-p="); + userdata = arg; + } else if (arg == "--profile" || arg == "-p") { if (i < argc - 1) // if i is less than argc - 1, we still have a parameter // left to process as the name { ++i; // the next arg is the name, so increment userdata = QString{argv[i]}; } + } else if (arg.startsWith("matrix:")) { + matrixUri = arg; } } SingleApplication app(argc, argv, - false, + true, SingleApplication::Mode::User | SingleApplication::Mode::ExcludeAppPath | - SingleApplication::Mode::ExcludeAppVersion, + SingleApplication::Mode::ExcludeAppVersion | + SingleApplication::Mode::SecondaryNotification, 100, userdata); + if (app.isSecondary()) { + // open uri in main instance + app.sendMessage(matrixUri.toUtf8()); + return 0; + } + QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); @@ -245,6 +256,25 @@ main(int argc, char *argv[]) w.activateWindow(); }); + QObject::connect( + &app, + &SingleApplication::receivedMessage, + ChatPage::instance(), + [&](quint32, QByteArray message) { ChatPage::instance()->handleMatrixUri(message); }); + + QMetaObject::Connection uriConnection; + if (app.isPrimary() && !matrixUri.isEmpty()) { + uriConnection = QObject::connect(ChatPage::instance(), + &ChatPage::contentLoaded, + ChatPage::instance(), + [&uriConnection, matrixUri]() { + ChatPage::instance()->handleMatrixUri( + matrixUri.toUtf8()); + QObject::disconnect(uriConnection); + }); + } + QDesktopServices::setUrlHandler("matrix", ChatPage::instance(), "handleMatrixUri"); + #if defined(Q_OS_MAC) // Temporary solution for the emoji picker until // nheko has a proper menu bar with more functionality. diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp index 974aa5cc..6ef82123 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp @@ -202,12 +202,7 @@ UserProfile::kickUser() void UserProfile::startChat() { - mtx::requests::CreateRoom req; - req.preset = mtx::requests::Preset::PrivateChat; - req.visibility = mtx::requests::Visibility::Private; - if (utils::localUser() != this->userid_) - req.invite = {this->userid_.toStdString()}; - emit ChatPage::instance()->createRoom(req); + ChatPage::instance()->startChat(this->userid_); } void