summary refs log tree commit diff
path: root/include
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 /include
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 'include')
-rw-r--r--include/AvatarProvider.h18
-rw-r--r--include/Cache.h150
-rw-r--r--include/ChatPage.h74
-rw-r--r--include/CommunitiesList.h2
-rw-r--r--include/Logging.hpp21
-rw-r--r--include/LoginPage.h33
-rw-r--r--include/MainWindow.h2
-rw-r--r--include/MatrixClient.h291
-rw-r--r--include/Olm.hpp78
-rw-r--r--include/RegisterPage.h5
-rw-r--r--include/RoomList.h2
-rw-r--r--include/TextInputWidget.h10
-rw-r--r--include/dialogs/ReCaptcha.hpp2
-rw-r--r--include/dialogs/RoomSettings.hpp24
-rw-r--r--include/timeline/TimelineItem.h15
-rw-r--r--include/timeline/TimelineView.h121
-rw-r--r--include/timeline/TimelineViewManager.h6
-rw-r--r--include/timeline/widgets/AudioItem.h7
-rw-r--r--include/timeline/widgets/FileItem.h7
-rw-r--r--include/timeline/widgets/ImageItem.h10
20 files changed, 518 insertions, 360 deletions
diff --git a/include/AvatarProvider.h b/include/AvatarProvider.h

