1 // SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
2 // SPDX-FileCopyrightText: 2021 Nheko Contributors
3 //
4 // SPDX-License-Identifier: GPL-3.0-or-later
5 
6 #include <QApplication>
7 #include <QInputDialog>
8 #include <QMessageBox>
9 
10 #include <mtx/responses.hpp>
11 
12 #include "AvatarProvider.h"
13 #include "Cache.h"
14 #include "Cache_p.h"
15 #include "ChatPage.h"
16 #include "EventAccessors.h"
17 #include "Logging.h"
18 #include "MainWindow.h"
19 #include "MatrixClient.h"
20 #include "UserSettingsPage.h"
21 #include "Utils.h"
22 #include "encryption/DeviceVerificationFlow.h"
23 #include "encryption/Olm.h"
24 #include "ui/OverlayModal.h"
25 #include "ui/Theme.h"
26 #include "ui/UserProfile.h"
27 #include "voip/CallManager.h"
28 
29 #include "notifications/Manager.h"
30 
31 #include "timeline/TimelineViewManager.h"
32 
33 #include "blurhash.hpp"
34 
35 ChatPage *ChatPage::instance_             = nullptr;
36 constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
37 constexpr int RETRY_TIMEOUT               = 5'000;
38 constexpr size_t MAX_ONETIME_KEYS         = 50;
39 
40 Q_DECLARE_METATYPE(std::optional<mtx::crypto::EncryptedFile>)
Q_DECLARE_METATYPE(std::optional<RelatedInfo>)41 Q_DECLARE_METATYPE(std::optional<RelatedInfo>)
42 Q_DECLARE_METATYPE(mtx::presence::PresenceState)
43 Q_DECLARE_METATYPE(mtx::secret_storage::AesHmacSha2KeyDescription)
44 Q_DECLARE_METATYPE(SecretsToDecrypt)
45 
46 ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
47   : QWidget(parent)
48   , isConnected_(true)
49   , userSettings_{userSettings}
50   , notificationsManager(this)
51   , callManager_(new CallManager(this))
52 {
53     setObjectName("chatPage");
54 
55     instance_ = this;
56 
57     qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>();
58     qRegisterMetaType<std::optional<RelatedInfo>>();
59     qRegisterMetaType<mtx::presence::PresenceState>();
60     qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>();
61     qRegisterMetaType<SecretsToDecrypt>();
62 
63     topLayout_ = new QHBoxLayout(this);
64     topLayout_->setSpacing(0);
65     topLayout_->setMargin(0);
66 
67     view_manager_ = new TimelineViewManager(callManager_, this);
68 
69     topLayout_->addWidget(view_manager_->getWidget());
70 
71     connect(this,
72             &ChatPage::downloadedSecrets,
73             this,
74             &ChatPage::decryptDownloadedSecrets,
75             Qt::QueuedConnection);
76 
__anonfd66f7f20102() 77     connect(this, &ChatPage::connectionLost, this, [this]() {
78         nhlog::net()->info("connectivity lost");
79         isConnected_ = false;
80         http::client()->shutdown();
81     });
__anonfd66f7f20202() 82     connect(this, &ChatPage::connectionRestored, this, [this]() {
83         nhlog::net()->info("trying to re-connect");
84         isConnected_ = true;
85 
86         // Drop all pending connections.
87         http::client()->shutdown();
88         trySync();
89     });
90 
91     connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
__anonfd66f7f20302() 92     connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
93         if (http::client()->access_token().empty()) {
94             connectivityTimer_.stop();
95             return;
96         }
97 
98         http::client()->versions(
99           [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
100               if (err) {
101                   emit connectionLost();
102                   return;
103               }
104 
105               if (!isConnected_)
106                   emit connectionRestored();
107           });
108     });
109 
110     connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
111 
112     connect(
113       view_manager_,
114       &TimelineViewManager::inviteUsers,
115       this,
__anonfd66f7f20502(QString roomId, QStringList users) 116       [this](QString roomId, QStringList users) {
117           for (int ii = 0; ii < users.size(); ++ii) {
118               QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() {
119                   const auto user = users.at(ii);
120 
121                   http::client()->invite_user(
122                     roomId.toStdString(),
123                     user.toStdString(),
124                     [this, user](const mtx::responses::RoomInvite &, mtx::http::RequestErr err) {
125                         if (err) {
126                             emit showNotification(tr("Failed to invite user: %1").arg(user));
127                             return;
128                         }
129 
130                         emit showNotification(tr("Invited user: %1").arg(user));
131                     });
132               });
133           }
134       });
135 
136     connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
137     connect(this, &ChatPage::changeToRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
138     connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
139     connect(this,
140             &ChatPage::highlightedNotifsRetrieved,
141             this,
__anonfd66f7f20802(const mtx::responses::Notifications &notif) 142             [](const mtx::responses::Notifications &notif) {
143                 try {
144                     cache::saveTimelineMentions(notif);
145                 } catch (const lmdb::error &e) {
146                     nhlog::db()->error("failed to save mentions: {}", e.what());
147                 }
148             });
149 
150     connect(&notificationsManager,
151             &NotificationsManager::notificationClicked,
152             this,
__anonfd66f7f20902(const QString &roomid, const QString &eventid) 153             [this](const QString &roomid, const QString &eventid) {
154                 Q_UNUSED(eventid)
155                 view_manager_->rooms()->setCurrentRoom(roomid);
156                 activateWindow();
157             });
158     connect(&notificationsManager,
159             &NotificationsManager::sendNotificationReply,
160             this,
__anonfd66f7f20a02(const QString &roomid, const QString &eventid, const QString &body) 161             [this](const QString &roomid, const QString &eventid, const QString &body) {
162                 view_manager_->rooms()->setCurrentRoom(roomid);
163                 view_manager_->queueReply(roomid, eventid, body);
164                 activateWindow();
165             });
166 
__anonfd66f7f20b02() 167     connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
168         // ensure the qml context is shutdown before we destroy all other singletons
169         // Otherwise Qml will try to access the room list or settings, after they have been
170         // destroyed
171         topLayout_->removeWidget(view_manager_->getWidget());
172         delete view_manager_->getWidget();
173     });
174 
175     connect(
176       this,
177       &ChatPage::initializeViews,
178       view_manager_,
__anonfd66f7f20c02(const mtx::responses::Rooms &rooms) 179       [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); },
180       Qt::QueuedConnection);
181     connect(this,
182             &ChatPage::initializeEmptyViews,
183             view_manager_,
184             &TimelineViewManager::initializeRoomlist);
185     connect(
186       this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
__anonfd66f7f20d02(const mtx::responses::Rooms &rooms) 187     connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
188         view_manager_->sync(rooms);
189 
190         static unsigned int prevNotificationCount = 0;
191         unsigned int notificationCount            = 0;
192         for (const auto &room : rooms.join) {
193             notificationCount += room.second.unread_notifications.notification_count;
194         }
195 
196         // HACK: If we had less notifications last time we checked, send an alert if the
197         // user wanted one. Technically, this may cause an alert to be missed if new ones
198         // come in while you are reading old ones. Since the window is almost certainly open
199         // in this edge case, that's probably a non-issue.
200         // TODO: Replace this once we have proper pushrules support. This is a horrible hack
201         if (prevNotificationCount < notificationCount) {
202             if (userSettings_->hasAlertOnNotification())
203                 QApplication::alert(this);
204         }
205         prevNotificationCount = notificationCount;
206 
207         // No need to check amounts for this section, as this function internally checks for
208         // duplicates.
209         if (notificationCount && userSettings_->hasNotifications())
210             http::client()->notifications(
211               5,
212               "",
213               "",
214               [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) {
215                   if (err) {
216                       nhlog::net()->warn("failed to retrieve notifications: {} ({})",
217                                          err->matrix_error.error,
218                                          static_cast<int>(err->status_code));
219                       return;
220                   }
221 
222                   emit notificationsRetrieved(std::move(res));
223               });
224     });
225 
226     connect(
227       this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
228     connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync, Qt::QueuedConnection);
229     connect(
230       this,
231       &ChatPage::tryDelayedSyncCb,
232       this,
__anonfd66f7f20f02() 233       [this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
234       Qt::QueuedConnection);
235 
236     connect(
237       this, &ChatPage::newSyncResponse, this, &ChatPage::handleSyncResponse, Qt::QueuedConnection);
238 
239     connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
240 
241     connectCallMessage<mtx::events::msg::CallInvite>();
242     connectCallMessage<mtx::events::msg::CallCandidates>();
243     connectCallMessage<mtx::events::msg::CallAnswer>();
244     connectCallMessage<mtx::events::msg::CallHangUp>();
245 }
246 
247 void
logout()248 ChatPage::logout()
249 {
250     resetUI();
251     deleteConfigs();
252 
253     emit closing();
254     connectivityTimer_.stop();
255 }
256 
257 void
dropToLoginPage(const QString & msg)258 ChatPage::dropToLoginPage(const QString &msg)
259 {
260     nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
261 
262     http::client()->shutdown();
263     connectivityTimer_.stop();
264 
265     resetUI();
266     deleteConfigs();
267 
268     emit showLoginPage(msg);
269 }
270 
271 void
resetUI()272 ChatPage::resetUI()
273 {
274     view_manager_->clearAll();
275 
276     emit unreadMessages(0);
277 }
278 
279 void
deleteConfigs()280 ChatPage::deleteConfigs()
281 {
282     auto settings = UserSettings::instance()->qsettings();
283 
284     if (UserSettings::instance()->profile() != "") {
285         settings->beginGroup("profile");
286         settings->beginGroup(UserSettings::instance()->profile());
287     }
288     settings->beginGroup("auth");
289     settings->remove("");
290     settings->endGroup(); // auth
291 
292     http::client()->shutdown();
293     cache::deleteData();
294 }
295 
296 void
bootstrap(QString userid,QString homeserver,QString token)297 ChatPage::bootstrap(QString userid, QString homeserver, QString token)
298 {
299     using namespace mtx::identifiers;
300 
301     try {
302         http::client()->set_user(parse<User>(userid.toStdString()));
303     } catch (const std::invalid_argument &) {
304         nhlog::ui()->critical("bootstrapped with invalid user_id: {}", userid.toStdString());
305     }
306 
307     http::client()->set_server(homeserver.toStdString());
308     http::client()->set_access_token(token.toStdString());
309     http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
310 
311     // The Olm client needs the user_id & device_id that will be included
312     // in the generated payloads & keys.
313     olm::client()->set_user_id(http::client()->user_id().to_string());
314     olm::client()->set_device_id(http::client()->device_id());
315 
316     try {
317         cache::init(userid);
318 
319         connect(cache::client(), &Cache::databaseReady, this, [this]() {
320             nhlog::db()->info("database ready");
321 
322             const bool isInitialized = cache::isInitialized();
323             const auto cacheVersion  = cache::formatVersion();
324 
325             try {
326                 if (!isInitialized) {
327                     cache::setCurrentFormat();
328                 } else {
329                     if (cacheVersion == cache::CacheVersion::Current) {
330                         loadStateFromCache();
331                         return;
332                     } else if (cacheVersion == cache::CacheVersion::Older) {
333                         if (!cache::runMigrations()) {
334                             QMessageBox::critical(
335                               this,
336                               tr("Cache migration failed!"),
337                               tr("Migrating the cache to the current version failed. "
338                                  "This can have different reasons. Please open an "
339                                  "issue and try to use an older version in the mean "
340                                  "time. Alternatively you can try deleting the cache "
341                                  "manually."));
342                             QCoreApplication::quit();
343                         }
344                         loadStateFromCache();
345                         return;
346                     } else if (cacheVersion == cache::CacheVersion::Newer) {
347                         QMessageBox::critical(
348                           this,
349                           tr("Incompatible cache version"),
350                           tr("The cache on your disk is newer than this version of Nheko "
351                              "supports. Please update Nheko or clear your cache."));
352                         QCoreApplication::quit();
353                         return;
354                     }
355                 }
356 
357                 // It's the first time syncing with this device
358                 // There isn't a saved olm account to restore.
359                 nhlog::crypto()->info("creating new olm account");
360                 olm::client()->create_new_account();
361                 cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret()));
362             } catch (const lmdb::error &e) {
363                 nhlog::crypto()->critical("failed to save olm account {}", e.what());
364                 emit dropToLoginPageCb(QString::fromStdString(e.what()));
365                 return;
366             } catch (const mtx::crypto::olm_exception &e) {
367                 nhlog::crypto()->critical("failed to create new olm account {}", e.what());
368                 emit dropToLoginPageCb(QString::fromStdString(e.what()));
369                 return;
370             }
371 
372             getProfileInfo();
373             getBackupVersion();
374             tryInitialSync();
375             callManager_->refreshTurnServer();
376             emit MainWindow::instance()->reload();
377         });
378 
379         connect(cache::client(),
380                 &Cache::newReadReceipts,
381                 view_manager_,
382                 &TimelineViewManager::updateReadReceipts);
383 
384         connect(cache::client(),
385                 &Cache::removeNotification,
386                 &notificationsManager,
387                 &NotificationsManager::removeNotification);
388 
389     } catch (const lmdb::error &e) {
390         nhlog::db()->critical("failure during boot: {}", e.what());
391         emit dropToLoginPageCb(tr("Failed to open database, logging out!"));
392     }
393 }
394 
395 void
loadStateFromCache()396 ChatPage::loadStateFromCache()
397 {
398     nhlog::db()->info("restoring state from cache");
399 
400     try {
401         olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret());
402 
403         emit initializeEmptyViews();
404         emit initializeMentions(cache::getTimelineMentions());
405 
406         cache::calculateRoomReadStatus();
407 
408     } catch (const mtx::crypto::olm_exception &e) {
409         nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
410         emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again."));
411         return;
412     } catch (const lmdb::error &e) {
413         nhlog::db()->critical("failed to restore cache: {}", e.what());
414         emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
415         return;
416     } catch (const json::exception &e) {
417         nhlog::db()->critical("failed to parse cache data: {}", e.what());
418         emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
419         return;
420     } catch (const std::exception &e) {
421         nhlog::db()->critical("failed to load cache data: {}", e.what());
422         emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
423         return;
424     }
425 
426     nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
427     nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
428 
429     getProfileInfo();
430     getBackupVersion();
431     verifyOneTimeKeyCountAfterStartup();
432 
433     emit contentLoaded();
434 
435     // Start receiving events.
436     emit trySyncCb();
437 }
438 
439 void
removeRoom(const QString & room_id)440 ChatPage::removeRoom(const QString &room_id)
441 {
442     try {
443         cache::removeRoom(room_id);
444         cache::removeInvite(room_id.toStdString());
445     } catch (const lmdb::error &e) {
446         nhlog::db()->critical("failure while removing room: {}", e.what());
447         // TODO: Notify the user.
448     }
449 }
450 
451 void
sendNotifications(const mtx::responses::Notifications & res)452 ChatPage::sendNotifications(const mtx::responses::Notifications &res)
453 {
454     for (const auto &item : res.notifications) {
455         const auto event_id = mtx::accessors::event_id(item.event);
456 
457         try {
458             if (item.read) {
459                 cache::removeReadNotification(event_id);
460                 continue;
461             }
462 
463             if (!cache::isNotificationSent(event_id)) {
464                 const auto room_id = QString::fromStdString(item.room_id);
465 
466                 // We should only sent one notification per event.
467                 cache::markSentNotification(event_id);
468 
469                 // Don't send a notification when the current room is opened.
470                 if (isRoomActive(room_id))
471                     continue;
472 
473                 if (userSettings_->hasDesktopNotifications()) {
474                     auto info = cache::singleRoomInfo(item.room_id);
475 
476                     AvatarProvider::resolve(QString::fromStdString(info.avatar_url),
477                                             96,
478                                             this,
479                                             [this, item](QPixmap image) {
480                                                 notificationsManager.postNotification(
481                                                   item, image.toImage());
482                                             });
483                 }
484             }
485         } catch (const lmdb::error &e) {
486             nhlog::db()->warn("error while sending notification: {}", e.what());
487         }
488     }
489 }
490 
491 void
tryInitialSync()492 ChatPage::tryInitialSync()
493 {
494     nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
495     nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
496 
497     // Upload one time keys for the device.
498     nhlog::crypto()->info("generating one time keys");
499     olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS);
500 
501     http::client()->upload_keys(
502       olm::client()->create_upload_keys_request(),
503       [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
504           if (err) {
505               const int status_code = static_cast<int>(err->status_code);
506 
507               if (status_code == 404) {
508                   nhlog::net()->warn("skipping key uploading. server doesn't provide /keys/upload");
509                   return startInitialSync();
510               }
511 
512               nhlog::crypto()->critical(
513                 "failed to upload one time keys: {} {}", err->matrix_error.error, status_code);
514 
515               QString errorMsg(tr("Failed to setup encryption keys. Server response: "
516                                   "%1 %2. Please try again later.")
517                                  .arg(QString::fromStdString(err->matrix_error.error))
518                                  .arg(status_code));
519 
520               emit dropToLoginPageCb(errorMsg);
521               return;
522           }
523 
524           olm::mark_keys_as_published();
525 
526           for (const auto &entry : res.one_time_key_counts)
527               nhlog::net()->info("uploaded {} {} one-time keys", entry.second, entry.first);
528 
529           cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()});
530 
531           startInitialSync();
532       });
533 }
534 
535 void
startInitialSync()536 ChatPage::startInitialSync()
537 {
538     nhlog::net()->info("trying initial sync");
539 
540     mtx::http::SyncOpts opts;
541     opts.timeout      = 0;
542     opts.set_presence = currentPresence();
543 
544     http::client()->sync(opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
545         // TODO: Initial Sync should include mentions as well...
546 
547         if (err) {
548             const auto error      = QString::fromStdString(err->matrix_error.error);
549             const auto msg        = tr("Please try to login again: %1").arg(error);
550             const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
551             const int status_code = static_cast<int>(err->status_code);
552 
553             nhlog::net()->error("initial sync error: {} {} {} {}",
554                                 err->parse_error,
555                                 status_code,
556                                 err->error_code,
557                                 err_code);
558 
559             // non http related errors
560             if (status_code <= 0 || status_code >= 600) {
561                 startInitialSync();
562                 return;
563             }
564 
565             switch (status_code) {
566             case 502:
567             case 504:
568             case 524: {
569                 startInitialSync();
570                 return;
571             }
572             default: {
573                 emit dropToLoginPageCb(msg);
574                 return;
575             }
576             }
577         }
578 
579         nhlog::net()->info("initial sync completed");
580 
581         try {
582             cache::client()->saveState(res);
583 
584             olm::handle_to_device_messages(res.to_device.events);
585 
586             emit initializeViews(std::move(res.rooms));
587             emit initializeMentions(cache::getTimelineMentions());
588 
589             cache::calculateRoomReadStatus();
590         } catch (const lmdb::error &e) {
591             nhlog::db()->error("failed to save state after initial sync: {}", e.what());
592             startInitialSync();
593             return;
594         }
595 
596         emit trySyncCb();
597         emit contentLoaded();
598     });
599 }
600 
601 void
handleSyncResponse(const mtx::responses::Sync & res,const std::string & prev_batch_token)602 ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token)
603 {
604     try {
605         if (prev_batch_token != cache::nextBatchToken()) {
606             nhlog::net()->warn("Duplicate sync, dropping");
607             return;
608         }
609     } catch (const lmdb::error &e) {
610         nhlog::db()->warn("Logged out in the mean time, dropping sync");
611     }
612 
613     nhlog::net()->debug("sync completed: {}", res.next_batch);
614 
615     // Ensure that we have enough one-time keys available.
616     ensureOneTimeKeyCount(res.device_one_time_keys_count);
617 
618     // TODO: fine grained error handling
619     try {
620         cache::client()->saveState(res);
621         olm::handle_to_device_messages(res.to_device.events);
622 
623         auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
624 
625         emit syncUI(res.rooms);
626 
627         // if we process a lot of syncs (1 every 200ms), this means we clean the
628         // db every 100s
629         static int syncCounter = 0;
630         if (syncCounter++ >= 500) {
631             cache::deleteOldData();
632             syncCounter = 0;
633         }
634     } catch (const lmdb::map_full_error &e) {
635         nhlog::db()->error("lmdb is full: {}", e.what());
636         cache::deleteOldData();
637     } catch (const lmdb::error &e) {
638         nhlog::db()->error("saving sync response: {}", e.what());
639     }
640 
641     emit trySyncCb();
642 }
643 
644 void
trySync()645 ChatPage::trySync()
646 {
647     mtx::http::SyncOpts opts;
648     opts.set_presence = currentPresence();
649 
650     if (!connectivityTimer_.isActive())
651         connectivityTimer_.start();
652 
653     try {
654         opts.since = cache::nextBatchToken();
655     } catch (const lmdb::error &e) {
656         nhlog::db()->error("failed to retrieve next batch token: {}", e.what());
657         return;
658     }
659 
660     http::client()->sync(
661       opts, [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
662           if (err) {
663               const auto error      = QString::fromStdString(err->matrix_error.error);
664               const auto msg        = tr("Please try to login again: %1").arg(error);
665               const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
666               const int status_code = static_cast<int>(err->status_code);
667 
668               if ((http::is_logged_in() &&
669                    (err->matrix_error.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
670                     err->matrix_error.errcode == mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
671                   !http::is_logged_in()) {
672                   emit dropToLoginPageCb(msg);
673                   return;
674               }
675 
676               nhlog::net()->error("sync error: {} {} {} {}",
677                                   err->parse_error,
678                                   status_code,
679                                   err->error_code,
680                                   err_code);
681               emit tryDelayedSyncCb();
682               return;
683           }
684 
685           emit newSyncResponse(res, since);
686       });
687 }
688 
689 void
joinRoom(const QString & room)690 ChatPage::joinRoom(const QString &room)
691 {
692     const auto room_id = room.toStdString();
693     joinRoomVia(room_id, {}, false);
694 }
695 
696 void
joinRoomVia(const std::string & room_id,const std::vector<std::string> & via,bool promptForConfirmation)697 ChatPage::joinRoomVia(const std::string &room_id,
698                       const std::vector<std::string> &via,
699                       bool promptForConfirmation)
700 {
701     if (promptForConfirmation &&
702         QMessageBox::Yes !=
703           QMessageBox::question(
704             this,
705             tr("Confirm join"),
706             tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
707         return;
708 
709     http::client()->join_room(
710       room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
711           if (err) {
712               emit showNotification(
713                 tr("Failed to join room: %1").arg(QString::fromStdString(err->matrix_error.error)));
714               return;
715           }
716 
717           emit tr("You joined the room");
718 
719           // We remove any invites with the same room_id.
720           try {
721               cache::removeInvite(room_id);
722           } catch (const lmdb::error &e) {
723               emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
724           }
725 
726           view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
727       });
728 }
729 
730 void
createRoom(const mtx::requests::CreateRoom & req)731 ChatPage::createRoom(const mtx::requests::CreateRoom &req)
732 {
733     http::client()->create_room(
734       req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
735           if (err) {
736               const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
737               const auto error      = err->matrix_error.error;
738               const int status_code = static_cast<int>(err->status_code);
739 
740               nhlog::net()->warn("failed to create room: {} {} ({})", error, err_code, status_code);
741 
742               emit showNotification(
743                 tr("Room creation failed: %1").arg(QString::fromStdString(error)));
744               return;
745           }
746 
747           QString newRoomId = QString::fromStdString(res.room_id.to_string());
748           emit showNotification(tr("Room %1 created.").arg(newRoomId));
749           emit newRoom(newRoomId);
750           emit changeToRoom(newRoomId);
751       });
752 }
753 
754 void
leaveRoom(const QString & room_id)755 ChatPage::leaveRoom(const QString &room_id)
756 {
757     http::client()->leave_room(
758       room_id.toStdString(),
759       [this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) {
760           if (err) {
761               emit showNotification(tr("Failed to leave room: %1")
762                                       .arg(QString::fromStdString(err->matrix_error.error)));
763               return;
764           }
765 
766           emit leftRoom(room_id);
767       });
768 }
769 
770 void
changeRoom(const QString & room_id)771 ChatPage::changeRoom(const QString &room_id)
772 {
773     view_manager_->rooms()->setCurrentRoom(room_id);
774 }
775 
776 void
inviteUser(QString userid,QString reason)777 ChatPage::inviteUser(QString userid, QString reason)
778 {
779     auto room = currentRoom();
780 
781     if (QMessageBox::question(this,
782                               tr("Confirm invite"),
783                               tr("Do you really want to invite %1 (%2)?")
784                                 .arg(cache::displayName(room, userid))
785                                 .arg(userid)) != QMessageBox::Yes)
786         return;
787 
788     http::client()->invite_user(
789       room.toStdString(),
790       userid.toStdString(),
791       [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
792           if (err) {
793               emit showNotification(tr("Failed to invite %1 to %2: %3")
794                                       .arg(userid)
795                                       .arg(room)
796                                       .arg(QString::fromStdString(err->matrix_error.error)));
797           } else
798               emit showNotification(tr("Invited user: %1").arg(userid));
799       },
800       reason.trimmed().toStdString());
801 }
802 void
kickUser(QString userid,QString reason)803 ChatPage::kickUser(QString userid, QString reason)
804 {
805     auto room = currentRoom();
806 
807     if (QMessageBox::question(this,
808                               tr("Confirm kick"),
809                               tr("Do you really want to kick %1 (%2)?")
810                                 .arg(cache::displayName(room, userid))
811                                 .arg(userid)) != QMessageBox::Yes)
812         return;
813 
814     http::client()->kick_user(
815       room.toStdString(),
816       userid.toStdString(),
817       [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
818           if (err) {
819               emit showNotification(tr("Failed to kick %1 from %2: %3")
820                                       .arg(userid)
821                                       .arg(room)
822                                       .arg(QString::fromStdString(err->matrix_error.error)));
823           } else
824               emit showNotification(tr("Kicked user: %1").arg(userid));
825       },
826       reason.trimmed().toStdString());
827 }
828 void
banUser(QString userid,QString reason)829 ChatPage::banUser(QString userid, QString reason)
830 {
831     auto room = currentRoom();
832 
833     if (QMessageBox::question(this,
834                               tr("Confirm ban"),
835                               tr("Do you really want to ban %1 (%2)?")
836                                 .arg(cache::displayName(room, userid))
837                                 .arg(userid)) != QMessageBox::Yes)
838         return;
839 
840     http::client()->ban_user(
841       room.toStdString(),
842       userid.toStdString(),
843       [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
844           if (err) {
845               emit showNotification(tr("Failed to ban %1 in %2: %3")
846                                       .arg(userid)
847                                       .arg(room)
848                                       .arg(QString::fromStdString(err->matrix_error.error)));
849           } else
850               emit showNotification(tr("Banned user: %1").arg(userid));
851       },
852       reason.trimmed().toStdString());
853 }
854 void
unbanUser(QString userid,QString reason)855 ChatPage::unbanUser(QString userid, QString reason)
856 {
857     auto room = currentRoom();
858 
859     if (QMessageBox::question(this,
860                               tr("Confirm unban"),
861                               tr("Do you really want to unban %1 (%2)?")
862                                 .arg(cache::displayName(room, userid))
863                                 .arg(userid)) != QMessageBox::Yes)
864         return;
865 
866     http::client()->unban_user(
867       room.toStdString(),
868       userid.toStdString(),
869       [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
870           if (err) {
871               emit showNotification(tr("Failed to unban %1 in %2: %3")
872                                       .arg(userid)
873                                       .arg(room)
874                                       .arg(QString::fromStdString(err->matrix_error.error)));
875           } else
876               emit showNotification(tr("Unbanned user: %1").arg(userid));
877       },
878       reason.trimmed().toStdString());
879 }
880 
881 void
receivedSessionKey(const std::string & room_id,const std::string & session_id)882 ChatPage::receivedSessionKey(const std::string &room_id, const std::string &session_id)
883 {
884     view_manager_->receivedSessionKey(room_id, session_id);
885 }
886 
887 QString
status() const888 ChatPage::status() const
889 {
890     return QString::fromStdString(cache::statusMessage(utils::localUser().toStdString()));
891 }
892 
893 void
setStatus(const QString & status)894 ChatPage::setStatus(const QString &status)
895 {
896     http::client()->put_presence_status(
897       currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) {
898           if (err) {
899               nhlog::net()->warn("failed to set presence status_msg: {}", err->matrix_error.error);
900           }
901       });
902 }
903 
904 mtx::presence::PresenceState
currentPresence() const905 ChatPage::currentPresence() const
906 {
907     switch (userSettings_->presence()) {
908     case UserSettings::Presence::Online:
909         return mtx::presence::online;
910     case UserSettings::Presence::Unavailable:
911         return mtx::presence::unavailable;
912     case UserSettings::Presence::Offline:
913         return mtx::presence::offline;
914     default:
915         return mtx::presence::online;
916     }
917 }
918 
919 void
verifyOneTimeKeyCountAfterStartup()920 ChatPage::verifyOneTimeKeyCountAfterStartup()
921 {
922     http::client()->upload_keys(
923       olm::client()->create_upload_keys_request(),
924       [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
925           if (err) {
926               nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
927                                     err->matrix_error.error,
928                                     static_cast<int>(err->status_code),
929                                     static_cast<int>(err->error_code));
930 
931               if (err->status_code < 400 || err->status_code >= 500)
932                   return;
933           }
934 
935           std::map<std::string, uint16_t> key_counts;
936           auto count = 0;
937           if (auto c = res.one_time_key_counts.find(mtx::crypto::SIGNED_CURVE25519);
938               c == res.one_time_key_counts.end()) {
939               key_counts[mtx::crypto::SIGNED_CURVE25519] = 0;
940           } else {
941               key_counts[mtx::crypto::SIGNED_CURVE25519] = c->second;
942               count                                      = c->second;
943           }
944 
945           nhlog::crypto()->info(
946             "Fetched server key count {} {}", count, mtx::crypto::SIGNED_CURVE25519);
947 
948           ensureOneTimeKeyCount(key_counts);
949       });
950 }
951 
952 void
ensureOneTimeKeyCount(const std::map<std::string,uint16_t> & counts)953 ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
954 {
955     if (auto count = counts.find(mtx::crypto::SIGNED_CURVE25519); count != counts.end()) {
956         nhlog::crypto()->debug(
957           "Updated server key count {} {}", count->second, mtx::crypto::SIGNED_CURVE25519);
958 
959         if (count->second < MAX_ONETIME_KEYS) {
960             const int nkeys = MAX_ONETIME_KEYS - count->second;
961 
962             nhlog::crypto()->info("uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519);
963             olm::client()->generate_one_time_keys(nkeys);
964 
965             http::client()->upload_keys(
966               olm::client()->create_upload_keys_request(),
967               [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
968                   if (err) {
969                       nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
970                                             err->matrix_error.error,
971                                             static_cast<int>(err->status_code),
972                                             static_cast<int>(err->error_code));
973 
974                       if (err->status_code < 400 || err->status_code >= 500)
975                           return;
976                   }
977 
978                   // mark as published anyway, otherwise we may end up in a loop.
979                   olm::mark_keys_as_published();
980               });
981         } else if (count->second > 2 * MAX_ONETIME_KEYS) {
982             nhlog::crypto()->warn("too many one-time keys, deleting 1");
983             mtx::requests::ClaimKeys req;
984             req.one_time_keys[http::client()->user_id().to_string()][http::client()->device_id()] =
985               std::string(mtx::crypto::SIGNED_CURVE25519);
986             http::client()->claim_keys(
987               req, [](const mtx::responses::ClaimKeys &, mtx::http::RequestErr err) {
988                   if (err)
989                       nhlog::crypto()->warn("failed to clear 1 one-time key: {} {} {}",
990                                             err->matrix_error.error,
991                                             static_cast<int>(err->status_code),
992                                             static_cast<int>(err->error_code));
993                   else
994                       nhlog::crypto()->info("cleared 1 one-time key");
995               });
996         }
997     }
998 }
999 
1000 void
getProfileInfo()1001 ChatPage::getProfileInfo()
1002 {
1003     const auto userid = utils::localUser().toStdString();
1004 
1005     http::client()->get_profile(
1006       userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
1007           if (err) {
1008               nhlog::net()->warn("failed to retrieve own profile info");
1009               return;
1010           }
1011 
1012           emit setUserDisplayName(QString::fromStdString(res.display_name));
1013 
1014           emit setUserAvatar(QString::fromStdString(res.avatar_url));
1015       });
1016 }
1017 
1018 void
getBackupVersion()1019 ChatPage::getBackupVersion()
1020 {
1021     if (!UserSettings::instance()->useOnlineKeyBackup()) {
1022         nhlog::crypto()->info("Online key backup disabled.");
1023         return;
1024     }
1025 
1026     http::client()->backup_version(
1027       [this](const mtx::responses::backup::BackupVersion &res, mtx::http::RequestErr err) {
1028           if (err) {
1029               nhlog::net()->warn("Failed to retrieve backup version");
1030               if (err->status_code == 404)
1031                   cache::client()->deleteBackupVersion();
1032               return;
1033           }
1034 
1035           // switch to UI thread for secrets stuff
1036           QTimer::singleShot(0, this, [res] {
1037               auto auth_data = nlohmann::json::parse(res.auth_data);
1038 
1039               if (res.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") {
1040                   auto key = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
1041                   if (!key) {
1042                       nhlog::crypto()->info("No key for online key backup.");
1043                       cache::client()->deleteBackupVersion();
1044                       return;
1045                   }
1046 
1047                   using namespace mtx::crypto;
1048                   auto pubkey = CURVE25519_public_key_from_private(to_binary_buf(base642bin(*key)));
1049 
1050                   if (auth_data["public_key"].get<std::string>() != pubkey) {
1051                       nhlog::crypto()->info("Our backup key {} does not match the one "
1052                                             "used in the online backup {}",
1053                                             pubkey,
1054                                             auth_data["public_key"]);
1055                       cache::client()->deleteBackupVersion();
1056                       return;
1057                   }
1058 
1059                   nhlog::crypto()->info("Using online key backup.");
1060                   OnlineBackupVersion data{};
1061                   data.algorithm = res.algorithm;
1062                   data.version   = res.version;
1063                   cache::client()->saveBackupVersion(data);
1064               } else {
1065                   nhlog::crypto()->info("Unsupported key backup algorithm: {}", res.algorithm);
1066                   cache::client()->deleteBackupVersion();
1067               }
1068           });
1069       });
1070 }
1071 
1072 void
initiateLogout()1073 ChatPage::initiateLogout()
1074 {
1075     http::client()->logout([this](const mtx::responses::Logout &, mtx::http::RequestErr err) {
1076         if (err) {
1077             // TODO: handle special errors
1078             emit contentLoaded();
1079             nhlog::net()->warn("failed to logout: {} - {}",
1080                                mtx::errors::to_string(err->matrix_error.errcode),
1081                                err->matrix_error.error);
1082             return;
1083         }
1084 
1085         emit loggedOut();
1086     });
1087 
1088     emit showOverlayProgressBar();
1089 }
1090 
1091 template<typename T>
1092 void
connectCallMessage()1093 ChatPage::connectCallMessage()
1094 {
1095     connect(callManager_,
1096             qOverload<const QString &, const T &>(&CallManager::newMessage),
1097             view_manager_,
1098             qOverload<const QString &, const T &>(&TimelineViewManager::queueCallMessage));
1099 }
1100 
1101 void
decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,const SecretsToDecrypt & secrets)1102 ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
1103                                    const SecretsToDecrypt &secrets)
1104 {
1105     QString text = QInputDialog::getText(
1106       ChatPage::instance(),
1107       QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
1108       keyDesc.name.empty()
1109         ? QCoreApplication::translate(
1110             "CrossSigningSecrets", "Enter your recovery key or passphrase to decrypt your secrets:")
1111         : QCoreApplication::translate(
1112             "CrossSigningSecrets",
1113             "Enter your recovery key or passphrase called %1 to decrypt your secrets:")
1114             .arg(QString::fromStdString(keyDesc.name)),
1115       QLineEdit::Password);
1116 
1117     if (text.isEmpty())
1118         return;
1119 
1120     auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc);
1121 
1122     if (!decryptionKey && keyDesc.passphrase) {
1123         try {
1124             decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
1125         } catch (std::exception &e) {
1126             nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", e.what());
1127         }
1128     }
1129 
1130     if (!decryptionKey) {
1131         QMessageBox::information(
1132           ChatPage::instance(),
1133           QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"),
1134           QCoreApplication::translate("CrossSigningSecrets",
1135                                       "Failed to decrypt secrets with the "
1136                                       "provided recovery key or passphrase"));
1137         return;
1138     }
1139 
1140     auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
1141     mtx::requests::KeySignaturesUpload req;
1142 
1143     for (const auto &[secretName, encryptedSecret] : secrets) {
1144         auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
1145         if (!decrypted.empty()) {
1146             cache::storeSecret(secretName, decrypted);
1147 
1148             if (deviceKeys && deviceKeys->device_keys.count(http::client()->device_id()) &&
1149                 secretName == mtx::secret_storage::secrets::cross_signing_self_signing) {
1150                 auto myKey = deviceKeys->device_keys.at(http::client()->device_id());
1151                 if (myKey.user_id == http::client()->user_id().to_string() &&
1152                     myKey.device_id == http::client()->device_id() &&
1153                     myKey.keys["ed25519:" + http::client()->device_id()] ==
1154                       olm::client()->identity_keys().ed25519 &&
1155                     myKey.keys["curve25519:" + http::client()->device_id()] ==
1156                       olm::client()->identity_keys().curve25519) {
1157                     json j = myKey;
1158                     j.erase("signatures");
1159                     j.erase("unsigned");
1160 
1161                     auto ssk = mtx::crypto::PkSigning::from_seed(decrypted);
1162                     myKey.signatures[http::client()->user_id().to_string()]
1163                                     ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
1164                     req.signatures[http::client()->user_id().to_string()]
1165                                   [http::client()->device_id()] = myKey;
1166                 }
1167             } else if (deviceKeys &&
1168                        secretName == mtx::secret_storage::secrets::cross_signing_master) {
1169                 auto mk = mtx::crypto::PkSigning::from_seed(decrypted);
1170 
1171                 if (deviceKeys->master_keys.user_id == http::client()->user_id().to_string() &&
1172                     deviceKeys->master_keys.keys["ed25519:" + mk.public_key()] == mk.public_key()) {
1173                     json j = deviceKeys->master_keys;
1174                     j.erase("signatures");
1175                     j.erase("unsigned");
1176                     mtx::crypto::CrossSigningKeys master_key = j;
1177                     master_key.signatures[http::client()->user_id().to_string()]
1178                                          ["ed25519:" + http::client()->device_id()] =
1179                       olm::client()->sign_message(j.dump());
1180                     req.signatures[http::client()->user_id().to_string()][mk.public_key()] =
1181                       master_key;
1182                 }
1183             }
1184         }
1185     }
1186 
1187     if (!req.signatures.empty())
1188         http::client()->keys_signatures_upload(
1189           req, [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
1190               if (err) {
1191                   nhlog::net()->error("failed to upload signatures: {},{}",
1192                                       mtx::errors::to_string(err->matrix_error.errcode),
1193                                       static_cast<int>(err->status_code));
1194               }
1195 
1196               for (const auto &[user_id, tmp] : res.errors)
1197                   for (const auto &[key_id, e] : tmp)
1198                       nhlog::net()->error("signature error for user '{}' and key "
1199                                           "id {}: {}, {}",
1200                                           user_id,
1201                                           key_id,
1202                                           mtx::errors::to_string(e.errcode),
1203                                           e.error);
1204           });
1205 }
1206 
1207 void
startChat(QString userid)1208 ChatPage::startChat(QString userid)
1209 {
1210     auto joined_rooms = cache::joinedRooms();
1211     auto room_infos   = cache::getRoomInfo(joined_rooms);
1212 
1213     for (std::string room_id : joined_rooms) {
1214         if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
1215             auto room_members = cache::roomMembers(room_id);
1216             if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) !=
1217                 room_members.end()) {
1218                 view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
1219                 return;
1220             }
1221         }
1222     }
1223 
1224     if (QMessageBox::Yes !=
1225         QMessageBox::question(
1226           this,
1227           tr("Confirm invite"),
1228           tr("Do you really want to start a private chat with %1?").arg(userid)))
1229         return;
1230 
1231     mtx::requests::CreateRoom req;
1232     req.preset     = mtx::requests::Preset::PrivateChat;
1233     req.visibility = mtx::common::RoomVisibility::Private;
1234     if (utils::localUser() != userid) {
1235         req.invite    = {userid.toStdString()};
1236         req.is_direct = true;
1237     }
1238     emit ChatPage::instance()->createRoom(req);
1239 }
1240 
1241 static QString
mxidFromSegments(QStringRef sigil,QStringRef mxid)1242 mxidFromSegments(QStringRef sigil, QStringRef mxid)
1243 {
1244     if (mxid.isEmpty())
1245         return "";
1246 
1247     auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
1248 
1249     if (sigil == "u") {
1250         return "@" + mxid_;
1251     } else if (sigil == "roomid") {
1252         return "!" + mxid_;
1253     } else if (sigil == "r") {
1254         return "#" + mxid_;
1255         //} else if (sigil == "group") {
1256         //        return "+" + mxid_;
1257     } else {
1258         return "";
1259     }
1260 }
1261 
1262 bool
handleMatrixUri(const QByteArray & uri)1263 ChatPage::handleMatrixUri(const QByteArray &uri)
1264 {
1265     nhlog::ui()->info("Received uri! {}", uri.toStdString());
1266     QUrl uri_{QString::fromUtf8(uri)};
1267 
1268     // Convert matrix.to URIs to proper format
1269     if (uri_.scheme() == "https" && uri_.host() == "matrix.to") {
1270         QString p = uri_.fragment(QUrl::FullyEncoded);
1271         if (p.startsWith("/"))
1272             p.remove(0, 1);
1273 
1274         auto temp = p.split("?");
1275         QString query;
1276         if (temp.size() >= 2)
1277             query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
1278 
1279         temp            = temp.first().split("/");
1280         auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
1281         QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
1282         if (!identifier.isEmpty()) {
1283             if (identifier.startsWith("@")) {
1284                 QByteArray newUri = "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
1285                 if (!query.isEmpty())
1286                     newUri.append("?" + query.toUtf8());
1287                 return handleMatrixUri(QUrl::fromEncoded(newUri));
1288             } else if (identifier.startsWith("#")) {
1289                 QByteArray newUri = "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
1290                 if (!eventId.isEmpty())
1291                     newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
1292                 if (!query.isEmpty())
1293                     newUri.append("?" + query.toUtf8());
1294                 return handleMatrixUri(QUrl::fromEncoded(newUri));
1295             } else if (identifier.startsWith("!")) {
1296                 QByteArray newUri =
1297                   "matrix:roomid/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
1298                 if (!eventId.isEmpty())
1299                     newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
1300                 if (!query.isEmpty())
1301                     newUri.append("?" + query.toUtf8());
1302                 return handleMatrixUri(QUrl::fromEncoded(newUri));
1303             }
1304         }
1305     }
1306 
1307     // non-matrix URIs are not handled by us, return false
1308     if (uri_.scheme() != "matrix")
1309         return false;
1310 
1311     auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
1312     if (tempPath.startsWith('/'))
1313         tempPath.remove(0, 1);
1314     auto segments = tempPath.splitRef('/');
1315 
1316     if (segments.size() != 2 && segments.size() != 4)
1317         return false;
1318 
1319     auto sigil1 = segments[0];
1320     auto mxid1  = mxidFromSegments(sigil1, segments[1]);
1321     if (mxid1.isEmpty())
1322         return false;
1323 
1324     QString mxid2;
1325     if (segments.size() == 4 && segments[2] == "e") {
1326         if (segments[3].isEmpty())
1327             return false;
1328         else
1329             mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
1330     }
1331 
1332     std::vector<std::string> vias;
1333     QString action;
1334 
1335     for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) {
1336         nhlog::ui()->info("item: {}", item.toStdString());
1337 
1338         if (item.startsWith("action=")) {
1339             action = item.remove("action=");
1340         } else if (item.startsWith("via=")) {
1341             vias.push_back(QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
1342         }
1343     }
1344 
1345     if (sigil1 == "u") {
1346         if (action.isEmpty()) {
1347             auto t = view_manager_->rooms()->currentRoom();
1348             if (t && cache::isRoomMember(mxid1.toStdString(), t->roomId().toStdString())) {
1349                 t->openUserProfile(mxid1);
1350                 return true;
1351             }
1352             emit view_manager_->openGlobalUserProfile(mxid1);
1353         } else if (action == "chat") {
1354             this->startChat(mxid1);
1355         }
1356         return true;
1357     } else if (sigil1 == "roomid") {
1358         auto joined_rooms = cache::joinedRooms();
1359         auto targetRoomId = mxid1.toStdString();
1360 
1361         for (auto roomid : joined_rooms) {
1362             if (roomid == targetRoomId) {
1363                 view_manager_->rooms()->setCurrentRoom(mxid1);
1364                 if (!mxid2.isEmpty())
1365                     view_manager_->showEvent(mxid1, mxid2);
1366                 return true;
1367             }
1368         }
1369 
1370         if (action == "join" || action.isEmpty()) {
1371             joinRoomVia(targetRoomId, vias);
1372             return true;
1373         }
1374         return false;
1375     } else if (sigil1 == "r") {
1376         auto joined_rooms    = cache::joinedRooms();
1377         auto targetRoomAlias = mxid1.toStdString();
1378 
1379         for (auto roomid : joined_rooms) {
1380             auto aliases = cache::client()->getRoomAliases(roomid);
1381             if (aliases) {
1382                 if (aliases->alias == targetRoomAlias) {
1383                     view_manager_->rooms()->setCurrentRoom(QString::fromStdString(roomid));
1384                     if (!mxid2.isEmpty())
1385                         view_manager_->showEvent(QString::fromStdString(roomid), mxid2);
1386                     return true;
1387                 }
1388             }
1389         }
1390 
1391         if (action == "join" || action.isEmpty()) {
1392             joinRoomVia(mxid1.toStdString(), vias);
1393             return true;
1394         }
1395         return false;
1396     }
1397     return false;
1398 }
1399 
1400 bool
handleMatrixUri(const QUrl & uri)1401 ChatPage::handleMatrixUri(const QUrl &uri)
1402 {
1403     return handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
1404 }
1405 
1406 bool
isRoomActive(const QString & room_id)1407 ChatPage::isRoomActive(const QString &room_id)
1408 {
1409     return isActiveWindow() && currentRoom() == room_id;
1410 }
1411 
1412 QString
currentRoom() const1413 ChatPage::currentRoom() const
1414 {
1415     if (view_manager_->rooms()->currentRoom())
1416         return view_manager_->rooms()->currentRoom()->roomId();
1417     else
1418         return "";
1419 }
1420