summary refs log tree commit diff
path: root/src/ChatPage.cc
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2018-06-17 19:18:12 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2018-06-17 19:18:12 +0300
commit8704265978572e60f8b04d89cec2f404f5ea4113 (patch)
treef1e272705c26ce2b0121d6fffccd60f98246b84d /src/ChatPage.cc
parentAdd Visual Studio 2017 support (#336) (diff)
parentUpdate build instructions (diff)
downloadnheko-8704265978572e60f8b04d89cec2f404f5ea4113.tar.xz
Merge branch 'e2ee'
- Support for e2ee rooms
- Implement categories & file logging
- Let the user know when the app can't reach the server (#93)

fixes #13
fixes #326
Diffstat (limited to 'src/ChatPage.cc')
-rw-r--r--src/ChatPage.cc1026
1 files changed, 732 insertions, 294 deletions
diff --git a/src/ChatPage.cc b/src/ChatPage.cc

index 9ae860fb..e543cdf9 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc
@@ -16,15 +16,16 @@ */ #include <QApplication> -#include <QDebug> #include <QSettings> #include <QtConcurrent> #include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" +#include "Logging.hpp" #include "MainWindow.h" #include "MatrixClient.h" +#include "Olm.hpp" #include "OverlayModal.h" #include "QuickSwitcher.h" #include "RoomList.h" @@ -43,13 +44,16 @@ #include "dialogs/ReadReceipts.h" #include "timeline/TimelineViewManager.h" -constexpr int SYNC_RETRY_TIMEOUT = 40 * 1000; -constexpr int INITIAL_SYNC_RETRY_TIMEOUT = 240 * 1000; +// TODO: Needs to be updated with an actual secret. +static const std::string STORAGE_SECRET_KEY("secret"); -ChatPage *ChatPage::instance_ = nullptr; +ChatPage *ChatPage::instance_ = nullptr; +constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; +constexpr size_t MAX_ONETIME_KEYS = 50; ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) : QWidget(parent) + , isConnected_(true) , userSettings_{userSettings} { setObjectName("chatPage"); @@ -78,13 +82,12 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) sidebarActions_ = new SideBarActions(this); connect( sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage); - connect( - sidebarActions_, &SideBarActions::joinRoom, http::client(), &MatrixClient::joinRoom); - connect( - sidebarActions_, &SideBarActions::createRoom, http::client(), &MatrixClient::createRoom); + connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom); + connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom); user_info_widget_ = new UserInfoWidget(sideBar_); room_list_ = new RoomList(userSettings_, sideBar_); + connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom); sideBarLayout_->addWidget(user_info_widget_); sideBarLayout_->addWidget(room_list_); @@ -107,6 +110,11 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) contentLayout_->addWidget(top_bar_); contentLayout_->addWidget(view_manager_); + connect(this, + &ChatPage::removeTimelineEvent, + view_manager_, + &TimelineViewManager::removeTimelineEvent); + // Splitter splitter->addWidget(sideBar_); splitter->addWidget(content_); @@ -120,16 +128,82 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) typingRefresher_ = new QTimer(this); typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); + connect(this, &ChatPage::connectionLost, this, [this]() { + nhlog::net()->info("connectivity lost"); + isConnected_ = false; + http::v2::client()->shutdown(); + text_input_->disableInput(); + }); + connect(this, &ChatPage::connectionRestored, this, [this]() { + nhlog::net()->info("trying to re-connect"); + text_input_->enableInput(); + isConnected_ = true; + + // Drop all pending connections. + http::v2::client()->shutdown(); + trySync(); + }); + + connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); + connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { + if (http::v2::client()->access_token().empty()) { + connectivityTimer_.stop(); + return; + } + + http::v2::client()->versions( + [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + emit connectionLost(); + return; + } + + if (!isConnected_) + emit connectionRestored(); + }); + }); + + connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); connect(user_info_widget_, &UserInfoWidget::logout, this, [this]() { - http::client()->logout(); + http::v2::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(); }); - connect(http::client(), &MatrixClient::loggedOut, this, &ChatPage::logout); connect(top_bar_, &TopRoomBar::inviteUsers, this, [this](QStringList users) { + const auto room_id = current_room_.toStdString(); + for (int ii = 0; ii < users.size(); ++ii) { - QTimer::singleShot(ii * 1000, this, [this, ii, users]() { - http::client()->inviteUser(current_room_, users.at(ii)); + QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { + const auto user = users.at(ii); + + http::v2::client()->invite_user( + room_id, + user.toStdString(), + [this, user](const mtx::responses::RoomInvite &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + QString("Failed to invite user: %1").arg(user)); + return; + } + + emit showNotification( + QString("Invited user: %1").arg(user)); + }); }); } }); @@ -155,36 +229,34 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) { view_manager_->addRoom(room_id); - http::client()->joinRoom(room_id); + joinRoom(room_id); room_list_->removeRoom(room_id, currentRoom() == room_id); }); connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) { - http::client()->leaveRoom(room_id); + leaveRoom(room_id); room_list_->removeRoom(room_id, currentRoom() == room_id); }); - connect(text_input_, &TextInputWidget::startedTyping, this, [this]() { - if (!userSettings_->isTypingNotificationsEnabled()) - return; - - typingRefresher_->start(); - http::client()->sendTypingNotification(current_room_); - }); - + connect( + text_input_, &TextInputWidget::startedTyping, this, &ChatPage::sendTypingNotifications); + connect(typingRefresher_, &QTimer::timeout, this, &ChatPage::sendTypingNotifications); connect(text_input_, &TextInputWidget::stoppedTyping, this, [this]() { if (!userSettings_->isTypingNotificationsEnabled()) return; typingRefresher_->stop(); - http::client()->removeTypingNotification(current_room_); - }); - connect(typingRefresher_, &QTimer::timeout, this, [this]() { - if (!userSettings_->isTypingNotificationsEnabled()) + if (current_room_.isEmpty()) return; - http::client()->sendTypingNotification(current_room_); + http::v2::client()->stop_typing( + current_room_.toStdString(), [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to stop typing notifications: {}", + err->matrix_error.error); + } + }); }); connect(view_manager_, @@ -207,142 +279,242 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) view_manager_, SLOT(queueEmoteMessage(const QString &))); - connect(text_input_, - &TextInputWidget::sendJoinRoomRequest, - http::client(), - &MatrixClient::joinRoom); + connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom); connect(text_input_, &TextInputWidget::uploadImage, this, - [this](QSharedPointer<QIODevice> data, const QString &fn) { - http::client()->uploadImage(current_room_, fn, data); + [this](QSharedPointer<QIODevice> dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::v2::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload image. Please try again.")); + nhlog::net()->warn("failed to upload image: {} ({})", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + emit imageUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); }); connect(text_input_, &TextInputWidget::uploadFile, this, - [this](QSharedPointer<QIODevice> data, const QString &fn) { - http::client()->uploadFile(current_room_, fn, data); + [this](QSharedPointer<QIODevice> dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::v2::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload file. Please try again.")); + nhlog::net()->warn("failed to upload file: {} ({})", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + emit fileUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); }); connect(text_input_, &TextInputWidget::uploadAudio, this, - [this](QSharedPointer<QIODevice> data, const QString &fn) { - http::client()->uploadAudio(current_room_, fn, data); + [this](QSharedPointer<QIODevice> dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::v2::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload audio. Please try again.")); + nhlog::net()->warn("failed to upload audio: {} ({})", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + emit audioUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); }); connect(text_input_, &TextInputWidget::uploadVideo, this, - [this](QSharedPointer<QIODevice> data, const QString &fn) { - http::client()->uploadVideo(current_room_, fn, data); + [this](QSharedPointer<QIODevice> dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::v2::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload video. Please try again.")); + nhlog::net()->warn("failed to upload video: {} ({})", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + emit videoUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); }); - connect( - http::client(), &MatrixClient::roomCreationFailed, this, &ChatPage::showNotification); - connect(http::client(), &MatrixClient::joinFailed, this, &ChatPage::showNotification); - connect(http::client(), &MatrixClient::uploadFailed, this, [this](int, const QString &msg) { + connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { text_input_->hideUploadSpinner(); emit showNotification(msg); }); - connect( - http::client(), - &MatrixClient::imageUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueImageMessage(roomid, filename, url, mime, dsize); - }); - connect( - http::client(), - &MatrixClient::fileUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); - }); - connect( - http::client(), - &MatrixClient::audioUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); - }); - connect( - http::client(), - &MatrixClient::videoUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); - }); - - connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); - - connect(http::client(), - &MatrixClient::initialSyncCompleted, - this, - &ChatPage::initialSyncCompleted); - connect( - http::client(), &MatrixClient::initialSyncFailed, this, &ChatPage::retryInitialSync); - connect(http::client(), &MatrixClient::syncCompleted, this, &ChatPage::syncCompleted); - connect(http::client(), - &MatrixClient::getOwnProfileResponse, + connect(this, + &ChatPage::imageUploaded, this, - &ChatPage::updateOwnProfileInfo); - connect(http::client(), - SIGNAL(getOwnCommunitiesResponse(QList<QString>)), + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueImageMessage(roomid, filename, url, mime, dsize); + }); + connect(this, + &ChatPage::fileUploaded, this, - SLOT(updateOwnCommunitiesInfo(QList<QString>))); - connect(http::client(), - &MatrixClient::communityProfileRetrieved, + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); + }); + connect(this, + &ChatPage::audioUploaded, this, - [this](QString communityId, QJsonObject profile) { - communities_[communityId]->parseProfile(profile); + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); }); - connect(http::client(), - &MatrixClient::communityRoomsRetrieved, + connect(this, + &ChatPage::videoUploaded, this, - [this](QString communityId, QJsonObject rooms) { - communities_[communityId]->parseRooms(rooms); - - if (communityId == current_community_) { - if (communityId == "world") { - room_list_->setFilterRooms(false); - } else { - room_list_->setRoomFilter( - communities_[communityId]->getRoomList()); - } - } + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); }); - connect(http::client(), &MatrixClient::joinedRoom, this, [this](const QString &room_id) { - emit showNotification("You joined the room."); + connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); - // We remove any invites with the same room_id. - try { - cache::client()->removeInvite(room_id.toStdString()); - } catch (const lmdb::error &e) { - emit showNotification(QString("Failed to remove invite: %1") - .arg(QString::fromStdString(e.what()))); - } - }); - connect(http::client(), &MatrixClient::leftRoom, this, &ChatPage::removeRoom); - connect(http::client(), &MatrixClient::invitedUser, this, [this](QString, QString user) { - emit showNotification(QString("Invited user %1").arg(user)); - }); - connect(http::client(), &MatrixClient::roomCreated, this, [this](QString room_id) { - emit showNotification(QString("Room %1 created").arg(room_id)); - }); - connect(http::client(), &MatrixClient::redactionFailed, this, [this](const QString &error) { - emit showNotification(QString("Message redaction failed: %1").arg(error)); - }); - connect(http::client(), - &MatrixClient::notificationsRetrieved, - this, - &ChatPage::sendDesktopNotifications); + // connect(http::client(), + // SIGNAL(getOwnCommunitiesResponse(QList<QString>)), + // this, + // SLOT(updateOwnCommunitiesInfo(QList<QString>))); + // connect(http::client(), + // &MatrixClient::communityProfileRetrieved, + // this, + // [this](QString communityId, QJsonObject profile) { + // communities_[communityId]->parseProfile(profile); + // }); + // connect(http::client(), + // &MatrixClient::communityRoomsRetrieved, + // this, + // [this](QString communityId, QJsonObject rooms) { + // communities_[communityId]->parseRooms(rooms); + + // if (communityId == current_community_) { + // if (communityId == "world") { + // room_list_->setFilterRooms(false); + // } else { + // room_list_->setRoomFilter( + // communities_[communityId]->getRoomList()); + // } + // } + // }); + + connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); + connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendDesktopNotifications); showContentTimer_ = new QTimer(this); showContentTimer_->setSingleShot(true); @@ -361,20 +533,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) } }); - initialSyncTimer_ = new QTimer(this); - connect(initialSyncTimer_, &QTimer::timeout, this, [this]() { retryInitialSync(); }); - - syncTimeoutTimer_ = new QTimer(this); - connect(syncTimeoutTimer_, &QTimer::timeout, this, [this]() { - if (http::client()->getHomeServer().isEmpty()) { - syncTimeoutTimer_->stop(); - return; - } - - qDebug() << "Sync took too long. Retrying..."; - http::client()->sync(); - }); - connect(communitiesList_, &CommunitiesList::communityChanged, this, @@ -394,12 +552,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) this, &ChatPage::setGroupViewState); - connect(this, &ChatPage::continueSync, this, [this](const QString &next_batch) { - syncTimeoutTimer_->start(SYNC_RETRY_TIMEOUT); - http::client()->setNextBatchToken(next_batch); - http::client()->sync(); - }); - connect(this, &ChatPage::startConsesusTimer, this, [this]() { consensusTimer_->start(CONSENSUS_TIMEOUT); showContentTimer_->start(SHOW_CONTENT_TIMEOUT); @@ -418,7 +570,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) try { room_list_->cleanupInvites(cache::client()->invites()); } catch (const lmdb::error &e) { - qWarning() << "failed to retrieve invites" << e.what(); + nhlog::db()->error("failed to retrieve invites: {}", e.what()); } view_manager_->initialize(rooms); @@ -437,7 +589,20 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) } if (hasNotifications) - http::client()->getNotifications(); + http::v2::client()->notifications( + 5, + [this](const mtx::responses::Notifications &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to retrieve notifications: {} ({})", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + emit notificationsRetrieved(std::move(res)); + }); }); connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync); connect( @@ -446,12 +611,24 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) changeTopRoomInfo(currentRoom()); }); - instance_ = this; + // Callbacks to update the user info (top left corner of the page). + connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar); + connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) { + QSettings settings; + auto userid = settings.value("auth/user_id").toString(); + user_info_widget_->setUserId(userid); + user_info_widget_->setDisplayName(name); + }); + + connect(this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync); + connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync); + connect(this, &ChatPage::tryDelayedSyncCb, this, [this]() { + QTimer::singleShot(5000, this, &ChatPage::trySync); + }); + + connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); - qRegisterMetaType<std::map<QString, RoomInfo>>(); - qRegisterMetaType<QMap<QString, RoomInfo>>(); - qRegisterMetaType<mtx::responses::Rooms>(); - qRegisterMetaType<std::vector<std::string>>(); + instance_ = this; } void @@ -462,6 +639,19 @@ ChatPage::logout() resetUI(); emit closing(); + connectivityTimer_.stop(); +} + +void +ChatPage::dropToLoginPage(const QString &msg) +{ + deleteConfigs(); + resetUI(); + + http::v2::client()->shutdown(); + connectivityTimer_.stop(); + + emit showLoginPage(msg); } void @@ -490,90 +680,71 @@ ChatPage::deleteConfigs() settings.endGroup(); cache::client()->deleteData(); - - http::client()->reset(); + http::v2::client()->clear(); } void ChatPage::bootstrap(QString userid, QString homeserver, QString token) { - http::client()->setServer(homeserver); - http::client()->setAccessToken(token); - http::client()->getOwnProfile(); - http::client()->getOwnCommunities(); + using namespace mtx::identifiers; + + try { + http::v2::client()->set_user(parse<User>(userid.toStdString())); + } catch (const std::invalid_argument &e) { + nhlog::ui()->critical("bootstrapped with invalid user_id: {}", + userid.toStdString()); + } + + http::v2::client()->set_server(homeserver.toStdString()); + http::v2::client()->set_access_token(token.toStdString()); - cache::init(userid); + // The Olm client needs the user_id & device_id that will be included + // in the generated payloads & keys. + olm::client()->set_user_id(http::v2::client()->user_id().to_string()); + olm::client()->set_device_id(http::v2::client()->device_id()); try { - cache::client()->setup(); + cache::init(userid); - if (!cache::client()->isFormatValid()) { + const bool isInitialized = cache::client()->isInitialized(); + const bool isValid = cache::client()->isFormatValid(); + + if (isInitialized && !isValid) { + nhlog::db()->warn("breaking changes in cache"); + // TODO: Deleting session data but keep using the + // same device doesn't work. cache::client()->deleteData(); - cache::client()->setup(); - cache::client()->setCurrentFormat(); - } - if (cache::client()->isInitialized()) { + cache::init(userid); + cache::client()->setCurrentFormat(); + } else if (isInitialized) { loadStateFromCache(); return; } } catch (const lmdb::error &e) { - qCritical() << "Cache failure" << e.what(); + nhlog::db()->critical("failure during boot: {}", e.what()); cache::client()->deleteData(); - qInfo() << "Falling back to initial sync ..."; + nhlog::net()->info("falling back to initial sync"); } - http::client()->initialSync(); - - initialSyncTimer_->start(INITIAL_SYNC_RETRY_TIMEOUT); -} - -void -ChatPage::syncCompleted(const mtx::responses::Sync &response) -{ - syncTimeoutTimer_->stop(); - - QtConcurrent::run([this, res = std::move(response)]() { - try { - cache::client()->saveState(res); - emit syncUI(res.rooms); - - auto updates = cache::client()->roomUpdates(res); - - emit syncTopBar(updates); - emit syncRoomlist(updates); - - } catch (const lmdb::error &e) { - std::cout << "save cache error:" << e.what() << '\n'; - // TODO: retry sync. - return; - } - - emit continueSync(cache::client()->nextBatchToken()); - }); -} - -void -ChatPage::initialSyncCompleted(const mtx::responses::Sync &response) -{ - initialSyncTimer_->stop(); - - qDebug() << "initial sync completed"; - - QtConcurrent::run([this, res = std::move(response)]() { - try { - cache::client()->saveState(res); - emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::client()->roomInfo()); - } catch (const lmdb::error &e) { - qWarning() << "cache error:" << QString::fromStdString(e.what()); - emit retryInitialSync(); - return; - } + try { + // It's the first time syncing with this device + // There isn't a saved olm account to restore. + nhlog::crypto()->info("creating new olm account"); + olm::client()->create_new_account(); + cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save olm account {}", e.what()); + emit dropToLoginPageCb(QString::fromStdString(e.what())); + return; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to create new olm account {}", e.what()); + emit dropToLoginPageCb(QString::fromStdString(e.what())); + return; + } - emit continueSync(cache::client()->nextBatchToken()); - emit contentLoaded(); - }); + getProfileInfo(); + tryInitialSync(); } void @@ -586,41 +757,6 @@ ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) } void -ChatPage::updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name) -{ - QSettings settings; - auto userid = settings.value("auth/user_id").toString(); - - user_info_widget_->setUserId(userid); - user_info_widget_->setDisplayName(display_name); - - if (!avatar_url.isValid()) - return; - - if (cache::client()) { - auto data = cache::client()->image(avatar_url.toString()); - if (!data.isNull()) { - user_info_widget_->setAvatar(QImage::fromData(data)); - return; - } - } - - auto proxy = http::client()->fetchUserAvatar(avatar_url); - - if (proxy.isNull()) - return; - - proxy->setParent(this); - connect(proxy.data(), - &DownloadMediaProxy::avatarDownloaded, - this, - [this, proxy](const QImage &img) { - proxy->deleteLater(); - user_info_widget_->setAvatar(img); - }); -} - -void ChatPage::updateOwnCommunitiesInfo(const QList<QString> &own_communities) { for (int i = 0; i < own_communities.size(); i++) { @@ -636,7 +772,7 @@ void ChatPage::changeTopRoomInfo(const QString &room_id) { if (room_id.isEmpty()) { - qWarning() << "can't switch to empty room_id"; + nhlog::ui()->warn("cannot switch to empty room_id"); return; } @@ -660,7 +796,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) top_bar_->updateRoomAvatar(img); } catch (const lmdb::error &e) { - qWarning() << "failed to change top bar room info" << e.what(); + nhlog::ui()->error("failed to change top bar room info: {}", e.what()); } current_room_ = room_id; @@ -681,22 +817,37 @@ ChatPage::showUnreadMessageNotification(int count) void ChatPage::loadStateFromCache() { - qDebug() << "restoring state from cache"; + nhlog::db()->info("restoring state from cache"); + + getProfileInfo(); QtConcurrent::run([this]() { try { + cache::client()->restoreSessions(); + olm::client()->load(cache::client()->restoreOlmAccount(), + STORAGE_SECRET_KEY); + cache::client()->populateMembers(); emit initializeEmptyViews(cache::client()->joinedRooms()); emit initializeRoomList(cache::client()->roomInfo()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); + emit dropToLoginPageCb( + tr("Failed to restore OLM account. Please login again.")); + return; } catch (const lmdb::error &e) { - std::cout << "load cache error:" << e.what() << '\n'; - // TODO Clear cache and restart. + nhlog::db()->critical("failed to restore cache: {}", e.what()); + emit dropToLoginPageCb( + tr("Failed to restore save data. Please login again.")); return; } + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + // Start receiving events. - emit continueSync(cache::client()->nextBatchToken()); + emit trySyncCb(); // Check periodically if the timelines have been loaded. emit startConsesusTimer(); @@ -740,7 +891,7 @@ ChatPage::removeRoom(const QString &room_id) cache::client()->removeRoom(room_id); cache::client()->removeInvite(room_id.toStdString()); } catch (const lmdb::error &e) { - qCritical() << "The cache couldn't be updated: " << e.what(); + nhlog::db()->critical("failure while removing room: {}", e.what()); // TODO: Notify the user. } @@ -824,33 +975,6 @@ ChatPage::setGroupViewState(bool isEnabled) } void -ChatPage::retryInitialSync(int status_code) -{ - initialSyncTimer_->stop(); - - if (http::client()->getHomeServer().isEmpty()) { - deleteConfigs(); - resetUI(); - emit showLoginPage("Sync error. Please try again."); - return; - } - - // Retry on Bad-Gateway & Gateway-Timeout errors - if (status_code == -1 || status_code == 504 || status_code == 502 || status_code == 524) { - qWarning() << "retrying initial sync"; - - http::client()->initialSync(); - initialSyncTimer_->start(INITIAL_SYNC_RETRY_TIMEOUT); - } else { - // Drop into the login screen. - deleteConfigs(); - resetUI(); - - emit showLoginPage(QString("Sync error %1. Please try again.").arg(status_code)); - } -} - -void ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notification_count) { room_list_->updateUnreadMessageCount(room_id, notification_count); @@ -886,7 +1010,321 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) utils::event_body(item.event)); } } catch (const lmdb::error &e) { - qWarning() << e.what(); + nhlog::db()->warn("error while sending desktop notification: {}", e.what()); } } } + +void +ChatPage::tryInitialSync() +{ + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + + // Upload one time keys for the device. + nhlog::crypto()->info("generating one time keys"); + olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS); + + http::v2::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { + if (err) { + const int status_code = static_cast<int>(err->status_code); + nhlog::crypto()->critical("failed to upload one time keys: {} {}", + err->matrix_error.error, + status_code); + // TODO We should have a timeout instead of keeping hammering the server. + emit tryInitialSyncCb(); + return; + } + + olm::client()->mark_keys_as_published(); + for (const auto &entry : res.one_time_key_counts) + nhlog::net()->info( + "uploaded {} {} one-time keys", entry.second, entry.first); + + nhlog::net()->info("trying initial sync"); + + mtx::http::SyncOpts opts; + opts.timeout = 0; + http::v2::client()->sync(opts, + std::bind(&ChatPage::initialSyncHandler, + this, + std::placeholders::_1, + std::placeholders::_2)); + }); +} + +void +ChatPage::trySync() +{ + mtx::http::SyncOpts opts; + + if (!connectivityTimer_.isActive()) + connectivityTimer_.start(); + + try { + opts.since = cache::client()->nextBatchToken(); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); + return; + } + + http::v2::client()->sync( + opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) { + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast<int>(err->status_code); + + nhlog::net()->error("sync error: {} {}", status_code, err_code); + + if (status_code <= 0 || status_code >= 600) { + if (!http::v2::is_logged_in()) + return; + + emit tryDelayedSyncCb(); + return; + } + + switch (status_code) { + case 502: + case 504: + case 524: { + emit trySyncCb(); + return; + } + default: { + if (!http::v2::is_logged_in()) + return; + + if (err->matrix_error.errcode == + mtx::errors::ErrorCode::M_UNKNOWN_TOKEN) + emit dropToLoginPageCb(msg); + else + emit tryDelayedSyncCb(); + + return; + } + } + } + + nhlog::net()->debug("sync completed: {}", res.next_batch); + + // Ensure that we have enough one-time keys available. + ensureOneTimeKeyCount(res.device_one_time_keys_count); + + // TODO: fine grained error handling + try { + cache::client()->saveState(res); + olm::handle_to_device_messages(res.to_device); + + emit syncUI(res.rooms); + + auto updates = cache::client()->roomUpdates(res); + + emit syncTopBar(updates); + emit syncRoomlist(updates); + } catch (const lmdb::error &e) { + nhlog::db()->error("saving sync response: {}", e.what()); + } + + emit trySyncCb(); + }); +} + +void +ChatPage::joinRoom(const QString &room) +{ + const auto room_id = room.toStdString(); + + http::v2::client()->join_room( + room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + QString("Failed to join room: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit showNotification("You joined the room"); + + // We remove any invites with the same room_id. + try { + cache::client()->removeInvite(room_id); + } catch (const lmdb::error &e) { + emit showNotification( + QString("Failed to remove invite: %1").arg(e.what())); + } + }); +} + +void +ChatPage::createRoom(const mtx::requests::CreateRoom &req) +{ + http::v2::client()->create_room( + req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Room creation failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit showNotification(QString("Room %1 created") + .arg(QString::fromStdString(res.room_id.to_string()))); + }); +} + +void +ChatPage::leaveRoom(const QString &room_id) +{ + http::v2::client()->leave_room( + room_id.toStdString(), [this, room_id](const json &, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to leave room: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit leftRoom(room_id); + }); +} + +void +ChatPage::sendTypingNotifications() +{ + if (!userSettings_->isTypingNotificationsEnabled()) + return; + + http::v2::client()->start_typing( + current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send typing notification: {}", + err->matrix_error.error); + } + }); +} + +void +ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err) +{ + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast<int>(err->status_code); + + nhlog::net()->error("sync error: {} {}", status_code, err_code); + + switch (status_code) { + case 502: + case 504: + case 524: { + emit tryInitialSyncCb(); + return; + } + default: { + emit dropToLoginPageCb(msg); + return; + } + } + } + + nhlog::net()->info("initial sync completed"); + + try { + cache::client()->saveState(res); + + olm::handle_to_device_messages(res.to_device); + + emit initializeViews(std::move(res.rooms)); + emit initializeRoomList(cache::client()->roomInfo()); + } catch (const lmdb::error &e) { + nhlog::db()->error("{}", e.what()); + emit tryInitialSyncCb(); + return; + } + + emit trySyncCb(); + emit contentLoaded(); +} + +void +ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts) +{ + for (const auto &entry : counts) { + if (entry.second < MAX_ONETIME_KEYS) { + const int nkeys = MAX_ONETIME_KEYS - entry.second; + + nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first); + olm::client()->generate_one_time_keys(nkeys); + + http::v2::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { + if (err) { + nhlog::crypto()->warn( + "failed to update one-time keys: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + olm::client()->mark_keys_as_published(); + }); + } + } +} + +void +ChatPage::getProfileInfo() +{ + QSettings settings; + const auto userid = settings.value("auth/user_id").toString().toStdString(); + + http::v2::client()->get_profile( + userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve own profile info"); + return; + } + + emit setUserDisplayName(QString::fromStdString(res.display_name)); + + if (cache::client()) { + auto data = cache::client()->image(res.avatar_url); + if (!data.isNull()) { + emit setUserAvatar(QImage::fromData(data)); + return; + } + } + + if (res.avatar_url.empty()) + return; + + http::v2::client()->download( + res.avatar_url, + [this, res](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to download user avatar: {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + if (cache::client()) + cache::client()->saveImage(res.avatar_url, data); + + emit setUserAvatar( + QImage::fromData(QByteArray(data.data(), data.size()))); + }); + }); + // TODO http::client()->getOwnCommunities(); +}