diff --git a/CMakeLists.txt b/CMakeLists.txt old mode 100644 new mode 100755 index abe86da..38c9153 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,10 @@ cmake_minimum_required(VERSION 2.8.12) project(Trinity LANGUAGES CXX) -set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(Qt5 COMPONENTS Core Quick Widgets REQUIRED) +find_package(Qt5 COMPONENTS Core Quick Widgets WebEngine REQUIRED) add_executable(${PROJECT_NAME} src/main.cpp @@ -29,9 +28,14 @@ add_executable(${PROJECT_NAME} include/roomlistsortmodel.h include/emotelistmodel.h src/emotelistmodel.cpp - include/emote.h) + include/emote.h + include/appcore.h + include/encryption.h + src/encryption.cpp) -target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Quick Qt5::Widgets cmark) +find_package(Olm REQUIRED) + +target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Quick Qt5::Widgets Qt5::WebEngine Olm::Olm cmark) target_include_directories(${PROJECT_NAME} PRIVATE include) install(TARGETS ${PROJECT_NAME} DESTINATION bin) diff --git a/include/appcore.h b/include/appcore.h new file mode 100755 index 0000000..20e2410 --- /dev/null +++ b/include/appcore.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +class MatrixCore; + +class AppCore : public QObject { + Q_OBJECT + Q_PROPERTY(QVariantList accounts READ getAccounts NOTIFY accountChange) +public: + Q_INVOKABLE void addAccount(QString profileName = ""); + + Q_INVOKABLE void switchAccount(QString profileName); + + Q_INVOKABLE QVariantList getAccounts(); + + QList accounts; + + QQmlContext* context = nullptr; + +signals: + void accountChange(); +}; diff --git a/include/desktop.h b/include/desktop.h old mode 100644 new mode 100755 index 7722d2f..fcc3bf1 --- a/include/desktop.h +++ b/include/desktop.h @@ -11,14 +11,18 @@ public: QApplication::setQuitOnLastWindowClosed(shouldHide); if(shouldHide) - icon->hide(); - else icon->show(); + else + icon->hide(); } Q_INVOKABLE void showMessage(const QString title, const QString content) { icon->showMessage(title, content); } + Q_INVOKABLE bool isTrayIconEnabled() { + return icon->isVisible();; + } + QSystemTrayIcon* icon; }; diff --git a/include/encryption.h b/include/encryption.h new file mode 100755 index 0000000..a31b1ca --- /dev/null +++ b/include/encryption.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include + +class Encryption { +public: + void createNewDeviceKeys(); + QJsonObject generateOneTimeKeys(int number); + int getRecommendedNumberOfOneTimeKeys(); + + QString saveDeviceKeys(); + void loadDeviceKeys(QString bytes); + + OlmOutboundGroupSession* beginOutboundSession(); + std::string getGroupSessionId(OlmOutboundGroupSession* session); + std::string getGroupSessionKey(OlmOutboundGroupSession* session); + OlmSession* beginOutboundOlmSession(std::string identityKey, std::string oneTimeKey); + std::string getSessionId(OlmSession* session); + std::string encrypt(OlmSession* session, std::string message); + std::string encryptGroup(OlmOutboundGroupSession* session, std::string message); + + QString saveSession(OlmOutboundGroupSession* session); + OlmOutboundGroupSession* loadSession(QString bytes); + + // inbound messaging + OlmSession* createInboundSession(std::string senderKey, std::string body); + std::vector decrypt(OlmSession* session, int msgType, std::string cipherText); + + OlmInboundGroupSession* beginInboundSession(std::string sessionKey); + std::vector decrypt(OlmInboundGroupSession* session, std::string cipherText); + + QString signMessage(QString message); + + QJsonObject identityKey; + +private: + void initAccount(); + + OlmAccount* account = nullptr; +}; diff --git a/include/matrixcore.h b/include/matrixcore.h old mode 100644 new mode 100755 index 79c0747..65eddcb --- a/include/matrixcore.h +++ b/include/matrixcore.h @@ -12,16 +12,21 @@ #include "roomlistsortmodel.h" #include "emote.h" #include "emotelistmodel.h" +#include "encryption.h" + +class Network; class MatrixCore : public QObject { Q_OBJECT + Q_PROPERTY(QString profileName MEMBER profileName CONSTANT) + Q_PROPERTY(bool initialSyncComplete READ isInitialSyncComplete NOTIFY initialSyncFinished) Q_PROPERTY(EventModel* eventModel READ getEventModel NOTIFY currentRoomChanged) Q_PROPERTY(RoomListSortModel* roomListModel READ getRoomListModel NOTIFY roomListChanged) Q_PROPERTY(Room* currentRoom READ getCurrentRoom NOTIFY currentRoomChanged) Q_PROPERTY(QList rooms MEMBER rooms NOTIFY roomListChanged) Q_PROPERTY(QString homeserverURL READ getHomeserverURL NOTIFY homeserverChanged) - Q_PROPERTY(MemberModel* memberModel READ getMemberModel NOTIFY currentRoomChanged) + Q_PROPERTY(MemberListSortModel* memberModel READ getMemberModel NOTIFY currentRoomChanged) Q_PROPERTY(QString displayName READ getDisplayName NOTIFY displayNameChanged) Q_PROPERTY(QVariantList joinedCommunities READ getJoinedCommunitiesList NOTIFY joinedCommunitiesChanged) Q_PROPERTY(RoomListSortModel* publicRooms READ getDirectoryListModel NOTIFY publicRoomsChanged) @@ -29,7 +34,10 @@ class MatrixCore : public QObject Q_PROPERTY(bool markdownEnabled READ getMarkdownEnabled WRITE setMarkdownEnabled NOTIFY markdownEnabledChanged) Q_PROPERTY(EmoteListModel* localEmoteModel READ getLocalEmoteListModel NOTIFY localEmotesChanged) public: - MatrixCore(QObject* parent = nullptr); + MatrixCore(QString profileName, QObject* parent = nullptr); + + Network* network = nullptr; + Encryption* encryption = nullptr; // account Q_INVOKABLE void registerAccount(const QString& username, const QString& password, const QString& session = "", const QString& type = ""); @@ -83,18 +91,29 @@ public: Q_INVOKABLE QString getUsername() const; - Q_INVOKABLE void loadDirectory(); + Q_INVOKABLE void loadDirectory(const QString& homeserver); Q_INVOKABLE void readUpTo(Room* room, const int index); + Q_INVOKABLE bool isInitialSyncComplete(); + void setMarkdownEnabled(const bool enabled); + void sendKeyToDevice(QString roomId, QString senderCurveIdentity, QString senderEdIdentity, QString session_id, QString session_key, QString user_id, QString device_id); + + OlmOutboundGroupSession* currentSession = nullptr; + QString currentSessionId, currentSessionKey; + QString deviceId; + void createOrLoadSession(); + + QMap inboundSessions; + Room* getCurrentRoom(); EventModel* getEventModel(); RoomListSortModel* getRoomListModel(); RoomListSortModel* getDirectoryListModel(); - MemberModel* getMemberModel(); + MemberListSortModel* getMemberModel(); EmoteListModel* getLocalEmoteListModel(); QString getHomeserverURL() const; @@ -109,10 +128,13 @@ public: RoomListModel roomListModel, directoryListModel; RoomListSortModel roomListSortModel, directoryListSortModel; MemberModel memberModel; + MemberListSortModel memberSortModel; EmoteListModel localEmoteModel; Room* currentRoom = nullptr; + QString profileName; + signals: void registerAttempt(bool error, QString description); void registerFlow(QJsonObject data); @@ -132,6 +154,7 @@ signals: private: void consumeEvent(const QJsonObject& event, Room& room, const bool insertFront = true); + void populateEvent(const QJsonObject& event, Event* e); Community* createCommunity(const QString& id); QString getMXCThumbnailURL(QString url); diff --git a/include/membermodel.h b/include/membermodel.h old mode 100644 new mode 100755 index 0aad72e..b0ecb34 --- a/include/membermodel.h +++ b/include/membermodel.h @@ -11,7 +11,8 @@ public: enum EventRoles { DisplayNameRole = Qt::UserRole + 1, AvatarURLRole, - IdRole + IdRole, + SectionRole }; int rowCount(const QModelIndex &parent) const override; diff --git a/include/network.h b/include/network.h old mode 100644 new mode 100755 index 6e88c68..77dd4d9 --- a/include/network.h +++ b/include/network.h @@ -8,9 +8,14 @@ #include "requestsender.h" -namespace network { - extern QNetworkAccessManager* manager; - extern QString homeserverURL, accessToken; +class Network { +public: + Network() { + manager = new QNetworkAccessManager(); + } + + QNetworkAccessManager* manager; + QString homeserverURL, accessToken; template inline void postJSON(const QString& path, const QJsonObject object, Fn&& fn) { @@ -120,4 +125,4 @@ namespace network { manager->get(request); } -} +}; diff --git a/include/requestsender.h b/include/requestsender.h old mode 100644 new mode 100755 index 6e3d9f4..ea04baa --- a/include/requestsender.h +++ b/include/requestsender.h @@ -23,6 +23,8 @@ public: void finished(QNetworkReply* reply) { if(reply->request().originatingObject() == this) { + //qDebug() << reply->errorString(); + fn(reply); deleteLater(); diff --git a/include/room.h b/include/room.h old mode 100644 new mode 100755 index caf2e03..3f62d8a --- a/include/room.h +++ b/include/room.h @@ -7,6 +7,13 @@ #include "community.h" +class EncryptionInformation : public QObject { + Q_OBJECT +public: + QString cipherText; + QString sessionId; +}; + class Event : public QObject { Q_OBJECT @@ -18,10 +25,12 @@ class Event : public QObject Q_PROPERTY(QString thumbnail READ getThumbnail NOTIFY thumbnailChanged) Q_PROPERTY(bool sent READ getSent NOTIFY sentChanged) Q_PROPERTY(double sentProgress READ getSentProgress NOTIFY sentProgressChanged) - Q_PROPERTY(QString eventId MEMBER eventId) + Q_PROPERTY(QString eventId MEMBER eventId NOTIFY msgChanged) public: Event(QObject* parent = nullptr) : QObject(parent) {} + EncryptionInformation* encryptionInfo = nullptr; + void setSender(const QString& id) { sender = id; emit senderChanged(); @@ -203,9 +212,19 @@ class Room : public QObject Q_PROPERTY(QString notificationCount READ getNotificationCount NOTIFY notificationCountChanged) Q_PROPERTY(bool direct READ getDirect NOTIFY directChanged) Q_PROPERTY(int notificationLevel READ getNotificationLevel WRITE setNotificationLevel NOTIFY notificationLevelChanged) + Q_PROPERTY(bool encrypted READ getEncrypted NOTIFY encryptionChanged) public: Room(QObject* parent = nullptr) : QObject(parent) {} + void setEncrypted() { + this->encrypted = true; + emit encryptionChanged(); + } + + bool getEncrypted() const { + return encrypted; + } + void setId(const QString& id) { this->id = id; emit idChanged(); @@ -309,6 +328,7 @@ public: QString prevBatch; QList members; + QMap powerLevelList; private: QString id, name, topic, avatar; @@ -318,6 +338,7 @@ private: unsigned int highlightCount = 0, notificationCount = 0; bool direct = false; int notificationLevel = 1; + bool encrypted = false; signals: void idChanged(); @@ -331,4 +352,5 @@ signals: void notificationCountChanged(); void directChanged(); void notificationLevelChanged(); + void encryptionChanged(); }; diff --git a/include/roomlistsortmodel.h b/include/roomlistsortmodel.h old mode 100644 new mode 100755 index 40d4461..773de93 --- a/include/roomlistsortmodel.h +++ b/include/roomlistsortmodel.h @@ -19,3 +19,20 @@ public: } }; +class MemberListSortModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const; + + Q_INVOKABLE unsigned int getOriginalIndex(const unsigned int i) const { + auto const proxyIndex = index(i, 0); + auto const sourceIndex = mapToSource(proxyIndex); + + if(!sourceIndex.isValid()) + return 0; + else + return sourceIndex.row(); + } +}; + diff --git a/qml.qrc b/qml.qrc old mode 100644 new mode 100755 index b1dfa9d..4553b8f --- a/qml.qrc +++ b/qml.qrc @@ -10,6 +10,8 @@ qml/RoomSettings.qml qml/Profile.qml qml/Community.qml + qml/ToolBarButton.qml + qml/RoundedImage.qml qml/Communities.qml qml/Directory.qml qml/InviteDialog.qml diff --git a/qml/BackButton.qml b/qml/BackButton.qml old mode 100644 new mode 100755 index f59b686..6ca021d --- a/qml/BackButton.qml +++ b/qml/BackButton.qml @@ -27,7 +27,7 @@ Rectangle { text: "ESC" - color: "grey" + color: myPalette.text } Shortcut { diff --git a/qml/Client.qml b/qml/Client.qml old mode 100644 new mode 100755 index 98ee57a..15ac773 --- a/qml/Client.qml +++ b/qml/Client.qml @@ -1,255 +1,403 @@ -import QtQuick 2.10 +import QtQuick 2.15 import QtQuick.Controls 2.3 import QtGraphicalEffects 1.0 import QtQuick.Dialogs 1.2 - +import QtQuick.Layouts 1.1 import trinity.matrix 1.0 Rectangle { id: client - color: Qt.rgba(0.05, 0.05, 0.05, 1.0) + color: myPalette.window property bool shouldScroll: false - ListView { - id: channels - width: 180 - height: parent.height - anchors.right: rightArea.left - anchors.left: client.left + property var openProfile: function(member) { + var popup = Qt.createComponent("qrc:/Profile.qml") + var popupContainer = popup.createObject(window, {"parent": window, "member": member}) - model: matrix.roomListModel + popupContainer.open() + } - section.property: "section" - section.criteria: ViewSection.FullString - section.delegate: Rectangle { - width: parent.width - height: 25 + property var openRoom: function(room) { + var popup = Qt.createComponent("qrc:/RoomSettings.qml") + var popupContainer = popup.createObject(window, {"parent": window, "room": room}) - color: "transparent" + popupContainer.open() + } - Text { - anchors.verticalCenter: parent.verticalCenter + Rectangle { + anchors.fill: parent + visible: !matrix.initialSyncComplete - anchors.left: parent.left - anchors.leftMargin: 5 + z: 1000 - text: section + MouseArea { + anchors.fill: parent + hoverEnabled: true + } - color: Qt.rgba(0.8, 0.8, 0.8, 1.0) + BusyIndicator { + id: syncIndicator - textFormat: Text.PlainText - } - } + anchors.centerIn: parent + } - delegate: Rectangle { - width: parent.width - height: 25 + Text { + anchors.top: syncIndicator.bottom + anchors.horizontalCenter: parent.horizontalCenter - property bool selected: channels.currentIndex === matrix.roomListModel.getOriginalIndex(index) + text: "Synchronizing events..." + } - color: selected ? "white" : "transparent" + Button { + anchors.bottom: parent.bottom + anchors.bottomMargin: 15 + anchors.horizontalCenter: parent.horizontalCenter - radius: 5 + text: "Log out" + } + } - Image { - id: roomAvatar + Rectangle { + id: channels - cache: true + width: 180 - anchors.top: parent.top - anchors.topMargin: 5 - anchors.left: parent.left - anchors.leftMargin: 5 + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom - width: 18 - height: 18 + Rectangle { + id: profileRect - sourceSize.width: 18 - sourceSize.height: 18 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right - source: avatarURL ? avatarURL : "placeholder.png" + height: 45 - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Item { - width: roomAvatar.width - height: roomAvatar.height - Rectangle { - anchors.centerIn: parent - width: roomAvatar.width - height: roomAvatar.height - radius: Math.min(width, height) - } - } - } - } + color: myPalette.mid Text { - text: alias + text: "Placeholder name" + } + Button { anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 10 - anchors.left: roomAvatar.right - anchors.leftMargin: 5 + text: "Switch" - color: selected ? "black" : (highlightCount > 0 ? "red" : (notificationCount > 0 ? "blue" : "white")) + onClicked: accountMenu.popup() - textFormat: Text.PlainText - } + Menu { + id: accountMenu - MouseArea { - anchors.fill: parent + Repeater { + model: app.accounts - cursorShape: Qt.PointingHandCursor + MenuItem { + text: modelData.profileName - acceptedButtons: Qt.LeftButton | Qt.RightButton - - onReleased: { - if(mouse.button == Qt.LeftButton) { - if(!selected) { - var originalIndex = matrix.roomListModel.getOriginalIndex(index) - matrix.changeCurrentRoom(originalIndex) - channels.currentIndex = originalIndex - } - } else - contextMenu.popup() - } - } - - Menu { - id: contextMenu - - MenuItem { - text: "Mark As Read" - - onReleased: matrix.readUpTo(matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)), 0) - } - - MenuSeparator {} - - GroupBox { - title: "Notification Settings" - - Column { - spacing: 10 - - RadioButton { - text: "All messages" - - ToolTip.text: "Recieve a notification for all messages in this room." - ToolTip.visible: hovered - - onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 2 - - checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 2 - } - - RadioButton { - text: "Only Mentions" - - ToolTip.text: "Recieve a notification for mentions in this room." - ToolTip.visible: hovered - - onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 1 - - checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 1 - } - - RadioButton { - text: "Mute" - - ToolTip.text: "Don't get notifications or unread indicators for this room." - ToolTip.visible: hovered - - onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 0 - - checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 3 + onClicked: app.switchAccount(modelData.profileName) } } + + MenuSeparator {} + + MenuItem { + text: "Add new account" + + onClicked: app.addAccount("test") + } + } + } + } + + ListView { + id: channelListView + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: profileRect.bottom + anchors.bottom: parent.bottom + anchors.margins: 10 + + spacing: 5 + + model: matrix.roomListModel + + section.property: "section" + section.criteria: ViewSection.FullString + section.delegate: Rectangle { + width: parent.width + height: 25 + + color: "transparent" + + Text { + anchors.verticalCenter: parent.verticalCenter + + anchors.left: parent.left + anchors.leftMargin: 5 + + text: section + + color: myPalette.text + + textFormat: Text.PlainText + } + } + + delegate: Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 25 + + property bool selected: channelListView.currentIndex === matrix.roomListModel.getOriginalIndex(index) + + color: selected ? myPalette.highlight : "transparent" + + radius: 5 + + RoundedImage { + id: roomAvatar + + anchors.top: parent.top + anchors.topMargin: 5 + anchors.left: parent.left + anchors.leftMargin: 5 + + width: 18 + height: 18 + + source: avatarURL ? avatarURL : "placeholder.png" } - MenuSeparator {} + Text { + text: alias - MenuItem { - text: "Room Settings" + anchors.verticalCenter: parent.verticalCenter - onReleased: stack.push("qrc:/RoomSettings.qml", {"room": matrix.getRoom(matrix.roomListModel.getOriginalIndex(index))}) + anchors.left: roomAvatar.right + anchors.leftMargin: 5 + anchors.right: parent.right + anchors.rightMargin: 5 + + color: selected ? "white" : (highlightCount > 0 ? "red" : (notificationCount > 0 ? "blue" : myPalette.text)) + + textFormat: Text.PlainText + elide: Text.ElideRight } - MenuSeparator {} + MouseArea { + anchors.fill: parent - MenuItem { - text: "Leave Room" + cursorShape: Qt.PointingHandCursor + + acceptedButtons: Qt.LeftButton | Qt.RightButton onReleased: { - showDialog("Leave Confirmation", "Are you sure you want to leave " + alias + "?", [ - { - text: "Yes", - onClicked: function(dialog) { - matrix.leaveRoom(id) - dialog.close() - } - }, - { - text: "No", - onClicked: function(dialog) { - dialog.close() - } - } - ]) + if(mouse.button == Qt.LeftButton) { + if(!selected) { + var originalIndex = matrix.roomListModel.getOriginalIndex(index) + matrix.changeCurrentRoom(originalIndex) + channelListView.currentIndex = originalIndex + } + } else + contextMenu.popup() + } + } + + Menu { + id: contextMenu + + MenuItem { + text: "Mark As Read" + + onReleased: matrix.readUpTo(matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)), 0) + } + + MenuSeparator {} + + GroupBox { + title: "Notification Settings" + + Column { + spacing: 10 + + RadioButton { + text: "All messages" + + ToolTip.text: "Recieve a notification for all messages in this room." + ToolTip.visible: hovered + + onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 2 + + checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 2 + } + + RadioButton { + text: "Only Mentions" + + ToolTip.text: "Recieve a notification for mentions in this room." + ToolTip.visible: hovered + + onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 1 + + checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 1 + } + + RadioButton { + text: "Mute" + + ToolTip.text: "Don't get notifications or unread indicators for this room." + ToolTip.visible: hovered + + onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 0 + + checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 3 + } + } + } + + MenuSeparator {} + + MenuItem { + text: "Settings" + + onReleased: openRoom(matrix.getRoom(matrix.roomListModel.getOriginalIndex(index))) + } + + MenuSeparator {} + + MenuItem { + text: "Leave Room" + + onReleased: { + showDialog("Leave Confirmation", "Are you sure you want to leave " + alias + "?", [ + { + text: "Yes", + onClicked: function(dialog) { + matrix.leaveRoom(id) + dialog.close() + } + }, + { + text: "No", + onClicked: function(dialog) { + dialog.close() + } + } + ]) + } } } } - } - } + } - Button { - id: communitiesButton + Button { + width: parent.width - width: channels.width + anchors.bottom: directoryButton.top - anchors.bottom: channels.bottom + text: "Switch account" - text: "Communities" + onClicked: app.addAccount("test") + } - onClicked: stack.push("qrc:/Communities.qml") - } + Button { + id: communitiesButton - Button { - id: directoryButton + width: parent.width - width: channels.width + anchors.bottom: parent.bottom - anchors.bottom: communitiesButton.top + text: "Communities" - text: "Directory" + onClicked: stack.push("qrc:/Communities.qml") + } - onClicked: stack.push("qrc:/Directory.qml") + Button { + id: directoryButton + + width: parent.width + + anchors.bottom: communitiesButton.top + + text: "Directory" + + onClicked: stack.push("qrc:/Directory.qml") + } } Rectangle { id: rightArea height: parent.height - width: parent.width - channels.width anchors.left: channels.right + anchors.right: parent.right - color: "green" + Rectangle { + id: overlay + + z: 999 + + anchors.top: roomHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + visible: matrix.currentRoom.guestDenied + + Text { + id: invitedByLabel + + anchors.centerIn: parent + + color: myPalette.text + + text: "You have been invited to this room by " + matrix.currentRoom.invitedBy + + textFormat: Text.PlainText + } + + RowLayout { + anchors.top: invitedByLabel.bottom + anchors.topMargin: 15 + anchors.horizontalCenter: parent.horizontalCenter + + Button { + text: "Accept" + + onReleased: { + matrix.joinRoom(matrix.currentRoom.id) + } + } + + Button { + text: "Deny" + + onReleased: { + matrix.joinRoom(matrix.currentRoom.id) + } + } + } + } Rectangle { id: roomHeader height: 45 - width: parent.width - anchors.bottom: messagesArea.top + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right - color: Qt.rgba(0.3, 0.3, 0.3, 1.0) + color: myPalette.mid - Image { + RoundedImage { id: channelAvatar - cache: true - anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 15 @@ -257,32 +405,13 @@ Rectangle { width: 33 height: 33 - sourceSize.width: 33 - sourceSize.height: 33 - - fillMode: Image.PreserveAspectFit - source: matrix.currentRoom.avatar ? matrix.currentRoom.avatar : "placeholder.png" - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Item { - width: 33 - height: 33 - Rectangle { - anchors.centerIn: parent - width: 33 - height: 33 - radius: Math.min(width, height) - } - } - } } Text { id: channelTitle - font.pointSize: 15 + font.pointSize: 12 anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 15 @@ -290,22 +419,31 @@ Rectangle { text: matrix.currentRoom.name - color: "white" + color: myPalette.text textFormat: Text.PlainText + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onReleased: openRoom(matrix.currentRoom) + } } Text { id: channelTopic - width: showMemberListButton.x - x - font.pointSize: 12 anchors.verticalCenter: parent.verticalCenter + maximumLineCount: 1 + anchors.left: channelTitle.right - anchors.leftMargin: 5 + anchors.leftMargin: 10 + anchors.right: audioCallButton.left text: { if(matrix.currentRoom.direct) @@ -317,7 +455,7 @@ Rectangle { return matrix.currentRoom.topic } - color: "gray" + color: myPalette.text elide: Text.ElideRight @@ -332,105 +470,94 @@ Rectangle { textFormat: Text.PlainText } - ToolButton { - id: showMemberListButton + ToolBarButton { + id: videoCallButton - width: 25 - height: 25 + anchors.verticalCenter: parent.verticalCenter + + anchors.right: audioCallButton.left + anchors.rightMargin: 10 + + + name: "Video Call" + toolIcon: "icons/memberlist.png" + } + + ToolBarButton { + id: audioCallButton + + anchors.verticalCenter: parent.verticalCenter + + anchors.right: notificationsButton.left + anchors.rightMargin: 10 + + name: "Voice Call" + toolIcon: "icons/memberlist.png" + } + + ToolBarButton { + id: notificationsButton + + anchors.verticalCenter: parent.verticalCenter + + anchors.right: roomInfoButton.left + anchors.rightMargin: 10 + + name: "Notifications" + toolIcon: "icons/memberlist.png" + } + + ToolBarButton { + id: roomInfoButton anchors.verticalCenter: parent.verticalCenter anchors.right: settingsButton.left anchors.rightMargin: 10 - onClicked: { + onPressed: { if(memberList.width == 0) memberList.width = 200 else memberList.width = 0 } - ToolTip.visible: hovered - ToolTip.text: "Member List" - - background: Rectangle { color: "transparent" } - contentItem: Rectangle { color: "transparent" } - - visible: !matrix.currentRoom.direct - - Image { - id: memberListButtonImage - - anchors.fill: parent - - sourceSize.width: parent.width - sourceSize.height: parent.height - - source: "icons/memberlist.png" - } - - ColorOverlay { - anchors.fill: parent - source: memberListButtonImage - - color: parent.hovered ? "white" : (memberList.width == 200 ? "white" : Qt.rgba(0.8, 0.8, 0.8, 1.0)) - } + name: "Room Info" + toolIcon: "icons/memberlist.png" + isActivated: memberList.width == 200 } - ToolButton { + ToolBarButton { id: settingsButton - width: 25 - height: 25 - anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 15 - onClicked: stack.push("qrc:/Settings.qml") - - ToolTip.visible: hovered - ToolTip.text: "Settings" - - background: Rectangle { color: "transparent" } - contentItem: Rectangle { color: "transparent" } - - Image { - id: settingsButtonImage - - anchors.fill: parent - - sourceSize.width: parent.width - sourceSize.height: parent.height - - source: "icons/settings.png" - } - - ColorOverlay { - anchors.fill: parent - source: settingsButtonImage - - color: parent.hovered ? "white" : Qt.rgba(0.8, 0.8, 0.8, 1.0) - } + onPressed: stack.push("qrc:/Settings.qml") + name: "Settings" + toolIcon: "icons/settings.png" } } Rectangle { id: messagesArea - width: parent.width - memberList.width - height: parent.height - roomHeader.height - anchors.top: roomHeader.bottom + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.right: memberList.left Rectangle { - height: parent.height - messageInputParent.height - width: parent.width + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: messageInputParent.top clip: true - color: Qt.rgba(0.1, 0.1, 0.1, 1.0) + color: myPalette.light ListView { id: messages @@ -441,7 +568,10 @@ Rectangle { cacheBuffer: 200 delegate: Rectangle { - width: parent.width + anchors.left: messages.contentItem.left + anchors.right: messages.contentItem.right + anchors.margins: 10 + height: (condense ? 5 : 25) + messageArea.height color: "transparent" @@ -451,47 +581,31 @@ Rectangle { property var eventId: display.eventId property var msg: display.msg - Image { + RoundedImage { id: avatar width: 33 height: 33 - cache: true - anchors.top: parent.top anchors.topMargin: 5 anchors.left: parent.left anchors.leftMargin: 5 - sourceSize.width: 33 - sourceSize.height: 33 - - source: sender.avatarURL ? sender.avatarURL : "placeholder.png" + source: sender == null ? "placeholder.png" + : (sender.avatarURL ? sender.avatarURL : "placeholder.png") visible: !condense - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Item { - width: avatar.width - height: avatar.height - Rectangle { - anchors.centerIn: parent - width: avatar.width - height: avatar.height - radius: Math.min(width, height) - } - } - } } Text { id: senderText - text: condense ? "" : sender.displayName + text: condense || sender == null ? "" : sender.displayName - color: "white" + color: myPalette.text + + font.bold: true anchors.left: avatar.right anchors.leftMargin: 10 @@ -499,10 +613,23 @@ Rectangle { textFormat: Text.PlainText } + MouseArea { + id: senderClickArea + + anchors.left: avatar.left + anchors.right: senderText.right + anchors.top: avatar.top + anchors.bottom: avatar.bottom + + cursorShape: Qt.PointingHandCursor + + onClicked: openProfile(sender) + } + Text { text: condense ? "" : timestamp - color: "gray" + color: myPalette.dark anchors.left: senderText.right anchors.leftMargin: 10 @@ -524,27 +651,27 @@ Rectangle { return preview.height } - width: parent.width - anchors.left: condense ? parent.left : avatar.right anchors.leftMargin: condense ? 48 : 10 + anchors.right: parent.right + color: "transparent" TextEdit { id: message - text: display.msg - width: parent.width - wrapMode: Text.Wrap + text: display.msg + + wrapMode: Text.WordWrap textFormat: Text.RichText readOnly: true selectByMouse: true - color: display.sent ? "white" : "gray" + color: display.sent ? myPalette.text : myPalette.mid visible: display.msgType === "text" } @@ -696,35 +823,10 @@ Rectangle { onContentHeightChanged: { var index = indexAt(0, contentY + height - 5) matrix.readUpTo(matrix.currentRoom, index) - } - } - Rectangle { - id: overlay - - anchors.fill: parent - - visible: matrix.currentRoom.guestDenied - - Text { - id: invitedByLabel - - anchors.centerIn: parent - - color: "white" - - text: "You have been invited to this room by " + matrix.currentRoom.invitedBy - - textFormat: Text.PlainText - } - - Button { - text: "Accept" - - anchors.top: invitedByLabel.bottom - - onReleased: { - matrix.joinRoom(matrix.currentRoom.id) + //print(contentHeight + "," + height); + if(contentHeight < height) { + matrix.readMessageHistory(matrix.currentRoom) } } } @@ -733,12 +835,12 @@ Rectangle { Rectangle { id: messageInputParent - anchors.top: messages.parent.bottom + anchors.bottom: parent.bottom width: parent.width height: 55 - color: Qt.rgba(0.1, 0.1, 0.1, 1.0) + color: "transparent" ToolButton { id: attachButton @@ -775,7 +877,7 @@ Rectangle { anchors.right: parent.right anchors.rightMargin: 5 - placeholderText: "Message " + matrix.currentRoom.name + placeholderText: "Send a " + (matrix.currentRoom.encrypted ? "encrypted" : "unencrypted") + " message to " + matrix.currentRoom.name Keys.onReturnPressed: { if(event.modifiers & Qt.ShiftModifier) { @@ -836,7 +938,7 @@ Rectangle { anchors.bottom: messageInputParent.bottom - color: "white" + color: myPalette.text text: matrix.typingText @@ -849,53 +951,66 @@ Rectangle { id: memberList anchors.top: roomHeader.bottom - anchors.left: messagesArea.right + anchors.right: parent.right - color: Qt.rgba(0.15, 0.15, 0.15, 1.0) + color: myPalette.midlight width: matrix.currentRoom.direct ? 0 : 200 height: parent.height - roomHeader.height ListView { + id: memberListView + model: matrix.memberModel - anchors.fill: parent + clip: true + + anchors.top: parent.top + anchors.bottom: inviteButton.top + anchors.left: parent.left + anchors.right: parent.right + + section.property: "section" + section.criteria: ViewSection.FullString + section.delegate: Rectangle { + width: parent.width + height: 25 + + color: "transparent" + + Text { + anchors.verticalCenter: parent.verticalCenter + + anchors.left: parent.left + anchors.leftMargin: 5 + + text: section + + color: myPalette.text + + textFormat: Text.PlainText + } + } delegate: Rectangle { - width: parent.width + width: memberListView.contentItem.width height: 50 color: "transparent" property string memberId: id - Image { + RoundedImage { id: memberAvatar - cache: true + width: 33 + height: 33 anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 10 - sourceSize.width: 33 - sourceSize.height: 33 - source: avatarURL ? avatarURL : "placeholder.png" - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Item { - width: memberAvatar.width - height: memberAvatar.height - Rectangle { - anchors.centerIn: parent - width: memberAvatar.width - height: memberAvatar.height - radius: Math.min(width, height) - } - } - } } Text { @@ -904,7 +1019,7 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter - color: "white" + color: myPalette.text text: displayName @@ -914,9 +1029,17 @@ Rectangle { MouseArea { anchors.fill: parent - acceptedButtons: Qt.RightButton + acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: memberContextMenu.popup() + cursorShape: Qt.PointingHandCursor + + onClicked: function(mouse) { + if(mouse.button == Qt.LeftButton) { + openProfile(matrix.resolveMemberId(id)) + } else { + memberContextMenu.popup() + } + } } Menu { @@ -925,12 +1048,7 @@ Rectangle { MenuItem { text: "Profile" - onReleased: { - var popup = Qt.createComponent("qrc:/Profile.qml") - var popupContainer = popup.createObject(client, {"parent": client, "member": matrix.resolveMemberId(id)}) - - popupContainer.open() - } + onReleased: openProfile(matrix.resolveMemberId(id)) } MenuItem { @@ -970,23 +1088,23 @@ Rectangle { boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.VerticalFlick } - } - Button { - id: inviteButton + Button { + id: inviteButton - width: memberList.width + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 8 - anchors.bottom: memberList.bottom - anchors.right: memberList.right + text: "Invite to room" - text: "Invite to room" + onClicked: { + var popup = Qt.createComponent("qrc:/InviteDialog.qml") + var popupContainer = popup.createObject(window, {"parent": window}) - onClicked: { - var popup = Qt.createComponent("qrc:/InviteDialog.qml") - var popupContainer = popup.createObject(window, {"parent": window}) - - popupContainer.open() + popupContainer.open() + } } } } @@ -1021,21 +1139,24 @@ Rectangle { Connections { target: matrix - onSyncFinished: { + + function onSyncFinished() { syncTimer.start() if(shouldScroll) messages.positionViewAtEnd() } - onInitialSyncFinished: matrix.changeCurrentRoom(0) + function onInitialSyncFinished() { + matrix.changeCurrentRoom(0) + } - onCurrentRoomChanged: { + function onCurrentRoomChanged() { if(messages.contentY < messages.originY + 5) matrix.readMessageHistory(matrix.currentRoom) } - onMessage: { + function onMessage(room, sender, content) { var notificationLevel = room.notificationLevel var shouldDisplay = false diff --git a/qml/Dialog.qml b/qml/Dialog.qml old mode 100644 new mode 100755 index 57bbd64..85e431f --- a/qml/Dialog.qml +++ b/qml/Dialog.qml @@ -22,7 +22,7 @@ Popup { text: title - color: "white" + color: myPalette.text } Text { @@ -32,7 +32,7 @@ Popup { anchors.top: titleLabel.bottom - color: "white" + color: myPalette.text } Repeater { diff --git a/qml/Directory.qml b/qml/Directory.qml old mode 100644 new mode 100755 index 72ee534..31038ca --- a/qml/Directory.qml +++ b/qml/Directory.qml @@ -8,9 +8,7 @@ import trinity.matrix 1.0 Rectangle { id: roomDirectory - color: Qt.rgba(0.1, 0.1, 0.1, 1.0) - - Component.onCompleted: matrix.loadDirectory() + color: myPalette.window Rectangle { width: 700 @@ -40,14 +38,25 @@ Rectangle { font.pointSize: 25 font.bold: true - color: "white" + color: myPalette.text + } + + TextEdit { + id: serverEdit + + anchors.top: directoryLabel.bottom + anchors.topMargin: 10 + + width: parent.width + + onEditingFinished: matrix.loadDirectory(text) } ListView { width: parent.width height: parent.height - backButton.height - anchors.top: directoryLabel.bottom + anchors.top: serverEdit.bottom anchors.topMargin: 10 model: matrix.publicRooms @@ -60,7 +69,7 @@ Rectangle { color: "transparent" - Image { + RoundedImage { id: roomAvatar width: 32 @@ -81,7 +90,7 @@ Rectangle { font.bold: true - color: "white" + color: myPalette.text } Text { @@ -98,7 +107,7 @@ Rectangle { wrapMode: Text.Wrap - color: "white" + color: myPalette.text } MouseArea { diff --git a/qml/Profile.qml b/qml/Profile.qml old mode 100644 new mode 100755 index 35f0cab..8423825 --- a/qml/Profile.qml +++ b/qml/Profile.qml @@ -18,7 +18,7 @@ Popup { Component.onCompleted: matrix.updateMemberCommunities(member) - Image { + RoundedImage { id: profileAvatar width: 64 @@ -38,7 +38,7 @@ Popup { font.pointSize: 22 - color: "white" + color: myPalette.text } Text { @@ -50,7 +50,40 @@ Popup { text: member.id - color: "grey" + color: myPalette.dark + } + + Text { + id: roleText + + anchors.top: profileIdLabel.bottom + anchors.topMargin: 5 + anchors.left: profileAvatar.right + anchors.leftMargin: 15 + + text: "Member Role Placeholder" + + color: myPalette.dark + } + + Button { + id: directMessageButton + + anchors.verticalCenter: profileAvatar.verticalCenter + anchors.right: hamburgerButton.left + anchors.rightMargin: 10 + + text: "Direct message" + } + + Button { + id: hamburgerButton + + anchors.verticalCenter: profileAvatar.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 15 + + text: "..." } TabBar { @@ -61,12 +94,22 @@ Popup { id: profileTabs + TabButton { + text: "Security" + } + TabButton { text: "Communities" } + + TabButton { + text: "Sessions" + } } SwipeView { + interactive: false + height: parent.height - profileNameLabel.height - profileTabs.height width: parent.width @@ -75,6 +118,8 @@ Popup { currentIndex: profileTabs.currentIndex Item { + id: communityTab + ListView { id: communityList @@ -90,7 +135,7 @@ Popup { color: "transparent" - Image { + RoundedImage { id: communityAvatar width: 32 @@ -110,7 +155,7 @@ Popup { text: display.name - color: "white" + color: myPalette.text } MouseArea { @@ -135,11 +180,15 @@ Popup { text: "This member does not have any public communities." - color: "white" + color: myPalette.text visible: !member.publicCommunities || member.publicCommunities.length == 0 } } } + + Item { + id: sessionsTab + } } } diff --git a/qml/RoomSettings.qml b/qml/RoomSettings.qml old mode 100644 new mode 100755 index 3665197..636bd12 --- a/qml/RoomSettings.qml +++ b/qml/RoomSettings.qml @@ -3,35 +3,48 @@ import QtQuick.Controls 2.3 import QtGraphicalEffects 1.0 import QtQuick.Shapes 1.0 -Rectangle { +Popup { id: roomSettings - color: Qt.rgba(0.1, 0.1, 0.1, 1.0) + width: 500 + height: 256 + + x: parent.width / 2 - width / 2 + y: parent.height / 2 - height / 2 + + modal: true property var room Rectangle { - width: 700 + width: parent.width height: parent.height anchors.horizontalCenter: parent.horizontalCenter color: "transparent" - Button { - id: backButton - - text: "Back" - onClicked: stack.pop() - } - TabBar { id: bar - anchors.top: backButton.bottom + TabButton { + text: "General" + } TabButton { - text: "Overview" + text: "Security & Privacy" + } + + TabButton { + text: "Roles & Permissions" + } + + TabButton { + text: "Notifications" + } + + TabButton { + text: "Advanced" } } @@ -53,7 +66,7 @@ Rectangle { Label { id: nameLabel - text: "Name" + text: "Room Name" } TextField { @@ -67,7 +80,7 @@ Rectangle { Label { id: topicLabel - text: "Topic" + text: "Room Topic" anchors.top: nameField.bottom } diff --git a/qml/RoundedImage.qml b/qml/RoundedImage.qml new file mode 100755 index 0000000..940535e --- /dev/null +++ b/qml/RoundedImage.qml @@ -0,0 +1,36 @@ +import QtQuick 2.15 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 2.3 + +Item { + property var source: String + + Image { + id: image + + cache: true + + width: parent.width + height: parent.height + + sourceSize.width: parent.width + sourceSize.height: parent.height + + source: parent.source + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: image.width + height: image.height + Rectangle { + anchors.centerIn: parent + width: image.width + height: image.height + radius: Math.min(width, height) + } + } + } + } +} + diff --git a/qml/Settings.qml b/qml/Settings.qml old mode 100644 new mode 100755 index 77038d0..12e1928 --- a/qml/Settings.qml +++ b/qml/Settings.qml @@ -3,11 +3,12 @@ import QtQuick.Controls 2.3 import QtGraphicalEffects 1.0 import QtQuick.Shapes 1.0 import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.15 Rectangle { id: settings - color: Qt.rgba(0.1, 0.1, 0.1, 1.0) + color: myPalette.window Component.onCompleted: matrix.updateAccountInformation() @@ -19,11 +20,13 @@ Rectangle { color: "transparent" - Button { + BackButton { id: backButton - text: "Back" - onClicked: stack.pop() + anchors.top: parent.top + anchors.topMargin: 15 + + anchors.right: parent.right } TabBar { @@ -50,7 +53,7 @@ Rectangle { } } - SwipeView { + StackLayout { id: settingsStack anchors.top: bar.bottom @@ -155,12 +158,27 @@ Rectangle { id: notificationsTab CheckBox { - text: "Enable Desktop Notifications" + id: notificationsEnable + + text: "Enable notifications" + } + + CheckBox { + text: "Enable notification bar icon" + + anchors.top: notificationsEnable.bottom + + checked: desktop.isTrayIconEnabled() + onToggled: desktop.showTrayIcon(checked) } } Item { id: appearanceTab + + CheckBox { + text: "Show developer options" + } } Item { @@ -220,7 +238,7 @@ Rectangle { text: display.name - color: "white" + color: myPalette.text } ToolButton { diff --git a/qml/ToolBarButton.qml b/qml/ToolBarButton.qml new file mode 100755 index 0000000..f3faa12 --- /dev/null +++ b/qml/ToolBarButton.qml @@ -0,0 +1,42 @@ +import QtQuick 2.15 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 2.3 + +ToolButton { + width: 25 + height: 25 + + signal pressed() + + property var name: String + property var toolIcon: String + property bool isActivated: false + + onClicked: pressed() + + ToolTip.visible: hovered + ToolTip.text: name + + background: Rectangle { color: "transparent" } + contentItem: Rectangle { color: "transparent" } + + visible: !matrix.currentRoom.direct + + Image { + id: internalImage + + anchors.fill: parent + + sourceSize.width: parent.width + sourceSize.height: parent.height + + source: toolIcon + } + + ColorOverlay { + anchors.fill: parent + source: internalImage + + color: parent.hovered ? "white" : (isActivated ? "white" : Qt.rgba(0.8, 0.8, 0.8, 1.0)) + } +} diff --git a/qml/main.qml b/qml/main.qml old mode 100644 new mode 100755 index 6530214..1581a10 --- a/qml/main.qml +++ b/qml/main.qml @@ -9,7 +9,9 @@ ApplicationWindow { visible: true width: 640 height: 480 - title: "Trinity" + title: "Trinity " + matrix.profileName + + SystemPalette { id: myPalette; colorGroup: SystemPalette.Active } property var showDialog: function(title, description, buttons) { var popup = Qt.createComponent("qrc:/Dialog.qml") @@ -24,17 +26,60 @@ ApplicationWindow { } Component.onCompleted: { - if(matrix.settingsValid()) { - desktop.showTrayIcon(false) - stack.push("qrc:/Client.qml") - } else { - desktop.showTrayIcon(true) - stack.push("qrc:/Login.qml") + connect.onAccountChange() + } + + Connections { + id: connect + + target: app + + function onAccountChange() { + if(matrix.settingsValid()) { + desktop.showTrayIcon(false) + stack.replace("qrc:/Client.qml") + } else { + desktop.showTrayIcon(true) + stack.replace("qrc:/Login.qml") + } } } StackView { id: stack anchors.fill: parent + + pushEnter: Transition { + PropertyAnimation { + property: "opacity" + from: 0 + to:1 + duration: 200 + } + } + pushExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to:0 + duration: 200 + } + } + popEnter: Transition { + PropertyAnimation { + property: "opacity" + from: 0 + to:1 + duration: 200 + } + } + popExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to:0 + duration: 200 + } + } } } diff --git a/src/encryption.cpp b/src/encryption.cpp new file mode 100755 index 0000000..f210acb --- /dev/null +++ b/src/encryption.cpp @@ -0,0 +1,337 @@ +#include "encryption.h" +#include +#include +#include +#include +#include +#include + +void Encryption::createNewDeviceKeys() { + initAccount(); + + // create account + auto length = olm_create_account_random_length(account); + + void* memory = malloc(length); + + int fd = open("/dev/random", O_RDONLY); + read(fd, memory, length); + + olm_create_account(account, memory, length); + qDebug() << olm_account_last_error(account); + + qDebug() << "Created new device keys!"; + + // retrieve identity keys + length = olm_account_identity_keys_length(account); + + char* identity_memory = (char*)malloc(length + 1); + identity_memory[length] = '\0'; + + olm_account_identity_keys(account, identity_memory, length); + + QJsonDocument doc = QJsonDocument::fromJson((char*)identity_memory); + identityKey = doc.object(); + + qDebug() << "identity keys: " << (char*)identity_memory; + qDebug() << "identity keys (parsed): " << identityKey; + +} + +QString Encryption::saveDeviceKeys() { + auto length = olm_pickle_account_length(account); + char* identity_memory = (char*)malloc(length + 1); + identity_memory[length] = '\0'; + + auto err = olm_pickle_account(account, "secret_key", 10, identity_memory, length); + qDebug() << olm_account_last_error(account); + qDebug() << err << " = " << olm_error(); + + qDebug() << "Save: " << identity_memory; + + return identity_memory; +} + +void Encryption::loadDeviceKeys(QString bytes) { + initAccount(); + + qDebug() << "load: " << bytes; + + auto length = bytes.length(); + + std::string key = bytes.toStdString(); + + olm_unpickle_account(account, "secret_key", 10, key.data(), key.length()); + qDebug() << olm_account_last_error(account); + + length = olm_account_identity_keys_length(account); + char* identity_memory = (char*)malloc(length + 1); + identity_memory[length] = '\0'; + + olm_account_identity_keys(account, identity_memory, length); + + qDebug() << identity_memory; + + QJsonDocument doc = QJsonDocument::fromJson(identity_memory); + qDebug() << doc; + + identityKey = doc.object(); +} + +QJsonObject Encryption::generateOneTimeKeys(int number_of_keys) { + int fd = open("/dev/random", O_RDONLY); + + // generate random data + auto length = olm_account_generate_one_time_keys_random_length(account, number_of_keys); + auto memory = malloc(length); + read(fd, memory, length); + + olm_account_generate_one_time_keys(account, number_of_keys, memory, length); + + // retrieve one time keys + length = olm_account_one_time_keys_length(account); + memory = malloc(length); + olm_account_one_time_keys(account, memory, length); + + olm_account_mark_keys_as_published(account); + + QJsonDocument doc = QJsonDocument::fromJson((char*)memory); + return doc.object(); +} + +int Encryption::getRecommendedNumberOfOneTimeKeys() { + return olm_account_max_number_of_one_time_keys(account) / 2; +} + +QString Encryption::signMessage(QString message) { + auto length = olm_account_signature_length(account); + + void* memory = malloc(length); + + olm_account_sign( + account, + message.data(), message.length(), + memory, length + ); + + qDebug() << "Signed: " << (char*)memory; + + return (char*)memory; +} + +void Encryption::initAccount() { + void* memory = malloc(olm_account_size()); + account = olm_account(memory); +} + +OlmOutboundGroupSession* Encryption::beginOutboundSession() { + void* outboundMemory = malloc(olm_outbound_group_session_size()); + auto outboundSession = olm_outbound_group_session(outboundMemory); + + auto length = olm_init_outbound_group_session_random_length(outboundSession); + + void* memory = malloc(length); + + int fd = open("/dev/random", O_RDONLY); + read(fd, memory, length); + + olm_init_outbound_group_session(outboundSession, (uint8_t*)memory, length); + + return outboundSession; +} + +std::string Encryption::getGroupSessionId(OlmOutboundGroupSession* session) { + auto encryptedLength = olm_outbound_group_session_id_length(session); + char* encryptedMemory = (char*)malloc(encryptedLength + 1); + + olm_outbound_group_session_id(session, (uint8_t*)encryptedMemory, encryptedLength); + + return encryptedMemory; +} + +std::string Encryption::getGroupSessionKey(OlmOutboundGroupSession* session) { + auto encryptedLength = olm_outbound_group_session_key_length(session); + char* encryptedMemory = (char*)malloc(encryptedLength + 1); + + olm_outbound_group_session_key(session, (uint8_t*)encryptedMemory, encryptedLength); + + return encryptedMemory; +} + +OlmSession* Encryption::beginOutboundOlmSession(std::string identityKey, std::string oneTimeKey) { + void* outboundMemory = malloc(olm_session_size()); + auto outboundSession = olm_session(outboundMemory); + + auto length = olm_create_outbound_session_random_length(outboundSession); + + void* memory = malloc(length); + + int fd = open("/dev/random", O_RDONLY); + read(fd, memory, length); + + olm_create_outbound_session(outboundSession, account, identityKey.data(), identityKey.length(), oneTimeKey.data(), oneTimeKey.length(), (uint8_t*)memory, length); + //qDebug() << "ERR: " << olm_session_last_error(outboundSession); + + return outboundSession; + + /*size_t olm_create_outbound_session( + OlmSession * session, + OlmAccount * account, + void const * their_identity_key, size_t their_identity_key_length, + void const * their_one_time_key, size_t their_one_time_key_length, + void * random, size_t random_length + );*/ + +} + +std::string Encryption::getSessionId(OlmSession* session) { + auto length = olm_session_id_length(session); + char* memory = (char*)malloc(length + 1); + memory[length] = '\0'; + + /** An identifier for this session. Will be the same for both ends of the + * conversation. If the id buffer is too small then olm_session_last_error() + * will be "OUTPUT_BUFFER_TOO_SMALL". */ + olm_session_id( + session, + memory, length + ); + + return memory; +} + +std::string Encryption::encrypt(OlmSession* session, std::string message) { + auto length = olm_encrypt_random_length(session); + + void* memory = malloc(length); + + int fd = open("/dev/random", O_RDONLY); + read(fd, memory, length); + + auto encryptedLength = olm_encrypt_message_length(session, message.length()); + char* encryptedMemory = (char*)malloc(encryptedLength + 1); + + olm_encrypt(session, message.data(), message.length(), memory, length, encryptedMemory, encryptedLength); + + return encryptedMemory; +} + +std::string Encryption::encryptGroup(OlmOutboundGroupSession* session, std::string message) { + auto encryptedLength = olm_group_encrypt_message_length(session, message.length()); + char* encryptedMemory = (char*)malloc(encryptedLength + 1); + + olm_group_encrypt(session, (uint8_t*)message.data(), message.length(), (uint8_t*)encryptedMemory, encryptedLength); + + return encryptedMemory; +} + +OlmSession* Encryption::createInboundSession(std::string senderKey, std::string body) { + void* inboundMemory = malloc(olm_session_size()); + auto inboundSession = olm_session(inboundMemory); + + olm_create_inbound_session_from(inboundSession, account, (void*)senderKey.c_str(), senderKey.length(), (void*)body.c_str(), body.length()); + + return inboundSession; +} + +std::vector Encryption::decrypt(OlmSession* session, int msgType, std::string cipherText) { + /*std::string maxPlaintextLengthBuffer = cipherText; + + size_t maxPlaintextLength = olm_decrypt_max_plaintext_length(session, msgType, (void*)maxPlaintextLengthBuffer.data(), maxPlaintextLengthBuffer.length());; + qDebug() << "THE ERROR YOU ARE LOOKING FOR " << olm_session_last_error(session); + if(maxPlaintextLength == olm_error()) + return ""; + + char* plaintext = new char[maxPlaintextLength]; + //plaintext[maxPlaintextLength] = '\0'; + memset(plaintext, '\0', maxPlaintextLength); + int size = olm_decrypt(session, msgType, (void*)cipherText.data(), cipherText.length(), (void*)plaintext, maxPlaintextLength); + //plaintext[size] = '\0'; + + plaintext[size + 1] = '\0'; + + return plaintext;*/ + + // because olm_group_decrypt_max_plaintext_length DESTROYS the buffer, why? + auto tmp_plaintext_buffer = std::vector(cipherText.size()); + std::copy(cipherText.begin(), cipherText.end(), tmp_plaintext_buffer.begin()); + + // create the result buffer + size_t length = olm_decrypt_max_plaintext_length(session, msgType, tmp_plaintext_buffer.data(), tmp_plaintext_buffer.size()); + if(length == olm_error()) + return {}; + + auto result_buffer = std::vector(length); + + // now create another buffer for olm_group_decrypt + auto tmp_buffer = std::vector(cipherText.size()); + std::copy(cipherText.begin(), cipherText.end(), tmp_buffer.begin()); + + auto size = olm_decrypt(session, msgType, tmp_buffer.data(), tmp_buffer.size(), result_buffer.data(), result_buffer.size()); + if(size == olm_error()) + return {}; + + auto output = std::vector(size); + std::memcpy(output.data(), result_buffer.data(), size); + + return output; +} + +QString Encryption::saveSession(OlmOutboundGroupSession* session) { + auto length = olm_pickle_outbound_group_session_length(session); + char* identity_memory = (char*)malloc(length + 1); + identity_memory[length] = '\0'; + + auto err = olm_pickle_outbound_group_session(session, "secret_key", 10, identity_memory, length); + + return identity_memory; +} + +OlmOutboundGroupSession* Encryption::loadSession(QString bytes) { + void* outboundMemory = malloc(olm_outbound_group_session_size()); + auto outboundSession = olm_outbound_group_session(outboundMemory); + + auto length = bytes.length(); + + std::string key = bytes.toStdString(); + + olm_unpickle_outbound_group_session(outboundSession, "secret_key", 10, (void*)key.data(), key.length()); + + return outboundSession; +} + +OlmInboundGroupSession* Encryption::beginInboundSession(std::string sessionKey) { + void* inboundMemory = malloc(olm_inbound_group_session_size()); + auto inboundSession = olm_inbound_group_session(inboundMemory); + + olm_init_inbound_group_session(inboundSession, (uint8_t*)sessionKey.data(), sessionKey.size()); + + return inboundSession; +} + +std::vector Encryption::decrypt(OlmInboundGroupSession* session, std::string cipherText) { + // because olm_group_decrypt_max_plaintext_length DESTROYS the buffer, why? + auto tmp_plaintext_buffer = std::vector(cipherText.size()); + std::copy(cipherText.begin(), cipherText.end(), tmp_plaintext_buffer.begin()); + + // create the result buffer + size_t length = olm_group_decrypt_max_plaintext_length(session, tmp_plaintext_buffer.data(), tmp_plaintext_buffer.size()); + if(length == olm_error()) + return {}; + + auto result_buffer = std::vector(length); + + // now create another buffer for olm_group_decrypt + auto tmp_buffer = std::vector(cipherText.size()); + std::copy(cipherText.begin(), cipherText.end(), tmp_buffer.begin()); + + uint32_t msgIndex; + auto size = olm_group_decrypt(session, tmp_buffer.data(), tmp_buffer.size(), result_buffer.data(), result_buffer.size(), &msgIndex); + if(size == olm_error()) + return {}; + + auto output = std::vector(size); + std::memcpy(output.data(), result_buffer.data(), size); + + return output; +} diff --git a/src/main.cpp b/src/main.cpp old mode 100644 new mode 100755 index d298a06..3bf98f5 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "eventmodel.h" #include "membermodel.h" @@ -19,23 +20,58 @@ #include "community.h" #include "roomlistsortmodel.h" #include "emote.h" +#include "appcore.h" -QNetworkAccessManager* network::manager; -QString network::homeserverURL, network::accessToken; +void AppCore::addAccount(QString profileName) { + accounts.push_back(new MatrixCore(profileName)); + + context->setContextProperty("matrix", accounts.back()); + + emit accountChange(); +} + +void AppCore::switchAccount(QString profileName) { + qDebug() << "switching to " << profileName; + + for(auto account : accounts) { + if(account->profileName == profileName) { + qDebug() << account->profileName << " = " << profileName; + context->setContextProperty("matrix", account); + } + } + + emit accountChange(); +} + +QVariantList AppCore::getAccounts() { + QVariantList list; + for(auto account : accounts) + list.push_back(QVariant::fromValue(account)); + + return list; +} int main(int argc, char* argv[]) { - QApplication app2(argc, argv); - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setApplicationName("Trinity"); + QCoreApplication::setOrganizationName("redstrate"); + + QtWebEngine::initialize(); + + QApplication app2(argc, argv); + + QCommandLineParser parser; + parser.addHelpOption(); + + QCommandLineOption profileOption("profile", "Default profile to load", "profile"); + + parser.addOption(profileOption); + parser.process(app2); qRegisterMetaType(); - network::manager = new QNetworkAccessManager(); - - network::homeserverURL = "https://matrix.org"; - // matrix + qmlRegisterUncreatableType("trinity.matrix", 1, 0, "AppCore", ""); qmlRegisterUncreatableType("trinity.matrix", 1, 0, "EventModel", ""); qmlRegisterUncreatableType("trinity.matrix", 1, 0, "MatrixCore", ""); qmlRegisterUncreatableType("trinity.matrix", 1, 0, "Room", ""); @@ -51,7 +87,15 @@ int main(int argc, char* argv[]) { QQmlApplicationEngine engine; QQmlContext* context = new QQmlContext(engine.rootContext(), &engine); - MatrixCore matrix; + AppCore* core = new AppCore(); + core->context = context; + + if(parser.isSet(profileOption)) { + core->addAccount(parser.value(profileOption)); + } else { + core->addAccount(); + } + Desktop desktop; QSystemTrayIcon* trayIcon = new QSystemTrayIcon(); @@ -59,7 +103,7 @@ int main(int argc, char* argv[]) { desktop.icon = trayIcon; context->setContextProperty("desktop", &desktop); - context->setContextProperty("matrix", &matrix); + context->setContextProperty("app", core); QQmlComponent component(&engine); component.loadUrl(QUrl("qrc:/main.qml")); diff --git a/src/matrixcore.cpp b/src/matrixcore.cpp old mode 100644 new mode 100755 index 45b9aa6..a3f2036 --- a/src/matrixcore.cpp +++ b/src/matrixcore.cpp @@ -10,18 +10,31 @@ #include #include #include - +#include +#include #include "network.h" #include "community.h" -MatrixCore::MatrixCore(QObject* parent) : QObject(parent), roomListModel(rooms), directoryListModel(publicRooms), eventModel(*this) { +MatrixCore::MatrixCore(QString profileName, QObject* parent) : QObject(parent), profileName(profileName), roomListModel(rooms), directoryListModel(publicRooms), eventModel(*this) { + network = new Network(); + encryption = new Encryption(); + QSettings settings; + settings.beginGroup(profileName); homeserverURL = settings.value("homeserver", "matrix.org").toString(); userId = settings.value("userId").toString(); - network::homeserverURL = "https://" + homeserverURL; + network->homeserverURL = "https://" + homeserverURL; + deviceId = settings.value("deviceId").toString(); if(settings.contains("accessToken")) - network::accessToken = "Bearer " + settings.value("accessToken").toString(); + network->accessToken = "Bearer " + settings.value("accessToken").toString(); + + if(settings.contains("accountPickle")) { + encryption->loadDeviceKeys(settings.value("accountPickle").toString()); + qDebug() << "testing identity key: " << encryption->identityKey; + } + + settings.endGroup(); emptyRoom.setName("Empty"); emptyRoom.setTopic("There is nothing here."); @@ -33,6 +46,13 @@ MatrixCore::MatrixCore(QObject* parent) : QObject(parent), roomListModel(rooms), roomListSortModel.sort(0); }); + memberSortModel.setSourceModel(&memberModel); + memberSortModel.setSortRole(RoomListModel::SectionRole); + + connect(this, &MatrixCore::currentRoomChanged, [this] { + memberSortModel.sort(0); + }); + directoryListSortModel.setSourceModel(&directoryListModel); updateAccountInformation(); @@ -74,7 +94,7 @@ void MatrixCore::registerAccount(const QString &username, const QString &passwor {"password", password} }; - network::postJSON("/_matrix/client/r0/register?kind=user", registerObject, [this](QNetworkReply* reply) { + network->postJSON("/_matrix/client/r0/register?kind=user", registerObject, [this](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); if(reply->error()) { @@ -103,12 +123,14 @@ void MatrixCore::registerAccount(const QString &username, const QString &passwor emit registerAttempt(true, document.object()["error"].toString()); } } else { - network::accessToken = "Bearer " + document.object()["access_token"].toString(); + network->accessToken = "Bearer " + document.object()["access_token"].toString(); QSettings settings; + settings.beginGroup(profileName); settings.setValue("accessToken", document.object()["access_token"].toString()); settings.setValue("userId", document.object()["user_id"].toString()); settings.setValue("deviceId", document.object()["device_id"].toString()); + settings.endGroup(); emit registerAttempt(false, ""); } @@ -123,26 +145,83 @@ void MatrixCore::login(const QString& username, const QString& password) { {"initial_device_display_name", "Trinity"} }; - network::postJSON("/_matrix/client/r0/login", loginObject, [this](QNetworkReply* reply) { + network->postJSON("/_matrix/client/r0/login", loginObject, [this](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); if(reply->error()) { emit loginAttempt(true, document.object()["error"].toString()); } else { - network::accessToken = "Bearer " + document.object()["access_token"].toString(); + network->accessToken = "Bearer " + document.object()["access_token"].toString(); QSettings settings; + settings.beginGroup(profileName); settings.setValue("accessToken", document.object()["access_token"].toString()); settings.setValue("userId", document.object()["user_id"].toString()); settings.setValue("deviceId", document.object()["device_id"].toString()); + // upload keys + encryption->createNewDeviceKeys(); + + QJsonObject keysObject { + {"curve25519:" + document.object()["device_id"].toString(), encryption->identityKey["curve25519"]}, + {"ed25519:" + document.object()["device_id"].toString(), encryption->identityKey["ed25519"]} + }; + + QJsonObject deviceKeysObject { + {"user_id", document.object()["user_id"].toString()}, + {"device_id", document.object()["device_id"].toString()}, + {"algorithms", QJsonArray({"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"})}, + {"keys", keysObject} + }; + + QJsonObject signature { + {"ed25519:" + document.object()["device_id"].toString(), encryption->signMessage(QJsonDocument(deviceKeysObject).toJson(QJsonDocument::Compact)) }}; + + deviceKeysObject["signatures"] = QJsonObject { + {document.object()["user_id"].toString(), signature}}; + + QJsonObject oneTimeKeyObject; + + auto one_time_keys = encryption->generateOneTimeKeys(encryption->getRecommendedNumberOfOneTimeKeys()); + + for(auto key : one_time_keys["curve25519"].toObject().keys()) { + QJsonObject keyObject { + {"key", one_time_keys["curve25519"].toObject()[key]} + }; + + QJsonObject signature { + {"ed25519:" + document.object()["device_id"].toString(), encryption->signMessage(QJsonDocument(keyObject).toJson(QJsonDocument::Compact)) }}; + + keyObject["signatures"] = QJsonObject { + {document.object()["user_id"].toString(), signature}}; + + oneTimeKeyObject["signed_curve25519:" + key] = keyObject; + } + + QJsonObject masterKeysObject { + {"device_keys", deviceKeysObject}, + {"one_time_keys", oneTimeKeyObject} + }; + + //qDebug() << masterKeysObject; + + auto keys = encryption->saveDeviceKeys(); + + settings.setValue("accountPickle", keys); + settings.endGroup(); + + network->postJSON("/_matrix/client/r0/keys/upload", masterKeysObject, [this](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + qDebug() << "KEY UPLOAD RESULT: " << document; + }); + emit loginAttempt(false, ""); } }); } void MatrixCore::logout() { - network::post("/_matrix/client/r0/logout"); + network->post("/_matrix/client/r0/logout"); QSettings settings; settings.remove("accessToken"); @@ -152,7 +231,7 @@ void MatrixCore::logout() { } void MatrixCore::updateAccountInformation() { - network::get("/_matrix/client/r0/profile/" + userId + "/displayname", [this](QNetworkReply* reply) { + network->get("/_matrix/client/r0/profile/" + userId + "/displayname", [this](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); displayName = document.object()["displayname"].toString(); @@ -167,26 +246,98 @@ void MatrixCore::setDisplayName(const QString& name) { {"displayname", name} }; - network::putJSON("/_matrix/client/r0/profile/" + userId + "/displayname", displayNameObject, [this, name](QNetworkReply* reply) { + network->putJSON("/_matrix/client/r0/profile/" + userId + "/displayname", displayNameObject, [this, name](QNetworkReply* reply) { emit displayNameChanged(); }); } void MatrixCore::sync() { - if(network::accessToken.isEmpty()) + if(network->accessToken.isEmpty()) return; QString url = "/_matrix/client/r0/sync"; if(!nextBatch.isEmpty()) url += "?since=" + nextBatch; - network::get(url, [this](QNetworkReply* reply) { + network->get(url, [this](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); if(!document.object()["next_batch"].isNull()) nextBatch = document.object()["next_batch"].toString(); - const auto createRoom = [this](const QString id, const QString joinState) { + //qDebug() << document.object()["to_device"]; + //qDebug() << document.object()["device_one_time_keys_count"]; + + for(auto event : document.object()["to_device"].toObject()["events"].toArray()) { + if(event.toObject()["type"] == "m.room_key_request" && event.toObject()["content"].toObject()["action"] == "request") { + auto sender = event.toObject()["sender"].toString(); + auto device_id = event.toObject()["content"].toObject()["requesting_device_id"].toString(); + auto room_id = event.toObject()["content"].toObject()["body"].toObject()["room_id"].toString(); + + QJsonObject queryObject { + {"timeout", 10000}, + {"device_keys", QJsonObject{ + {sender, QJsonArray({device_id})} + }}, + {"token", "string"}, + }; + + network->postJSON("/_matrix/client/r0/keys/query", queryObject, [this, event, sender, device_id, room_id](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + + auto senderCurveKey = document.object()["device_keys"].toObject()[sender].toObject()[device_id].toObject()["keys"].toObject()["curve25519:" + device_id].toString(); + auto senderEdKey = document.object()["device_keys"].toObject()[sender].toObject()[device_id].toObject()["keys"].toObject()["ed25519:" + device_id].toString(); + + qDebug() << "sending keys to " << device_id; + + createOrLoadSession(); + + sendKeyToDevice(room_id, senderCurveKey, senderEdKey, currentSessionId, currentSessionKey, sender, device_id); + }); + } else if(event.toObject()["type"] == "m.room_key") { + qDebug() << "we recieved a new key from a user in the room :-)"; + } else if(event.toObject()["type"] == "m.forwarded_room_key") { + qDebug() << "we recieved a new key from a user in the room :-)"; + } else if(event.toObject()["type"] == "m.room.encrypted") { + auto curveKey = event.toObject()["content"].toObject()["ciphertext"].toObject().keys()[0]; + auto senderKey = event.toObject()["content"].toObject()["sender_key"].toString(); + int type = event.toObject()["content"].toObject()["ciphertext"].toObject()[curveKey].toObject()["type"].toInt(); + auto body = event.toObject()["content"].toObject()["ciphertext"].toObject()[curveKey].toObject()["body"].toString(); + + // create a new inbound session + auto session = encryption->createInboundSession(senderKey.toStdString(), body.toStdString()); + + auto decryptedMsg = encryption->decrypt(session, type, body.toStdString()); + + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(reinterpret_cast(decryptedMsg.data()), decryptedMsg.size())); + auto id = document.object()["content"].toObject()["session_id"].toString(); + + qDebug() << "NEW KEY " << id << " = " << document; + + // create new inbound session, append to list + auto sess = encryption->beginInboundSession(document.object()["content"].toObject()["session_key"].toString().toStdString()); + inboundSessions[id] = sess; + + // if we recieved a new key, let's see if we can decrypt some old messages! + for(auto room : rooms) { + if(room->getId() == document.object()["content"].toObject()["room_id"].toString()) { + for(auto event : room->events) { + if(event->encryptionInfo != nullptr && event->encryptionInfo->sessionId == id) { + auto msg = encryption->decrypt(sess, event->encryptionInfo->cipherText.toStdString()); + + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(reinterpret_cast(msg.data()), msg.size())); + + populateEvent(document.object(), event); + + emit event->msgChanged(); + } + } + } + } + } + } + + const auto createRoom = [this](const QString id, const QString joinState, bool autofill_data = true) { roomListModel.beginInsertRoom(); Room* room = new Room(this); @@ -201,45 +352,65 @@ void MatrixCore::sync() { room->setNotificationLevel(1); settings.endGroup(); - network::get("/_matrix/client/r0/rooms/" + id + "/state/m.room.name", [this, room](QNetworkReply* reply) { - const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); - if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { - room->setGuestDenied(true); - return; - } else if(document.object()["errcode"].toString() == "M_NOT_FOUND") - return; + if(autofill_data) { + network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.name", [this, room](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { + room->setGuestDenied(true); + return; + } else if(document.object()["errcode"].toString() == "M_NOT_FOUND") + return; - room->setName(document.object()["name"].toString()); + room->setName(document.object()["name"].toString()); - roomListModel.updateRoom(room); - }); + roomListModel.updateRoom(room); + }); - network::get("/_matrix/client/r0/rooms/" + id + "/state/m.room.topic", [this, room](QNetworkReply* reply) { - const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); - if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { - room->setGuestDenied(true); - return; - } + network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.topic", [this, room](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { + room->setGuestDenied(true); + return; + } - room->setTopic(document.object()["topic"].toString()); + room->setTopic(document.object()["topic"].toString()); - roomListModel.updateRoom(room); - }); + roomListModel.updateRoom(room); + }); - network::get("/_matrix/client/r0/rooms/" + id + "/state/m.room.avatar", [this, room](QNetworkReply* reply) { - const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); - if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { - room->setGuestDenied(true); - return; - } + network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.avatar", [this, room](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { + room->setGuestDenied(true); + return; + } - if(document.object().contains("url")) { - const QString imageId = document.object()["url"].toString().remove("mxc://"); - room->setAvatar(network::homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); - } + if(document.object().contains("url")) { + const QString imageId = document.object()["url"].toString().remove("mxc://"); + room->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); + } - roomListModel.updateRoom(room); - }); + roomListModel.updateRoom(room); + }); + + network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.power_levels", [this, room](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + + for(auto user : document.object()["users"].toObject().keys()) { + room->powerLevelList[user] = document.object()["users"].toObject()[user].toInt(); + } + }); + + network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.encryption", [this, room](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + + if(document.object().contains("algorithm")) { + room->setEncrypted(); + } + + //qDebug() << document; + }); + } rooms.push_back(room); idToRoom.insert(id, room); @@ -255,7 +426,8 @@ void MatrixCore::sync() { for(const auto id : document.object()["rooms"].toObject()["invite"].toObject().keys()) { if(!invitedRooms.count(id)) { - Room* room = createRoom(id, "Invited"); + Room* room = createRoom(id, "Invited", false); + room->setGuestDenied(true); for(auto event : document.object()["rooms"].toObject()["invite"].toObject()[id].toObject()["invite_state"].toObject()["events"].toArray()) { const QString type = event.toObject()["type"].toString(); @@ -266,7 +438,7 @@ void MatrixCore::sync() { room->setName(event.toObject()["content"].toObject()["name"].toString()); } else if(type == "m.room.avatar") { const QString imageId = event.toObject()["content"].toObject()["url"].toString().remove("mxc://"); - room->setAvatar(network::homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); + room->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); } roomListModel.updateRoom(room); @@ -393,6 +565,10 @@ void MatrixCore::sync() { }); } +bool MatrixCore::isInitialSyncComplete() { + return !firstSync; +} + void MatrixCore::sendMessage(Room* room, const QString& message) { if(message.isEmpty()) return; @@ -437,8 +613,9 @@ void MatrixCore::sendMessage(Room* room, const QString& message) { shouldSendAsMarkdown = strlen(formatted) > 8 + message.length(); } + QJsonObject messageObject; if(shouldSendAsMarkdown) { - const QJsonObject messageObject { + messageObject = QJsonObject { {"msgtype", "m.text"}, {"formatted_body", formatted}, {"body", message}, @@ -446,15 +623,80 @@ void MatrixCore::sendMessage(Room* room, const QString& message) { }; e->setMsg(formatted); - - network::putJSON("/_matrix/client/r0/rooms/" + room->getId() + "/send/m.room.message/" + QRandomGenerator::global()->generate(), messageObject, onMessageFeedbackReceived); } else { - const QJsonObject messageObject { + messageObject = QJsonObject { {"msgtype", "m.text"}, {"body", message} }; - network::putJSON(QString("/_matrix/client/r0/rooms/" + room->getId() + "/send/m.room.message/") + QRandomGenerator::global()->generate(), messageObject, onMessageFeedbackReceived); + e->setMsg(message); + } + + if(room->getEncrypted()) { + // create megolm session + createOrLoadSession(); + + /*QJsonObject deviceKeys; + + // get device list for each user + for(auto member : room->members) { + deviceKeys.insert(member->getId(), QJsonArray()); + } + + QJsonObject queryObject { + {"timeout", 10000}, + {"device_keys", deviceKeys}, + {"token", "string"}, + }; + + network->postJSON("/_matrix/client/r0/keys/query", queryObject, [this, room](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + // for each user, and for each of their devices, start a new olm session + for(auto user_id : document.object()["device_keys"].toObject().keys()) { + for(auto device_id : document.object()["device_keys"].toObject()[user_id].toObject().keys()) { + QJsonObject claimObject { + {"timeout", 10000}, + {"one_time_keys", QJsonObject { + {user_id, QJsonObject { + {device_id, "signed_curve25519"} + }} + }} + }; + + std::string identityKey = document.object()["device_keys"].toObject()[user_id].toObject()[device_id].toObject()["keys"].toObject()[QString("curve25519:") + device_id].toString().toStdString(); + + std::string edIdentityKey = document.object()["device_keys"].toObject()[user_id].toObject()[device_id].toObject()["keys"].toObject()[QString("ed25519:") + device_id].toString().toStdString(); + + sendKeyToDevice(room->getId(), identityKey.c_str(), edIdentityKey.c_str(), currentSessionId, currentSessionKey, user_id, device_id); + } + } + });*/ + + QJsonObject trueObject { + {"room_id", room->getId()}, + {"type", "m.room.message"}, + {"content", messageObject} + /*{"keys", QJsonObject { + {"ed25519", encryption->identityKey["ed25519"]} + }}, + {"sender", userId}, + {"sender_device", deviceId}*/ + }; + + // construct the m.room.encrypted event + const QJsonObject roomEncryptedObject { + {"algorithm", "m.megolm.v1.aes-sha2"}, + {"ciphertext", encryption->encryptGroup(currentSession, QString(QJsonDocument(trueObject).toJson(QJsonDocument::Compact)).toStdString()).c_str()}, + {"sender_key", encryption->identityKey["curve25519"]}, + {"session_id", currentSessionId}, + {"device_id", deviceId} + }; + + network->putJSON("/_matrix/client/r0/rooms/" + room->getId() + "/send/m.room.encrypted/" + QRandomGenerator::global()->generate(), roomEncryptedObject, [](QNetworkReply* reply) { + //qDebug() << "reply from room send: " << reply->readAll(); + }); + } else { + network->putJSON(QString("/_matrix/client/r0/rooms/" + room->getId() + "/send/m.room.message/") + QString::number(QRandomGenerator::global()->generate()), messageObject, onMessageFeedbackReceived); } } @@ -463,7 +705,7 @@ void MatrixCore::removeMessage(const QString& eventId) { {"reason", ""} }; - network::putJSON("/_matrix/client/r0/rooms/" + currentRoom->getId() + "/redact/" + eventId + "/" + QRandomGenerator::global()->generate(), reasonObject, [this, eventId](QNetworkReply* reply) { + network->putJSON("/_matrix/client/r0/rooms/" + currentRoom->getId() + "/redact/" + eventId + "/" + QString::number(QRandomGenerator::global()->generate()), reasonObject, [this, eventId](QNetworkReply* reply) { auto& events = currentRoom->events; for(int i = 0; i < events.size(); i++) { if(events[i]->eventId == eventId) { @@ -506,7 +748,7 @@ void MatrixCore::uploadAttachment(Room* room, const QString& path) { e->setMsg(fileName); e->setMsgType(mimeType.name().contains("image") ? "image" : "file"); - network::postBinary("/_matrix/media/r0/upload?filename=" + f.fileName(), f.readAll(), mimeType.name(), [this, mimeType, fileName, fileSize, e](QNetworkReply* reply) { + network->postBinary("/_matrix/media/r0/upload?filename=" + f.fileName(), f.readAll(), mimeType.name(), [this, mimeType, fileName, fileSize, e](QNetworkReply* reply) { if(!reply->error()) { e->setSent(true); @@ -524,7 +766,7 @@ void MatrixCore::uploadAttachment(Room* room, const QString& path) { {"info", infoObject} }; - network::putJSON("/_matrix/client/r0/rooms/" + currentRoom->getId() + "/send/m.room.message/" + QRandomGenerator::global()->generate(), imageObject); + network->putJSON("/_matrix/client/r0/rooms/" + currentRoom->getId() + "/send/m.room.message/" + QString::number(QRandomGenerator::global()->generate()), imageObject); } }, [e](qint64 sent, qint64 total) { e->setSentProgress((double)sent / (double)total); @@ -540,7 +782,7 @@ void MatrixCore::startDirectChat(const QString& id) { {"invite", QJsonArray{id}} }; - network::postJSON("/_matrix/client/r0/createRoom", roomObject, [](QNetworkReply*) {}); + network->postJSON("/_matrix/client/r0/createRoom", roomObject, [](QNetworkReply*) {}); } void MatrixCore::setTyping(Room* room) { @@ -549,11 +791,11 @@ void MatrixCore::setTyping(Room* room) { {"timeout", 15000} }; - network::putJSON("/_matrix/client/r0/rooms/" + room->getId() + "/typing/" + userId, typingObject); + network->putJSON("/_matrix/client/r0/rooms/" + room->getId() + "/typing/" + userId, typingObject); } void MatrixCore::joinRoom(const QString& id) { - network::post("/_matrix/client/r0/rooms/" + id + "/join", [this, id](QNetworkReply* reply) { + network->post("/_matrix/client/r0/rooms/" + id + "/join", [this, id](QNetworkReply* reply) { if(!reply->error()) { //check if its by an invite if(invitedRooms.contains(id)) { @@ -576,7 +818,7 @@ void MatrixCore::joinRoom(const QString& id) { } void MatrixCore::leaveRoom(const QString& id) { - network::post("/_matrix/client/r0/rooms/" + id + "/leave"); + network->post("/_matrix/client/r0/rooms/" + id + "/leave"); } void MatrixCore::inviteToRoom(Room* room, const QString& userId) { @@ -584,14 +826,14 @@ void MatrixCore::inviteToRoom(Room* room, const QString& userId) { {"user_id", userId} }; - network::postJSON("/_matrix/client/r0/rooms/" + room->getId() + "/invite", inviteObject, [](QNetworkReply*) {}); + network->postJSON("/_matrix/client/r0/rooms/" + room->getId() + "/invite", inviteObject, [](QNetworkReply*) {}); } void MatrixCore::updateMembers(Room* room) { if(!room) return; - network::get("/_matrix/client/r0/rooms/" + room->getId() + "/members", [this, room](QNetworkReply* reply) { + network->get("/_matrix/client/r0/rooms/" + room->getId() + "/members", [this, room](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); const QJsonArray& chunk = document.object()["chunk"].toArray(); @@ -620,7 +862,7 @@ void MatrixCore::updateMembers(Room* room) { if(!memberJson["content"].toObject()["avatar_url"].isNull()) { const QString imageId = memberJson["content"].toObject()["avatar_url"].toString().remove("mxc://"); - m->setAvatar(network::homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); + m->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); } idToMember.insert(id, m); @@ -648,7 +890,7 @@ void MatrixCore::readMessageHistory(Room* room) { if(!room || room->prevBatch.isEmpty()) return; - network::get("/_matrix/client/r0/rooms/" + room->getId() + "/messages?from=" + room->prevBatch + "&dir=b", [this, room](QNetworkReply* reply) { + network->get("/_matrix/client/r0/rooms/" + room->getId() + "/messages?from=" + room->prevBatch + "&dir=b", [this, room](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); room->prevBatch = document.object()["end"].toString(); @@ -674,7 +916,7 @@ void MatrixCore::updateMemberCommunities(Member* member) { {"user_ids", userIdsArray} }; - network::postJSON("/_matrix/client/r0/publicised_groups", userIdsObject, [this, member](QNetworkReply* reply) { + network->postJSON("/_matrix/client/r0/publicised_groups", userIdsObject, [this, member](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); for(const auto id : document.object()["users"].toObject()[member->getId()].toArray()) { @@ -698,22 +940,23 @@ void MatrixCore::updateMemberCommunities(Member* member) { } bool MatrixCore::settingsValid() { - QSettings settings; - return settings.contains("accessToken"); + return !network->accessToken.isEmpty() && !network->homeserverURL.isEmpty(); } void MatrixCore::setHomeserver(const QString& url) { - network::homeserverURL = "https://" + url; + network->homeserverURL = "https://" + url; - network::get("/_matrix/client/versions", [this, url](QNetworkReply* reply) { + network->get("/_matrix/client/versions", [this, url](QNetworkReply* reply) { if(!reply->error()) { homeserverURL = url; QSettings settings; + settings.beginGroup(profileName); settings.setValue("homeserver", url); + settings.endGroup(); } - network::homeserverURL = "https://" + homeserverURL; + network->homeserverURL = "https://" + homeserverURL; emit homeserverChanged(reply->error() == 0, reply->errorString()); }); @@ -798,10 +1041,10 @@ QString MatrixCore::getUsername() const { return id.remove('@').split(':')[0]; } -void MatrixCore::loadDirectory() { +void MatrixCore::loadDirectory(const QString& homeserver) { const QJsonObject bodyObject; - network::postJSON("/_matrix/client/r0/publicRooms", bodyObject, [this](QNetworkReply* reply) { + network->postJSON("/_matrix/client/r0/publicRooms?server=" + homeserver, bodyObject, [this](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); if(publicRooms.size() != document.object()["chunk"].toArray().size()) { @@ -820,7 +1063,7 @@ void MatrixCore::loadDirectory() { if(!roomObject["avatar_url"].isNull()) { const QString imageId = roomObject["avatar_url"].toString().remove("mxc://"); - r->setAvatar(network::homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); + r->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); } r->setTopic(roomObject["topic"].toString()); @@ -850,7 +1093,7 @@ void MatrixCore::readUpTo(Room* room, const int index) { if(index < 0) return; - network::post("/_matrix/client/r0/rooms/" + room->getId() + "/receipt/m.read/" + room->events[index]->eventId); + network->post("/_matrix/client/r0/rooms/" + room->getId() + "/receipt/m.read/" + room->events[index]->eventId); } void MatrixCore::setMarkdownEnabled(const bool enabled) { @@ -878,8 +1121,8 @@ EmoteListModel* MatrixCore::getLocalEmoteListModel() { return &localEmoteModel; } -MemberModel* MatrixCore::getMemberModel() { - return &memberModel; +MemberListSortModel* MatrixCore::getMemberModel() { + return &memberSortModel; } QString MatrixCore::getHomeserverURL() const { @@ -910,7 +1153,7 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i }; bool found = false; - if(eventType == "m.room.message") { + if(eventType == "m.room.message" || eventType == "m.room.encrypted") { for(size_t i = 0; i < unsentMessages.size(); i++) { if(event["sender"].toString() == userId && unsentMessages[i]->getRoom() == room.getId()) { found = true; @@ -920,9 +1163,13 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i eventModel.updateEvent(unsentMessages[i]); unsentMessages.removeAt(i); + + return; } } - } else if(eventType == "m.room.member") { + } + + if(eventType == "m.room.member") { // avoid events tied to us if(event["state_key"].toString() == userId) return; @@ -935,11 +1182,75 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i if(!event["content"].toObject()["avatar_url"].isNull()) { const QString imageId = event["content"].toObject()["avatar_url"].toString().remove("mxc://"); - room.setAvatar(network::homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); + room.setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); } } roomListModel.updateRoom(&room); + } else if(eventType == "m.room.encrypted") { + Event* e = new Event(&room); + + if(event["content"].toObject()["device_id"] != deviceId) { + EncryptionInformation* info = new EncryptionInformation(); + e->encryptionInfo = info; + info->cipherText = event["content"].toObject()["ciphertext"].toString(); + info->sessionId = event["content"].toObject()["session_id"].toString(); + + if(!inboundSessions.contains(event["content"].toObject()["session_id"].toString())) { + //qDebug() << "new encrypted event " << event; + e->setMsg("** ERR: we can't decrypt this yet, we do not have the key."); + + qDebug() << "the failed id: " << event["content"].toObject()["session_id"].toString(); + qDebug() << inboundSessions; + + // let's send a room key request event + // construct m.room.key event + const QJsonObject roomKeyObject { + {"action", "request"}, + {"body", QJsonObject { + {"algorithm", event["content"].toObject()["algorithm"]}, + {"room_id", room.getId()}, + {"sender_key", event["content"].toObject()["sender_key"]}, + {"session_id", event["content"].toObject()["session_id"]} + }}, + {"requesting_device_id", deviceId}, + {"request_id", QString("lololol") + QString::number(QRandomGenerator::global()->generate())} + }; + + const QJsonObject sendToDeviceObject { + {"messages", QJsonObject { + { event["sender"].toString(), QJsonObject { + { event["content"].toObject()["device_id"].toString(), roomKeyObject} + }} + }} + }; + + //qDebug() << QJsonDocument(sendToDeviceObject).toJson(QJsonDocument::Compact); + + network->putJSON(QString("/_matrix/client/r0/sendToDevice/m.room_key_request/") + QString::number(QRandomGenerator::global()->generate()), sendToDeviceObject, [](QNetworkReply* reply) { + //qDebug() << "REPLY FROM KEY REQUEST SEND: " << reply->readAll(); + }); + } else { + auto session = inboundSessions[event["content"].toObject()["session_id"].toString()]; + auto msg = encryption->decrypt(session, event["content"].toObject()["ciphertext"].toString().toStdString()); + + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(reinterpret_cast(msg.data()), msg.size())); + + populateEvent(document.object(), e); + + } + } else { + e->setMsg("** ERR: messages sent from the same device are not supported yet."); + } + + e->setSender("placeholder"); + e->eventId = event["event_id"].toString(); + e->setMsgType("text"); + + addEvent(e); + + if(!firstSync && !traversingHistory) + emit message(&room, e->getSender(), e->getMsg()); } else return; @@ -948,50 +1259,9 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i return; if(!found && eventType == "m.room.message") { - const QString msgType = event["content"].toObject()["msgtype"].toString(); - Event* e = new Event(&room); - e->timestamp = QDateTime(QDate::currentDate(), - QTime(QTime::currentTime().hour(), - QTime::currentTime().minute(), - QTime::currentTime().second(), - QTime::currentTime().msec() - event["unsigned"].toObject()["age"].toInt())); - - e->setSender(event["sender"].toString()); - e->eventId = event["event_id"].toString(); - - if(msgType == "m.text" && !event["content"].toObject().contains("formatted_body")) { - e->setMsgType("text"); - e->setMsg(event["content"].toObject()["body"].toString()); - } else if(msgType == "m.text" && event["content"].toObject().contains("formatted_body")) { - e->setMsgType("text"); - e->setMsg(event["content"].toObject()["formatted_body"].toString()); - } else if(msgType == "m.image") { - e->setMsgType("image"); - e->setAttachment(getMXCMediaURL(event["content"].toObject()["url"].toString())); - e->setAttachmentSize(event["content"].toObject()["info"].toObject()["size"].toInt()); - - if(event["content"].toObject()["info"].toObject().contains("thumbnail_url")) - e->setThumbnail(getMXCThumbnailURL(event["content"].toObject()["info"].toObject()["thumbnail_url"].toString())); - else - e->setThumbnail(getMXCMediaURL(event["content"].toObject()["url"].toString())); - - e->setMsg(event["content"].toObject()["body"].toString()); - } else if(msgType == "m.file") { - e->setMsgType("file"); - e->setAttachment(getMXCMediaURL(event["content"].toObject()["url"].toString())); - e->setAttachmentSize(event["content"].toObject()["info"].toObject()["size"].toInt()); - e->setMsg(event["content"].toObject()["body"].toString()); - } else - e->setMsg(event["content"].toObject()["body"].toString()); - - QString msg = e->getMsg(); - for(const auto& emote : emotes) { - msg.replace(":" + emote->name + ":", ""); - } - - e->setMsg(msg); + populateEvent(event, e); addEvent(e); @@ -1000,11 +1270,51 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i } } +void MatrixCore::populateEvent(const QJsonObject& event, Event* e) { + const QString msgType = event["content"].toObject()["msgtype"].toString(); + + e->timestamp = QDateTime::currentDateTime().addMSecs(-event["unsigned"].toObject()["age"].toInt()); + e->setSender(event["sender"].toString()); + e->eventId = event["event_id"].toString(); + + if(msgType == "m.text" && !event["content"].toObject().contains("formatted_body")) { + e->setMsgType("text"); + e->setMsg(event["content"].toObject()["body"].toString()); + } else if(msgType == "m.text" && event["content"].toObject().contains("formatted_body")) { + e->setMsgType("text"); + e->setMsg(event["content"].toObject()["formatted_body"].toString()); + } else if(msgType == "m.image") { + e->setMsgType("image"); + e->setAttachment(getMXCMediaURL(event["content"].toObject()["url"].toString())); + e->setAttachmentSize(event["content"].toObject()["info"].toObject()["size"].toInt()); + + if(event["content"].toObject()["info"].toObject().contains("thumbnail_url")) + e->setThumbnail(getMXCThumbnailURL(event["content"].toObject()["info"].toObject()["thumbnail_url"].toString())); + else + e->setThumbnail(getMXCMediaURL(event["content"].toObject()["url"].toString())); + + e->setMsg(event["content"].toObject()["body"].toString()); + } else if(msgType == "m.file") { + e->setMsgType("file"); + e->setAttachment(getMXCMediaURL(event["content"].toObject()["url"].toString())); + e->setAttachmentSize(event["content"].toObject()["info"].toObject()["size"].toInt()); + e->setMsg(event["content"].toObject()["body"].toString()); + } else + e->setMsg(event["content"].toObject()["body"].toString()); + + QString msg = e->getMsg(); + for(const auto& emote : emotes) { + msg.replace(":" + emote->name + ":", ""); + } + + e->setMsg(msg); +} + Community* MatrixCore::createCommunity(const QString& id) { Community* community = new Community(this); community->setId(id); - network::get("/_matrix/client/r0/groups/" + community->getId() + "/summary", [this, community](QNetworkReply* reply) { + network->get("/_matrix/client/r0/groups/" + community->getId() + "/summary", [this, community](QNetworkReply* reply) { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); const QJsonObject& profile = document.object()["profile"].toObject(); @@ -1013,7 +1323,7 @@ Community* MatrixCore::createCommunity(const QString& id) { if(!profile["avatar_url"].isNull()) { const QString imageId = profile["avatar_url"].toString().remove("mxc://"); - community->setAvatar(network::homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); + community->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"); } community->setShortDescription(profile["short_description"].toString()); @@ -1027,12 +1337,12 @@ Community* MatrixCore::createCommunity(const QString& id) { QString MatrixCore::getMXCThumbnailURL(QString url) { const QString imageId = url.remove("mxc://"); - return network::homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"; + return network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale"; } QString MatrixCore::getMXCMediaURL(QString url) { const QString imageId = url.remove("mxc://"); - return network::homeserverURL + "/_matrix/media/v1/download/" + imageId; + return network->homeserverURL + "/_matrix/media/v1/download/" + imageId; } QString MatrixCore::getDisplayName() const { @@ -1054,3 +1364,102 @@ QString MatrixCore::getTypingText() const { bool MatrixCore::getMarkdownEnabled() const { return markdownEnabled; } + +void MatrixCore::sendKeyToDevice(QString roomId, QString senderCurveIdentity, QString senderEdIdentity, QString session_id, QString session_key, QString user_id, QString device_id) +{ + // why we would we send ourselves a key? + if(device_id == deviceId) + return; + + QJsonObject claimObject { + {"timeout", 10000}, + {"one_time_keys", QJsonObject { + {user_id, QJsonObject { + {device_id, "signed_curve25519"} + }} + }} + }; + + network->postJSON("/_matrix/client/r0/keys/claim", claimObject, [this, device_id, user_id, senderCurveIdentity, senderEdIdentity, roomId, session_id, session_key](QNetworkReply* reply) { + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + + std::string identityKey = senderCurveIdentity.toStdString(); + + // we couldnt claim a key, skip + if(document.object()["one_time_keys"].toObject()[user_id].toObject()[device_id].toObject().keys().empty()) + return; + + std::string oneTimeKey = document.object()["one_time_keys"].toObject()[user_id].toObject()[device_id].toObject()[document.object()["one_time_keys"].toObject()[user_id].toObject()[device_id].toObject().keys()[0]].toObject()["key"].toString().toStdString(); + + auto session = encryption->beginOutboundOlmSession(identityKey, oneTimeKey); + + // construct m.room.key event + const QJsonObject roomKeyObject { + {"content", QJsonObject { + {"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", roomId}, + {"session_id", session_id}, + {"session_key", session_key} + }}, + {"type", "m.room_key"}, + {"keys", QJsonObject { + {"ed25519", encryption->identityKey["ed25519"]} + }}, + {"sender", userId}, + {"sender_device", deviceId}, + {"recipient", user_id}, + {"recipient_keys", QJsonObject { + {"ed25519", senderEdIdentity} + }} + }; + + // construct the m.room.encrypted event + const QJsonObject roomEncryptedObject { + {"algorithm", "m.olm.v1.curve25519-aes-sha2"}, + {"ciphertext", QJsonObject { + {identityKey.c_str(), QJsonObject { + {"body", encryption->encrypt(session, QString(QJsonDocument(roomKeyObject).toJson(QJsonDocument::Compact)).toStdString()).c_str()}, + {"type", (int)olm_encrypt_message_type(session)} + }}}}, + {"sender_key", encryption->identityKey["curve25519"]}, + }; + + const QJsonObject sendToDeviceObject { + {"messages", QJsonObject { + { user_id, QJsonObject { + { device_id, roomEncryptedObject} + }} + }} + }; + + network->putJSON(QString("/_matrix/client/r0/sendToDevice/m.room.encrypted/") + QString::number(QRandomGenerator::global()->generate()), sendToDeviceObject, [](QNetworkReply* reply) { + //qDebug() << "REPLY FROM KEY SEND: " << reply->readAll(); + }); + }); +} + +void MatrixCore::createOrLoadSession() { + if(currentSession != nullptr) + return; + + QSettings settings; + settings.beginGroup(profileName); + + if(settings.contains("sessionPickle")) { + currentSession = encryption->loadSession(settings.value("sessionPickle").toString()); + currentSessionId = encryption->getGroupSessionId(currentSession).c_str(); + currentSessionKey = encryption->getGroupSessionKey(currentSession).c_str(); + } else { + currentSession = encryption->beginOutboundSession(); + + auto session_id = encryption->getGroupSessionId(currentSession); + auto session_key = encryption->getGroupSessionKey(currentSession); + + //qDebug () << "CREATING NEW SESSION WITH ID " << session_id.c_str(); + + currentSessionId = session_id.c_str(); + currentSessionKey = session_key.c_str(); + } + + settings.endGroup(); +} diff --git a/src/membermodel.cpp b/src/membermodel.cpp old mode 100644 new mode 100755 index e439846..8997b88 --- a/src/membermodel.cpp +++ b/src/membermodel.cpp @@ -20,8 +20,17 @@ QVariant MemberModel::data(const QModelIndex &index, int role) const { return room->members.at(index.row())->getDisplayName(); else if(role == AvatarURLRole) return room->members.at(index.row())->getAvatar(); - else + else if(role == IdRole) return room->members.at(index.row())->getId(); + else { + int powerLevel = room->powerLevelList[room->members.at(index.row())->getId()]; + if(powerLevel == 100) + return "Admin"; + else if(powerLevel == 50) + return "Moderator"; + else + return "User"; + } } QHash MemberModel::roleNames() const { @@ -29,6 +38,7 @@ QHash MemberModel::roleNames() const { roles[DisplayNameRole] = "displayName"; roles[AvatarURLRole] = "avatarURL"; roles[IdRole] = "id"; + roles[SectionRole] = "section"; return roles; } diff --git a/src/roomlistsortmodel.cpp b/src/roomlistsortmodel.cpp old mode 100644 new mode 100755 index d640ac9..230b293 --- a/src/roomlistsortmodel.cpp +++ b/src/roomlistsortmodel.cpp @@ -2,6 +2,7 @@ #include "room.h" #include "roomlistmodel.h" +#include "membermodel.h" bool RoomListSortModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { const QString sectionLeft = sourceModel()->data(left, RoomListModel::SectionRole).toString(); @@ -15,3 +16,20 @@ bool RoomListSortModel::lessThan(const QModelIndex& left, const QModelIndex& rig return false; } + +bool MemberListSortModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { + const QString sectionLeft = sourceModel()->data(left, MemberModel::SectionRole).toString(); + const QString sectionRight = sourceModel()->data(right, MemberModel::SectionRole).toString(); + + if(sectionLeft == "Admin" && sectionRight == "Moderator") + return true; + else if(sectionLeft == "Moderator" && sectionRight == "User") + return true; + else if(sectionLeft == "Admin" && sectionRight == "User") + return true; + + if(sectionLeft == sectionRight) + return sourceModel()->data(left, MemberModel::DisplayNameRole).toString() < sourceModel()->data(right, MemberModel::DisplayNameRole).toString(); + + return false; +}