index ce82f2aa..4b4e15e9 100644 --- a/include/AvatarProvider.h +++ b/include/AvatarProvider.h
@@ -20,15 +20,17 @@ #include <QImage> #include <functional> -class AvatarProvider : public QObject +class AvatarProxy : public QObject { Q_OBJECT -public: - //! The callback is called with the downloaded avatar for the given user - //! or the avatar is downloaded first and then saved for re-use. - static void resolve(const QString &room_id, - const QString &userId, - QObject *receiver, - std::function<void(QImage)> callback); +signals: + void avatarDownloaded(const QByteArray &data); }; + +using AvatarCallback = std::function<void(QImage)>; + +namespace AvatarProvider { +void +resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb); +} diff --git a/include/Cache.h b/include/Cache.h
index d2574b76..f5a655cf 100644 --- a/include/Cache.h +++ b/include/Cache.h
@@ -17,13 +17,18 @@ #pragma once -#include <QDebug> +#include <boost/optional.hpp> + #include <QDir> #include <QImage> + #include <json.hpp> #include <lmdb++.h> #include <mtx/events/join_rules.hpp> #include <mtx/responses.hpp> +#include <mtxclient/crypto/client.hpp> +#include <mutex> + using mtx::events::state::JoinRule; struct RoomMember @@ -140,6 +145,82 @@ struct RoomSearchResult Q_DECLARE_METATYPE(RoomSearchResult) Q_DECLARE_METATYPE(RoomInfo) +// Extra information associated with an outbound megolm session. +struct OutboundGroupSessionData +{ + std::string session_id; + std::string session_key; + uint64_t message_index = 0; +}; + +inline void +to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) +{ + obj["session_id"] = msg.session_id; + obj["session_key"] = msg.session_key; + obj["message_index"] = msg.message_index; +} + +inline void +from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) +{ + msg.session_id = obj.at("session_id"); + msg.session_key = obj.at("session_key"); + msg.message_index = obj.at("message_index"); +} + +struct OutboundGroupSessionDataRef +{ + OlmOutboundGroupSession *session; + OutboundGroupSessionData data; +}; + +struct DevicePublicKeys +{ + std::string ed25519; + std::string curve25519; +}; + +inline void +to_json(nlohmann::json &obj, const DevicePublicKeys &msg) +{ + obj["ed25519"] = msg.ed25519; + obj["curve25519"] = msg.curve25519; +} + +inline void +from_json(const nlohmann::json &obj, DevicePublicKeys &msg) +{ + msg.ed25519 = obj.at("ed25519"); + msg.curve25519 = obj.at("curve25519"); +} + +//! Represents a unique megolm session identifier. +struct MegolmSessionIndex +{ + //! The room in which this session exists. + std::string room_id; + //! The session_id of the megolm session. + std::string session_id; + //! The curve25519 public key of the sender. + std::string sender_key; + + //! Representation to be used in a hash map. + std::string to_hash() const { return room_id + session_id + sender_key; } +}; + +struct OlmSessionStorage +{ + // Megolm sessions + std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions; + std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions; + std::map<std::string, OutboundGroupSessionData> group_outbound_session_data; + + // Guards for accessing megolm sessions. + std::mutex group_outbound_mtx; + std::mutex group_inbound_mtx; +}; + class Cache : public QObject { Q_OBJECT @@ -192,7 +273,7 @@ public: void saveState(const mtx::responses::Sync &res); bool isInitialized() const; - QString nextBatchToken() const; + std::string nextBatchToken() const; void deleteData(); @@ -206,6 +287,9 @@ public: bool isFormatValid(); void setCurrentFormat(); + //! Retrieve all the user ids from a room. + std::vector<std::string> roomMembers(const std::string &room_id); + //! Check if the given user has power leve greater than than //! lowest power level of the given events. bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, @@ -237,6 +321,7 @@ public: { return image(QString::fromStdString(url)); } + void saveImage(const std::string &url, const std::string &data); void saveImage(const QString &url, const QByteArray &data); RoomInfo singleRoomInfo(const std::string &room_id); @@ -259,6 +344,51 @@ public: //! Check if we have sent a desktop notification for the given event id. bool isNotificationSent(const std::string &event_id); + //! Mark a room that uses e2e encryption. + void setEncryptedRoom(const std::string &room_id); + bool isRoomEncrypted(const std::string &room_id); + + //! Save the public keys for a device. + void saveDeviceKeys(const std::string &device_id); + void getDeviceKeys(const std::string &device_id); + + //! Save the device list for a user. + void setDeviceList(const std::string &user_id, const std::vector<std::string> &devices); + std::vector<std::string> getDeviceList(const std::string &user_id); + + // + // Outbound Megolm Sessions + // + void saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); + OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); + bool outboundMegolmSessionExists(const std::string &room_id) noexcept; + void updateOutboundMegolmSession(const std::string &room_id, int message_index); + + // + // Inbound Megolm Sessions + // + void saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); + OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); + bool inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept; + + // + // Olm Sessions + // + void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); + std::vector<std::string> getOlmSessions(const std::string &curve25519); + boost::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, + const std::string &session_id); + + void saveOlmAccount(const std::string &pickled); + std::string restoreOlmAccount(); + + void restoreSessions(); + + OlmSessionStorage session_storage; + private: //! Save an invited room. void saveInvite(lmdb::txn &txn, @@ -431,6 +561,16 @@ private: return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); } + //! Retrieves or creates the database that stores the open OLM sessions between our device + //! and the given curve25519 key which represents another device. + //! + //! Each entry is a map from the session_id to the pickled representation of the session. + lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) + { + return lmdb::dbi::open( + txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); + } + QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) { if (!event.content.display_name.empty()) @@ -450,6 +590,12 @@ private: lmdb::dbi readReceiptsDb_; lmdb::dbi notificationsDb_; + lmdb::dbi devicesDb_; + lmdb::dbi deviceKeysDb_; + + lmdb::dbi inboundMegolmSessionDb_; + lmdb::dbi outboundMegolmSessionDb_; + QString localUserId_; QString cacheDirectory_; }; diff --git a/include/ChatPage.h b/include/ChatPage.h
index b6c431e4..ffea2914 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h
@@ -17,6 +17,8 @@ #pragma once +#include <atomic> + #include <QFrame> #include <QHBoxLayout> #include <QMap> @@ -27,8 +29,7 @@ #include "Cache.h" #include "CommunitiesList.h" #include "Community.h" - -#include <mtx.hpp> +#include "MatrixClient.h" class OverlayModal; class QuickSwitcher; @@ -50,9 +51,6 @@ constexpr int CONSENSUS_TIMEOUT = 1000; constexpr int SHOW_CONTENT_TIMEOUT = 3000; constexpr int TYPING_REFRESH_TIMEOUT = 10000; -Q_DECLARE_METATYPE(mtx::responses::Rooms) -Q_DECLARE_METATYPE(std::vector<std::string>) - class ChatPage : public QWidget { Q_OBJECT @@ -71,7 +69,37 @@ public: QSharedPointer<UserSettings> userSettings() { return userSettings_; } void deleteConfigs(); +public slots: + void leaveRoom(const QString &room_id); + signals: + void connectionLost(); + void connectionRestored(); + + void notificationsRetrieved(const mtx::responses::Notifications &); + + void uploadFailed(const QString &msg); + void imageUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void fileUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void audioUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void videoUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void contentLoaded(); void closing(); void changeWindowTitle(const QString &msg); @@ -82,30 +110,50 @@ signals: void showOverlayProgressBar(); void startConsesusTimer(); + void removeTimelineEvent(const QString &room_id, const QString &event_id); + + void ownProfileOk(); + void setUserDisplayName(const QString &name); + void setUserAvatar(const QImage &avatar); + void loggedOut(); + + void trySyncCb(); + void tryDelayedSyncCb(); + void tryInitialSyncCb(); + void leftRoom(const QString &room_id); + void initializeRoomList(QMap<QString, RoomInfo>); void initializeViews(const mtx::responses::Rooms &rooms); void initializeEmptyViews(const std::vector<std::string> &rooms); void syncUI(const mtx::responses::Rooms &rooms); - void continueSync(const QString &next_batch); void syncRoomlist(const std::map<QString, RoomInfo> &updates); void syncTopBar(const std::map<QString, RoomInfo> &updates); + void dropToLoginPageCb(const QString &msg); private slots: void showUnreadMessageNotification(int count); void updateTopBarAvatar(const QString &roomid, const QPixmap &img); - void updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name); void updateOwnCommunitiesInfo(const QList<QString> &own_communities); - void initialSyncCompleted(const mtx::responses::Sync &response); - void syncCompleted(const mtx::responses::Sync &response); void changeTopRoomInfo(const QString &room_id); void logout(); void removeRoom(const QString &room_id); - //! Handles initial sync failures. - void retryInitialSync(int status_code = -1); + void dropToLoginPage(const QString &msg); + + void joinRoom(const QString &room); + void createRoom(const mtx::requests::CreateRoom &req); + void sendTypingNotifications(); private: static ChatPage *instance_; + //! Handler callback for initial sync. It doesn't run on the main thread so all + //! communication with the GUI should be done through signals. + void initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err); + void tryInitialSync(); + void trySync(); + void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts); + void getProfileInfo(); + //! Check if the given room is currently open. bool isRoomActive(const QString &room_id) { @@ -161,8 +209,8 @@ private: // Safety net if consensus is not possible or too slow. QTimer *showContentTimer_; QTimer *consensusTimer_; - QTimer *syncTimeoutTimer_; - QTimer *initialSyncTimer_; + QTimer connectivityTimer_; + std::atomic_bool isConnected_; QString current_room_; QString current_community_; diff --git a/include/CommunitiesList.h b/include/CommunitiesList.h
index 3299e7c4..78b9602e 100644 --- a/include/CommunitiesList.h +++ b/include/CommunitiesList.h
@@ -23,12 +23,14 @@ public: signals: void communityChanged(const QString &id); + void avatarRetrieved(const QString &id, const QPixmap &img); public slots: void updateCommunityAvatar(const QString &id, const QPixmap &img); void highlightSelectedCommunity(const QString &id); private: + void fetchCommunityAvatar(const QString &id, const QString &avatarUrl); void addGlobalItem() { addCommunity(QSharedPointer<Community>(new Community), "world"); } //! Check whether or not a community id is currently managed. diff --git a/include/Logging.hpp b/include/Logging.hpp new file mode 100644
index 00000000..2feae60d --- /dev/null +++ b/include/Logging.hpp
@@ -0,0 +1,21 @@ +#pragma once + +#include <memory> +#include <spdlog/spdlog.h> + +namespace nhlog { +void +init(const std::string &file); + +std::shared_ptr<spdlog::logger> +ui(); + +std::shared_ptr<spdlog::logger> +net(); + +std::shared_ptr<spdlog::logger> +db(); + +std::shared_ptr<spdlog::logger> +crypto(); +} diff --git a/include/LoginPage.h b/include/LoginPage.h
index 34a08df9..c52ccaa4 100644 --- a/include/LoginPage.h +++ b/include/LoginPage.h
@@ -28,6 +28,12 @@ class OverlayModal; class RaisedButton; class TextField; +namespace mtx { +namespace responses { +struct Login; +} +} + class LoginPage : public QWidget { Q_OBJECT @@ -42,12 +48,19 @@ signals: void loggingIn(); void errorOccurred(); + //! Used to trigger the corresponding slot outside of the main thread. + void versionErrorCb(const QString &err); + void loginErrorCb(const QString &err); + void versionOkCb(); + + void loginOk(const mtx::responses::Login &res); + protected: void paintEvent(QPaintEvent *event) override; public slots: // Displays errors produced during the login. - void loginError(QString msg) { error_label_->setText(msg); } + void loginError(const QString &msg) { error_label_->setText(msg); } private slots: // Callback for the back button. @@ -63,13 +76,25 @@ private slots: void onServerAddressEntered(); // Callback for errors produced during server probing - void versionError(QString error_message); - + void versionError(const QString &error_message); // Callback for successful server probing - void versionSuccess(); + void versionOk(); private: bool isMatrixIdValid(); + void checkHomeserverVersion(); + std::string initialDeviceName() + { +#if defined(Q_OS_MAC) + return "nheko on macOS"; +#elif defined(Q_OS_LINUX) + return "nheko on Linux"; +#elif defined(Q_OS_WIN) + return "nheko on Windows"; +#else + return "nheko"; +#endif + } QVBoxLayout *top_layout_; diff --git a/include/MainWindow.h b/include/MainWindow.h
index 0fbc7567..b068e8f6 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h
@@ -96,7 +96,7 @@ private slots: void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); } //! Show the chat page and start communicating with the given access token. - void showChatPage(QString user_id, QString home_server, QString token); + void showChatPage(); void showOverlayProgressBar(); void removeOverlayProgressBar(); diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index eae57281..7ea5e0b7 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h
@@ -1,287 +1,28 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - #pragma once -#include <QFileInfo> -#include <QJsonDocument> -#include <QNetworkAccessManager> -#include <QNetworkReply> -#include <QNetworkRequest> -#include <QUrl> -#include <memory> -#include <mtx.hpp> -#include <mtx/errors.hpp> - -class DownloadMediaProxy : public QObject -{ - Q_OBJECT - -signals: - void imageDownloaded(const QPixmap &data); - void fileDownloaded(const QByteArray &data); - void avatarDownloaded(const QImage &img); -}; +#include <QMetaType> -class StateEventProxy : public QObject -{ - Q_OBJECT - -signals: - void stateEventSent(); - void stateEventError(const QString &msg); -}; +#include <mtx/responses.hpp> +#include <mtxclient/http/client.hpp> +Q_DECLARE_METATYPE(mtx::responses::Login) +Q_DECLARE_METATYPE(mtx::responses::Messages) +Q_DECLARE_METATYPE(mtx::responses::Notifications) +Q_DECLARE_METATYPE(mtx::responses::Rooms) Q_DECLARE_METATYPE(mtx::responses::Sync) +Q_DECLARE_METATYPE(std::string) +Q_DECLARE_METATYPE(std::vector<std::string>) -/* - * MatrixClient provides the high level API to communicate with - * a Matrix homeserver. All the responses are returned through signals. - */ -class MatrixClient : public QNetworkAccessManager -{ - Q_OBJECT -public: - MatrixClient(QObject *parent = 0); - - // Client API. - void initialSync() noexcept; - void sync() noexcept; - template<class EventBody, mtx::events::EventType EventT> - std::shared_ptr<StateEventProxy> sendStateEvent(const EventBody &body, - const QString &roomId, - const QString &stateKey = ""); - void sendRoomMessage(mtx::events::MessageType ty, - int txnId, - const QString &roomid, - const QString &msg, - const QString &mime, - uint64_t media_size, - const QString &url = "") noexcept; - void login(const QString &username, const QString &password) noexcept; - void registerUser(const QString &username, - const QString &password, - const QString &server, - const QString &session = "") noexcept; - void versions() noexcept; - void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url); - //! Download user's avatar. - QSharedPointer<DownloadMediaProxy> fetchUserAvatar(const QUrl &avatarUrl); - void fetchCommunityAvatar(const QString &communityId, const QUrl &avatarUrl); - void fetchCommunityProfile(const QString &communityId); - void fetchCommunityRooms(const QString &communityId); - QSharedPointer<DownloadMediaProxy> downloadImage(const QUrl &url); - QSharedPointer<DownloadMediaProxy> downloadFile(const QUrl &url); - void messages(const QString &room_id, const QString &from_token, int limit = 30) noexcept; - void uploadImage(const QString &roomid, - const QString &filename, - const QSharedPointer<QIODevice> data); - void uploadFile(const QString &roomid, - const QString &filename, - const QSharedPointer<QIODevice> data); - void uploadAudio(const QString &roomid, - const QString &filename, - const QSharedPointer<QIODevice> data); - void uploadVideo(const QString &roomid, - const QString &filename, - const QSharedPointer<QIODevice> data); - void uploadFilter(const QString &filter) noexcept; - void joinRoom(const QString &roomIdOrAlias); - void leaveRoom(const QString &roomId); - void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000); - void removeTypingNotification(const QString &roomid); - void readEvent(const QString &room_id, const QString &event_id); - void redactEvent(const QString &room_id, const QString &event_id); - void inviteUser(const QString &room_id, const QString &user); - void createRoom(const mtx::requests::CreateRoom &request); - void getNotifications() noexcept; - - QUrl getHomeServer() { return server_; }; - int transactionId() { return txn_id_; }; - int incrementTransactionId() { return ++txn_id_; }; - - void reset() noexcept; - -public slots: - void getOwnProfile() noexcept; - void getOwnCommunities() noexcept; - void logout() noexcept; - - void setServer(const QString &server) - { - server_ = QUrl(QString("%1://%2").arg(serverProtocol_).arg(server)); - }; - void setAccessToken(const QString &token) { token_ = token; }; - void setNextBatchToken(const QString &next_batch) { next_batch_ = next_batch; }; - -signals: - void loginError(const QString &error); - void registerError(const QString &error); - void registrationFlow(const QString &user, - const QString &pass, - const QString &server, - const QString &session); - void versionError(const QString &error); - - void loggedOut(); - void invitedUser(const QString &room_id, const QString &user); - void roomCreated(const QString &room_id); - - void loginSuccess(const QString &userid, const QString &homeserver, const QString &token); - void registerSuccess(const QString &userid, - const QString &homeserver, - const QString &token); - void versionSuccess(); - void uploadFailed(int statusCode, const QString &msg); - void imageUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size); - void fileUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size); - void audioUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size); - void videoUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size); - void roomAvatarRetrieved(const QString &roomid, - const QPixmap &img, - const QString &url, - const QByteArray &data); - void userAvatarRetrieved(const QString &userId, const QImage &img); - void communityAvatarRetrieved(const QString &communityId, const QPixmap &img); - void communityProfileRetrieved(const QString &communityId, const QJsonObject &profile); - void communityRoomsRetrieved(const QString &communityId, const QJsonObject &rooms); - - // Returned profile data for the user's account. - void getOwnProfileResponse(const QUrl &avatar_url, const QString &display_name); - void getOwnCommunitiesResponse(const QList<QString> &own_communities); - void initialSyncCompleted(const mtx::responses::Sync &response); - void initialSyncFailed(int status_code = -1); - void syncCompleted(const mtx::responses::Sync &response); - void syncFailed(const QString &msg); - void joinFailed(const QString &msg); - void messageSent(const QString &event_id, const QString &roomid, int txn_id); - void messageSendFailed(const QString &roomid, int txn_id); - void emoteSent(const QString &event_id, const QString &roomid, int txn_id); - void messagesRetrieved(const QString &room_id, const mtx::responses::Messages &msgs); - void joinedRoom(const QString &room_id); - void leftRoom(const QString &room_id); - void roomCreationFailed(const QString &msg); - - void redactionFailed(const QString &error); - void redactionCompleted(const QString &room_id, const QString &event_id); - void invalidToken(); - void syncError(const QString &error); - void notificationsRetrieved(const mtx::responses::Notifications &notifications); - -private: - QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev); - QJsonObject getUploadReply(QNetworkReply *reply); - void setupAuth(QNetworkRequest &req) - { - req.setRawHeader("Authorization", QString("Bearer %1").arg(token_).toLocal8Bit()); - } - - // Client API prefix. - QString clientApiUrl_; - - // Media API prefix. - QString mediaApiUrl_; - - // The Matrix server used for communication. - QUrl server_; - - // The access token used for authentication. - QString token_; - - // Increasing transaction ID. - int txn_id_; +namespace http { +namespace v2 { +mtx::http::Client * +client(); - //! Token to be used for the next sync. - QString next_batch_; - //! http or https (default). - QString serverProtocol_; - //! Filter to be send as filter-param for (initial) /sync requests. - QString filter_; -}; +bool +is_logged_in(); +} -namespace http { //! Initialize the http module void init(); - -//! Retrieve the client instance. -MatrixClient * -client(); -} - -template<class EventBody, mtx::events::EventType EventT> -std::shared_ptr<StateEventProxy> -MatrixClient::sendStateEvent(const EventBody &body, const QString &roomId, const QString &stateKey) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/state/%2/%3") - .arg(roomId) - .arg(QString::fromStdString(to_string(EventT))) - .arg(stateKey)); - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - setupAuth(request); - - auto proxy = std::shared_ptr<StateEventProxy>(new StateEventProxy, - [](StateEventProxy *p) { p->deleteLater(); }); - - auto serializedBody = nlohmann::json(body).dump(); - auto reply = put(request, QByteArray(serializedBody.data(), serializedBody.size())); - connect(reply, &QNetworkReply::finished, this, [reply, proxy]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - auto data = reply->readAll(); - - if (status == 0 || status >= 400) { - try { - mtx::errors::Error res = nlohmann::json::parse(data); - emit proxy->stateEventError(QString::fromStdString(res.error)); - } catch (const std::exception &e) { - emit proxy->stateEventError(QString::fromStdString(e.what())); - } - - return; - } - - try { - mtx::responses::EventId res = nlohmann::json::parse(data); - emit proxy->stateEventSent(); - } catch (const std::exception &e) { - emit proxy->stateEventError(QString::fromStdString(e.what())); - } - }); - - return proxy; } diff --git a/include/Olm.hpp b/include/Olm.hpp new file mode 100644
index 00000000..6f871628 --- /dev/null +++ b/include/Olm.hpp
@@ -0,0 +1,78 @@ +#pragma once + +#include <boost/optional.hpp> + +#include <memory> +#include <mtx.hpp> +#include <mtxclient/crypto/client.hpp> + +constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; + +namespace olm { + +struct OlmCipherContent +{ + std::string body; + uint8_t type; +}; + +inline void +from_json(const nlohmann::json &obj, OlmCipherContent &msg) +{ + msg.body = obj.at("body"); + msg.type = obj.at("type"); +} + +struct OlmMessage +{ + std::string sender_key; + std::string sender; + + using RecipientKey = std::string; + std::map<RecipientKey, OlmCipherContent> ciphertext; +}; + +inline void +from_json(const nlohmann::json &obj, OlmMessage &msg) +{ + if (obj.at("type") != "m.room.encrypted") + throw std::invalid_argument("invalid type for olm message"); + + if (obj.at("content").at("algorithm") != OLM_ALGO) + throw std::invalid_argument("invalid algorithm for olm message"); + + msg.sender = obj.at("sender"); + msg.sender_key = obj.at("content").at("sender_key"); + msg.ciphertext = + obj.at("content").at("ciphertext").get<std::map<std::string, OlmCipherContent>>(); +} + +mtx::crypto::OlmClient * +client(); + +void +handle_to_device_messages(const std::vector<nlohmann::json> &msgs); + +boost::optional<json> +try_olm_decryption(const std::string &sender_key, const OlmCipherContent &content); + +void +handle_olm_message(const OlmMessage &msg); + +//! Establish a new inbound megolm session with the decrypted payload from olm. +void +create_inbound_megolm_session(const std::string &sender, + const std::string &sender_key, + const nlohmann::json &payload); + +void +handle_pre_key_olm_message(const std::string &sender, + const std::string &sender_key, + const OlmCipherContent &content); + +mtx::events::msg::Encrypted +encrypt_group_message(const std::string &room_id, + const std::string &device_id, + const std::string &body); + +} // namespace olm diff --git a/include/RegisterPage.h b/include/RegisterPage.h
index f4d97816..d02de7c4 100644 --- a/include/RegisterPage.h +++ b/include/RegisterPage.h
@@ -44,6 +44,11 @@ signals: void backButtonClicked(); void errorOccurred(); void registering(); + void registerOk(); + void registerErrorCb(const QString &msg); + void registrationFlow(const std::string &user, + const std::string &pass, + const std::string &session); private slots: void onBackButtonClicked(); diff --git a/include/RoomList.h b/include/RoomList.h
index 98d9443e..59b0e865 100644 --- a/include/RoomList.h +++ b/include/RoomList.h
@@ -60,6 +60,8 @@ signals: void acceptInvite(const QString &room_id); void declineInvite(const QString &room_id); void roomAvatarChanged(const QString &room_id, const QPixmap &img); + void joinRoom(const QString &room_id); + void updateRoomAvatarCb(const QString &room_id, const QPixmap &img); public slots: void updateRoomAvatar(const QString &roomid, const QPixmap &img); diff --git a/include/TextInputWidget.h b/include/TextInputWidget.h
index c679b9b2..af58c2c3 100644 --- a/include/TextInputWidget.h +++ b/include/TextInputWidget.h
@@ -129,6 +129,16 @@ public: QColor borderColor() const { return borderColor_; } void setBorderColor(QColor &color) { borderColor_ = color; } + void disableInput() + { + input_->setEnabled(false); + input_->setPlaceholderText(tr("Connection lost. Nheko is trying to re-connect...")); + } + void enableInput() + { + input_->setEnabled(true); + input_->setPlaceholderText(tr("Write a message...")); + } public slots: void openFileSelection(); diff --git a/include/dialogs/ReCaptcha.hpp b/include/dialogs/ReCaptcha.hpp
index 1eda40c7..5f47b0eb 100644 --- a/include/dialogs/ReCaptcha.hpp +++ b/include/dialogs/ReCaptcha.hpp
@@ -12,7 +12,7 @@ class ReCaptcha : public QWidget Q_OBJECT public: - ReCaptcha(const QString &server, const QString &session, QWidget *parent = nullptr); + ReCaptcha(const QString &session, QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; diff --git a/include/dialogs/RoomSettings.hpp b/include/dialogs/RoomSettings.hpp
index 375a531e..6cab03b7 100644 --- a/include/dialogs/RoomSettings.hpp +++ b/include/dialogs/RoomSettings.hpp
@@ -5,16 +5,17 @@ #include "Cache.h" +class Avatar; class FlatButton; -class TextField; +class QComboBox; class QHBoxLayout; -class Avatar; -class QPixmap; -class QLayout; class QLabel; -class QComboBox; -class TextField; class QLabel; +class QLayout; +class QPixmap; +class TextField; +class TextField; +class Toggle; template<class T> class QSharedPointer; @@ -30,6 +31,9 @@ public: signals: void nameChanged(const QString &roomName); + void nameEventSentCb(const QString &newName); + void topicEventSentCb(); + void stateEventErrorCb(const QString &msg); private: QString roomId_; @@ -81,6 +85,7 @@ public: signals: void closing(); + void enableEncryptionError(const QString &msg); protected: void paintEvent(QPaintEvent *event) override; @@ -95,13 +100,15 @@ private: void setupEditButton(); //! Retrieve the current room information from cache. void retrieveRoomInfo(); + void enableEncryption(); //! Whether the user would be able to change the name or the topic of the room. - bool hasEditRights_ = true; + bool hasEditRights_ = true; + bool usesEncryption_ = false; QHBoxLayout *editLayout_; // Button section - FlatButton *saveBtn_; + FlatButton *okBtn_; FlatButton *cancelBtn_; FlatButton *editFieldsBtn_; @@ -113,6 +120,7 @@ private: TopSection *topSection_; QComboBox *accessCombo; + Toggle *encryptionToggle_; }; } // dialogs diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h
index 9997ec1d..f055f217 100644 --- a/include/timeline/TimelineItem.h +++ b/include/timeline/TimelineItem.h
@@ -193,16 +193,17 @@ public: QString eventId() const { return event_id_; } void setEventId(const QString &event_id) { event_id_ = event_id; } void markReceived(); + bool isReceived() { return isReceived_; }; void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const - { - if (!event_id_.isEmpty()) - http::client()->readEvent(room_id_, event_id_); - } + void sendReadReceipt() const; //! Add a user avatar for this event. void addAvatar(); +signals: + void eventRedacted(const QString &event_id); + void redactionFailed(const QString &msg); + protected: void paintEvent(QPaintEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override; @@ -225,6 +226,10 @@ private: void setupAvatarLayout(const QString &userName); void setupSimpleLayout(); + //! Whether or not the event associated with the widget + //! has been acknowledged by the server. + bool isReceived_ = false; + QString replaceEmoji(const QString &body); QString event_id_; QString room_id_; diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h
index e6e35ccb..a86c0286 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h
@@ -18,7 +18,6 @@ #pragma once #include <QApplication> -#include <QDebug> #include <QLayout> #include <QList> #include <QQueue> @@ -34,6 +33,19 @@ #include "ScrollBar.h" #include "TimelineItem.h" +class StateKeeper +{ +public: + StateKeeper(std::function<void()> &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function<void()> fn_; +}; + class FloatingButton; struct DescInfo; @@ -42,33 +54,44 @@ struct DescInfo; struct PendingMessage { mtx::events::MessageType ty; - int txn_id; + std::string txn_id; QString body; QString filename; QString mime; uint64_t media_size; QString event_id; TimelineItem *widget; - - PendingMessage(mtx::events::MessageType ty, - int txn_id, - QString body, - QString filename, - QString mime, - uint64_t media_size, - QString event_id, - TimelineItem *widget) - : ty(ty) - , txn_id(txn_id) - , body(body) - , filename(filename) - , mime(mime) - , media_size(media_size) - , event_id(event_id) - , widget(widget) - {} + bool is_encrypted = false; }; +template<class MessageT> +MessageT +toRoomMessage(const PendingMessage &) = delete; + +template<> +mtx::events::msg::Audio +toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m); + +template<> +mtx::events::msg::Emote +toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m); + +template<> +mtx::events::msg::File +toRoomMessage<mtx::events::msg::File>(const PendingMessage &); + +template<> +mtx::events::msg::Image +toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m); + +template<> +mtx::events::msg::Text +toRoomMessage<mtx::events::msg::Text>(const PendingMessage &); + +template<> +mtx::events::msg::Video +toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m); + // In which place new TimelineItems should be inserted. enum class TimelineDirection { @@ -129,7 +152,7 @@ public: const QString &filename, const QString &mime, uint64_t size); - void updatePendingMessage(int txn_id, QString event_id); + void updatePendingMessage(const std::string &txn_id, const QString &event_id); void scrollDown(); QLabel *createDateSeparator(QDateTime datetime); @@ -142,18 +165,21 @@ public slots: void fetchHistory(); // Add old events at the top of the timeline. - void addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs); + void addBackwardsEvents(const mtx::responses::Messages &msgs); // Whether or not the initial batch has been loaded. bool hasLoaded() { return scroll_layout_->count() > 1 || isTimelineFinished; } - void handleFailedMessage(int txnid); + void handleFailedMessage(const std::string &txn_id); private slots: void sendNextPendingMessage(); signals: void updateLastTimelineMessage(const QString &user, const DescInfo &info); + void messagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(const std::string &txn_id); + void messageSent(const std::string &txn_id, const QString &event_id); protected: void paintEvent(QPaintEvent *event) override; @@ -165,6 +191,25 @@ private: QWidget *relativeWidget(TimelineItem *item, int dt) const; + TimelineEvent parseEncryptedEvent( + const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e); + + void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, + const std::string &room_key, + const DevicePublicKeys &pks, + const std::string &user_id, + const std::string &device_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + + //! Callback for all message sending. + void sendRoomMessageHandler(const std::string &txn_id, + const mtx::responses::EventId &res, + mtx::http::RequestErr err); + void prepareEncryptedMessage(const PendingMessage &msg); + + //! Call the /messages endpoint to fill the timeline. + void getMessages(); //! HACK: Fixing layout flickering when adding to the bottom //! of the timeline. void pushTimelineItem(TimelineItem *item) @@ -230,8 +275,10 @@ private: uint64_t origin_server_ts, TimelineDirection direction); - bool isPendingMessage(const QString &txnid, const QString &sender, const QString &userid); - void removePendingMessage(const QString &txnid); + bool isPendingMessage(const std::string &txn_id, + const QString &sender, + const QString &userid); + void removePendingMessage(const std::string &txn_id); bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } @@ -315,14 +362,18 @@ TimelineView::addUserMessage(const QString &url, lastMessageDirection_ = TimelineDirection::Bottom; - QApplication::processEvents(); - // Keep track of the sender and the timestamp of the current message. saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - int txn_id = http::client()->incrementTransactionId(); + PendingMessage message; + message.ty = MsgType; + message.txn_id = http::v2::client()->generate_txn_id(); + message.body = url; + message.filename = trimmed; + message.mime = mime; + message.media_size = size; + message.widget = view_item; - PendingMessage message(MsgType, txn_id, url, trimmed, mime, size, "", view_item); handleNewUserMessage(message); } @@ -351,10 +402,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio const auto event_id = QString::fromStdString(event.event_id); const auto sender = QString::fromStdString(event.sender); - const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id); - if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) || + const auto txn_id = event.unsigned_data.transaction_id; + if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || isDuplicate(event_id)) { - removePendingMessage(txnid); + removePendingMessage(txn_id); return nullptr; } @@ -376,10 +427,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio const auto event_id = QString::fromStdString(event.event_id); const auto sender = QString::fromStdString(event.sender); - const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id); - if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) || + const auto txn_id = event.unsigned_data.transaction_id; + if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || isDuplicate(event_id)) { - removePendingMessage(txnid); + removePendingMessage(txn_id); return nullptr; } diff --git a/include/timeline/TimelineViewManager.h b/include/timeline/TimelineViewManager.h
index 308b83aa..9e31ecbf 100644 --- a/include/timeline/TimelineViewManager.h +++ b/include/timeline/TimelineViewManager.h
@@ -56,6 +56,8 @@ signals: void updateRoomsLastMessage(const QString &user, const DescInfo &info); public slots: + void removeTimelineEvent(const QString &room_id, const QString &event_id); + void setHistoryView(const QString &room_id); void queueTextMessage(const QString &msg); void queueEmoteMessage(const QString &msg); @@ -80,10 +82,6 @@ public slots: const QString &mime, uint64_t dsize); -private slots: - void messageSent(const QString &eventid, const QString &roomid, int txnid); - void messageSendFailed(const QString &roomid, int txnid); - private: //! Check if the given room id is managed by a TimelineView. bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } diff --git a/include/timeline/widgets/AudioItem.h b/include/timeline/widgets/AudioItem.h
index b31385d1..7b0781a2 100644 --- a/include/timeline/widgets/AudioItem.h +++ b/include/timeline/widgets/AudioItem.h
@@ -69,9 +69,14 @@ protected: void resizeEvent(QResizeEvent *event) override; void mousePressEvent(QMouseEvent *event) override; +signals: + void fileDownloadedCb(const QByteArray &data); + +private slots: + void fileDownloaded(const QByteArray &data); + private: void init(); - void fileDownloaded(const QByteArray &data); enum class AudioState { diff --git a/include/timeline/widgets/FileItem.h b/include/timeline/widgets/FileItem.h
index 09181d32..66543e79 100644 --- a/include/timeline/widgets/FileItem.h +++ b/include/timeline/widgets/FileItem.h
@@ -52,15 +52,20 @@ public: QColor iconColor() const { return iconColor_; } QColor backgroundColor() const { return backgroundColor_; } +signals: + void fileDownloadedCb(const QByteArray &data); + protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; +private slots: + void fileDownloaded(const QByteArray &data); + private: void openUrl(); void init(); - void fileDownloaded(const QByteArray &data); QUrl url_; QString text_; diff --git a/include/timeline/widgets/ImageItem.h b/include/timeline/widgets/ImageItem.h
index b17b2d8b..e9d823f4 100644 --- a/include/timeline/widgets/ImageItem.h +++ b/include/timeline/widgets/ImageItem.h
@@ -40,13 +40,17 @@ public: uint64_t size, QWidget *parent = nullptr); - void setImage(const QPixmap &image); - QSize sizeHint() const override; public slots: //! Show a save as dialog for the image. void saveAs(); + void setImage(const QPixmap &image); + void saveImage(const QString &filename, const QByteArray &data); + +signals: + void imageDownloaded(const QPixmap &img); + void imageSaved(const QString &filename, const QByteArray &data); protected: void paintEvent(QPaintEvent *event) override; @@ -57,7 +61,9 @@ protected: bool isInteractive_ = true; private: + void init(); void openUrl(); + void downloadMedia(const QUrl &url); int max_width_ = 500; int max_height_ = 300;