Skip to content
Snippets Groups Projects
ChatPage.cpp 50 KiB
Newer Older
  • Learn to ignore specific revisions
  •           if (err) {
                  nhlog::net()->warn("failed to retrieve own profile info");
                  return;
              }
    
              emit setUserDisplayName(QString::fromStdString(res.display_name));
    
              emit setUserAvatar(QString::fromStdString(res.avatar_url));
          });
    
    void
    ChatPage::getBackupVersion()
    {
    
        if (!UserSettings::instance()->useOnlineKeyBackup()) {
            nhlog::crypto()->info("Online key backup disabled.");
            return;
        }
    
        http::client()->backup_version(
          [this](const mtx::responses::backup::BackupVersion &res, mtx::http::RequestErr err) {
              if (err) {
                  nhlog::net()->warn("Failed to retrieve backup version");
                  if (err->status_code == 404)
                      cache::client()->deleteBackupVersion();
                  return;
              }
    
              // switch to UI thread for secrets stuff
              QTimer::singleShot(0, this, [res] {
                  auto auth_data = nlohmann::json::parse(res.auth_data);
    
                  if (res.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") {
                      auto key = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
                      if (!key) {
                          nhlog::crypto()->info("No key for online key backup.");
                          cache::client()->deleteBackupVersion();
                          return;
    
                      using namespace mtx::crypto;
                      auto pubkey = CURVE25519_public_key_from_private(to_binary_buf(base642bin(*key)));
    
                      if (auth_data["public_key"].get<std::string>() != pubkey) {
                          nhlog::crypto()->info("Our backup key {} does not match the one "
    
                                                "used in the online backup {}",
                                                pubkey,
                                                auth_data["public_key"]);
    
                          cache::client()->deleteBackupVersion();
                          return;
                      }
    
                      nhlog::crypto()->info("Using online key backup.");
                      OnlineBackupVersion data{};
                      data.algorithm = res.algorithm;
                      data.version   = res.version;
                      cache::client()->saveBackupVersion(data);
                  } else {
                      nhlog::crypto()->info("Unsupported key backup algorithm: {}", res.algorithm);
                      cache::client()->deleteBackupVersion();
                  }
    
    void
    ChatPage::initiateLogout()
    {
    
        http::client()->logout([this](const mtx::responses::Logout &, mtx::http::RequestErr err) {
            if (err) {
                // TODO: handle special errors
                emit contentLoaded();
                nhlog::net()->warn("failed to logout: {} - {}",
                                   mtx::errors::to_string(err->matrix_error.errcode),
                                   err->matrix_error.error);
                return;
            }
    
            emit loggedOut();
        });
    
        emit showOverlayProgressBar();
    
    CH Chethan Reddy's avatar
    CH Chethan Reddy committed
    
    
    trilene's avatar
    trilene committed
    template<typename T>
    void
    ChatPage::connectCallMessage()
    {
    
        connect(callManager_,
                qOverload<const QString &, const T &>(&CallManager::newMessage),
                view_manager_,
                qOverload<const QString &, const T &>(&TimelineViewManager::queueCallMessage));
    
    trilene's avatar
    trilene committed
    }
    
    
    void
    ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
                                       const SecretsToDecrypt &secrets)
    {
    
        QString text = QInputDialog::getText(
          ChatPage::instance(),
          QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
          keyDesc.name.empty()
            ? QCoreApplication::translate(
                "CrossSigningSecrets", "Enter your recovery key or passphrase to decrypt your secrets:")
            : QCoreApplication::translate(
                "CrossSigningSecrets",
                "Enter your recovery key or passphrase called %1 to decrypt your secrets:")
                .arg(QString::fromStdString(keyDesc.name)),
          QLineEdit::Password);
    
        if (text.isEmpty())
            return;
    
        auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc);
    
        if (!decryptionKey && keyDesc.passphrase) {
            try {
                decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
            } catch (std::exception &e) {
                nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", e.what());
    
        if (!decryptionKey) {
            QMessageBox::information(
              ChatPage::instance(),
              QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"),
              QCoreApplication::translate("CrossSigningSecrets",
                                          "Failed to decrypt secrets with the "
                                          "provided recovery key or passphrase"));
            return;
        }
    
    
        auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
        mtx::requests::KeySignaturesUpload req;
    
    
        for (const auto &[secretName, encryptedSecret] : secrets) {
            auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
    
            if (!decrypted.empty()) {
    
                cache::storeSecret(secretName, decrypted);
    
                if (deviceKeys && deviceKeys->device_keys.count(http::client()->device_id()) &&
    
                    secretName == mtx::secret_storage::secrets::cross_signing_self_signing) {
                    auto myKey = deviceKeys->device_keys.at(http::client()->device_id());
                    if (myKey.user_id == http::client()->user_id().to_string() &&
                        myKey.device_id == http::client()->device_id() &&
                        myKey.keys["ed25519:" + http::client()->device_id()] ==
                          olm::client()->identity_keys().ed25519 &&
                        myKey.keys["curve25519:" + http::client()->device_id()] ==
                          olm::client()->identity_keys().curve25519) {
                        json j = myKey;
                        j.erase("signatures");
                        j.erase("unsigned");
    
                        auto ssk = mtx::crypto::PkSigning::from_seed(decrypted);
                        myKey.signatures[http::client()->user_id().to_string()]
                                        ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
                        req.signatures[http::client()->user_id().to_string()]
                                      [http::client()->device_id()] = myKey;
                    }
                } else if (deviceKeys &&
                           secretName == mtx::secret_storage::secrets::cross_signing_master) {
                    auto mk = mtx::crypto::PkSigning::from_seed(decrypted);
    
                    if (deviceKeys->master_keys.user_id == http::client()->user_id().to_string() &&
                        deviceKeys->master_keys.keys["ed25519:" + mk.public_key()] == mk.public_key()) {
                        json j = deviceKeys->master_keys;
                        j.erase("signatures");
                        j.erase("unsigned");
                        mtx::crypto::CrossSigningKeys master_key = j;
                        master_key.signatures[http::client()->user_id().to_string()]
                                             ["ed25519:" + http::client()->device_id()] =
                          olm::client()->sign_message(j.dump());
                        req.signatures[http::client()->user_id().to_string()][mk.public_key()] =
                          master_key;
                    }
                }
            }
    
    
        if (!req.signatures.empty())
            http::client()->keys_signatures_upload(
              req, [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
                  if (err) {
                      nhlog::net()->error("failed to upload signatures: {},{}",
                                          mtx::errors::to_string(err->matrix_error.errcode),
                                          static_cast<int>(err->status_code));
                  }
    
                  for (const auto &[user_id, tmp] : res.errors)
                      for (const auto &[key_id, e] : tmp)
                          nhlog::net()->error("signature error for user '{}' and key "
                                              "id {}: {}, {}",
                                              user_id,
                                              key_id,
                                              mtx::errors::to_string(e.errcode),
                                              e.error);
              });
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
    void
    ChatPage::startChat(QString userid)
    {
    
        auto joined_rooms = cache::joinedRooms();
        auto room_infos   = cache::getRoomInfo(joined_rooms);
    
        for (std::string room_id : joined_rooms) {
            if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
                auto room_members = cache::roomMembers(room_id);
                if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) !=
                    room_members.end()) {
                    view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
    
        }
    
        if (QMessageBox::Yes !=
            QMessageBox::question(
              this,
              tr("Confirm invite"),
              tr("Do you really want to start a private chat with %1?").arg(userid)))
            return;
    
        mtx::requests::CreateRoom req;
        req.preset     = mtx::requests::Preset::PrivateChat;
        req.visibility = mtx::common::RoomVisibility::Private;
        if (utils::localUser() != userid) {
            req.invite    = {userid.toStdString()};
            req.is_direct = true;
        }
        emit ChatPage::instance()->createRoom(req);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    }
    
    static QString
    mxidFromSegments(QStringRef sigil, QStringRef mxid)
    {
    
        if (mxid.isEmpty())
            return "";
    
        auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
    
        if (sigil == "u") {
            return "@" + mxid_;
        } else if (sigil == "roomid") {
            return "!" + mxid_;
        } else if (sigil == "r") {
            return "#" + mxid_;
            //} else if (sigil == "group") {
            //        return "+" + mxid_;
        } else {
            return "";
        }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    ChatPage::handleMatrixUri(const QByteArray &uri)
    {
    
        nhlog::ui()->info("Received uri! {}", uri.toStdString());
        QUrl uri_{QString::fromUtf8(uri)};
    
        // Convert matrix.to URIs to proper format
        if (uri_.scheme() == "https" && uri_.host() == "matrix.to") {
            QString p = uri_.fragment(QUrl::FullyEncoded);
            if (p.startsWith("/"))
                p.remove(0, 1);
    
            auto temp = p.split("?");
            QString query;
            if (temp.size() >= 2)
                query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
    
            temp            = temp.first().split("/");
            auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
            QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
            if (!identifier.isEmpty()) {
                if (identifier.startsWith("@")) {
                    QByteArray newUri = "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
                    if (!query.isEmpty())
                        newUri.append("?" + query.toUtf8());
                    return handleMatrixUri(QUrl::fromEncoded(newUri));
                } else if (identifier.startsWith("#")) {
                    QByteArray newUri = "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
                    if (!eventId.isEmpty())
                        newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
                    if (!query.isEmpty())
                        newUri.append("?" + query.toUtf8());
                    return handleMatrixUri(QUrl::fromEncoded(newUri));
                } else if (identifier.startsWith("!")) {
                    QByteArray newUri =
                      "matrix:roomid/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
                    if (!eventId.isEmpty())
                        newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
                    if (!query.isEmpty())
                        newUri.append("?" + query.toUtf8());
                    return handleMatrixUri(QUrl::fromEncoded(newUri));
                }
    
        // non-matrix URIs are not handled by us, return false
        if (uri_.scheme() != "matrix")
            return false;
    
        auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
        if (tempPath.startsWith('/'))
            tempPath.remove(0, 1);
        auto segments = tempPath.splitRef('/');
    
        if (segments.size() != 2 && segments.size() != 4)
            return false;
    
        auto sigil1 = segments[0];
        auto mxid1  = mxidFromSegments(sigil1, segments[1]);
        if (mxid1.isEmpty())
            return false;
    
        QString mxid2;
        if (segments.size() == 4 && segments[2] == "e") {
            if (segments[3].isEmpty())
                return false;
            else
                mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
        }
    
        std::vector<std::string> vias;
        QString action;
    
        for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) {
            nhlog::ui()->info("item: {}", item.toStdString());
    
            if (item.startsWith("action=")) {
                action = item.remove("action=");
            } else if (item.startsWith("via=")) {
                vias.push_back(QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
    
        if (sigil1 == "u") {
            if (action.isEmpty()) {
                auto t = view_manager_->rooms()->currentRoom();
                if (t && cache::isRoomMember(mxid1.toStdString(), t->roomId().toStdString())) {
                    t->openUserProfile(mxid1);
    
                    return true;
    
                }
                emit view_manager_->openGlobalUserProfile(mxid1);
            } else if (action == "chat") {
                this->startChat(mxid1);
            }
            return true;
        } else if (sigil1 == "roomid") {
            auto joined_rooms = cache::joinedRooms();
            auto targetRoomId = mxid1.toStdString();
    
            for (auto roomid : joined_rooms) {
                if (roomid == targetRoomId) {
                    view_manager_->rooms()->setCurrentRoom(mxid1);
                    if (!mxid2.isEmpty())
                        view_manager_->showEvent(mxid1, mxid2);
                    return true;
                }
            }
    
            if (action == "join" || action.isEmpty()) {
                joinRoomVia(targetRoomId, vias);
                return true;
            }
            return false;
        } else if (sigil1 == "r") {
            auto joined_rooms    = cache::joinedRooms();
            auto targetRoomAlias = mxid1.toStdString();
    
            for (auto roomid : joined_rooms) {
                auto aliases = cache::client()->getRoomAliases(roomid);
                if (aliases) {
                    if (aliases->alias == targetRoomAlias) {
                        view_manager_->rooms()->setCurrentRoom(QString::fromStdString(roomid));
                        if (!mxid2.isEmpty())
                            view_manager_->showEvent(QString::fromStdString(roomid), mxid2);
                        return true;
    
                }
            }
    
            if (action == "join" || action.isEmpty()) {
                joinRoomVia(mxid1.toStdString(), vias);
                return true;
    
            return false;
    
        }
        return false;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    ChatPage::handleMatrixUri(const QUrl &uri)
    {
    
        return handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    bool
    ChatPage::isRoomActive(const QString &room_id)
    {
    
        return isActiveWindow() && currentRoom() == room_id;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    }
    
    QString
    ChatPage::currentRoom() const
    
        if (view_manager_->rooms()->currentRoom())
            return view_manager_->rooms()->currentRoom()->roomId();
        else
            return "";