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 ¬if) 142 [](const mtx::responses::Notifications ¬if) {
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(¬ificationsManager,
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(¬ificationsManager,
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 ¬ificationsManager,
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