Archived
1
Fork 0

Add basic encryption support

Sorry this is a huge commit, this actually includes a ton
of stuff. Text color is now readable, multiple accounts
are supported alongside end-to-end encryption but no
cross-signing yet :-) There's also a whole lot of other
small changes, such as choosing the server you want to
request a room directory from.
This commit is contained in:
Joshua Goins 2022-03-01 16:20:32 -05:00
parent 303001a357
commit a825c8886d
26 changed files with 1869 additions and 570 deletions

12
CMakeLists.txt Normal file → Executable file
View file

@ -1,11 +1,10 @@
cmake_minimum_required(VERSION 2.8.12) cmake_minimum_required(VERSION 2.8.12)
project(Trinity LANGUAGES CXX) project(Trinity LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC 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} add_executable(${PROJECT_NAME}
src/main.cpp src/main.cpp
@ -29,9 +28,14 @@ add_executable(${PROJECT_NAME}
include/roomlistsortmodel.h include/roomlistsortmodel.h
include/emotelistmodel.h include/emotelistmodel.h
src/emotelistmodel.cpp 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) target_include_directories(${PROJECT_NAME} PRIVATE include)
install(TARGETS ${PROJECT_NAME} DESTINATION bin) install(TARGETS ${PROJECT_NAME} DESTINATION bin)

25
include/appcore.h Executable file
View file

@ -0,0 +1,25 @@
#pragma once
#include <QObject>
#include <vector>
#include <QQmlContext>
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<MatrixCore*> accounts;
QQmlContext* context = nullptr;
signals:
void accountChange();
};

8
include/desktop.h Normal file → Executable file
View file

@ -11,14 +11,18 @@ public:
QApplication::setQuitOnLastWindowClosed(shouldHide); QApplication::setQuitOnLastWindowClosed(shouldHide);
if(shouldHide) if(shouldHide)
icon->hide();
else
icon->show(); icon->show();
else
icon->hide();
} }
Q_INVOKABLE void showMessage(const QString title, const QString content) { Q_INVOKABLE void showMessage(const QString title, const QString content) {
icon->showMessage(title, content); icon->showMessage(title, content);
} }
Q_INVOKABLE bool isTrayIconEnabled() {
return icon->isVisible();;
}
QSystemTrayIcon* icon; QSystemTrayIcon* icon;
}; };

43
include/encryption.h Executable file
View file

@ -0,0 +1,43 @@
#pragma once
#include <QString>
#include <QJsonObject>
#include <olm/olm.h>
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<std::uint8_t> decrypt(OlmSession* session, int msgType, std::string cipherText);
OlmInboundGroupSession* beginInboundSession(std::string sessionKey);
std::vector<std::uint8_t> decrypt(OlmInboundGroupSession* session, std::string cipherText);
QString signMessage(QString message);
QJsonObject identityKey;
private:
void initAccount();
OlmAccount* account = nullptr;
};

31
include/matrixcore.h Normal file → Executable file
View file

@ -12,16 +12,21 @@
#include "roomlistsortmodel.h" #include "roomlistsortmodel.h"
#include "emote.h" #include "emote.h"
#include "emotelistmodel.h" #include "emotelistmodel.h"
#include "encryption.h"
class Network;
class MatrixCore : public QObject class MatrixCore : public QObject
{ {
Q_OBJECT 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(EventModel* eventModel READ getEventModel NOTIFY currentRoomChanged)
Q_PROPERTY(RoomListSortModel* roomListModel READ getRoomListModel NOTIFY roomListChanged) Q_PROPERTY(RoomListSortModel* roomListModel READ getRoomListModel NOTIFY roomListChanged)
Q_PROPERTY(Room* currentRoom READ getCurrentRoom NOTIFY currentRoomChanged) Q_PROPERTY(Room* currentRoom READ getCurrentRoom NOTIFY currentRoomChanged)
Q_PROPERTY(QList<Room*> rooms MEMBER rooms NOTIFY roomListChanged) Q_PROPERTY(QList<Room*> rooms MEMBER rooms NOTIFY roomListChanged)
Q_PROPERTY(QString homeserverURL READ getHomeserverURL NOTIFY homeserverChanged) 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(QString displayName READ getDisplayName NOTIFY displayNameChanged)
Q_PROPERTY(QVariantList joinedCommunities READ getJoinedCommunitiesList NOTIFY joinedCommunitiesChanged) Q_PROPERTY(QVariantList joinedCommunities READ getJoinedCommunitiesList NOTIFY joinedCommunitiesChanged)
Q_PROPERTY(RoomListSortModel* publicRooms READ getDirectoryListModel NOTIFY publicRoomsChanged) 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(bool markdownEnabled READ getMarkdownEnabled WRITE setMarkdownEnabled NOTIFY markdownEnabledChanged)
Q_PROPERTY(EmoteListModel* localEmoteModel READ getLocalEmoteListModel NOTIFY localEmotesChanged) Q_PROPERTY(EmoteListModel* localEmoteModel READ getLocalEmoteListModel NOTIFY localEmotesChanged)
public: public:
MatrixCore(QObject* parent = nullptr); MatrixCore(QString profileName, QObject* parent = nullptr);
Network* network = nullptr;
Encryption* encryption = nullptr;
// account // account
Q_INVOKABLE void registerAccount(const QString& username, const QString& password, const QString& session = "", const QString& type = ""); 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 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 void readUpTo(Room* room, const int index);
Q_INVOKABLE bool isInitialSyncComplete();
void setMarkdownEnabled(const bool enabled); 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<QString, OlmInboundGroupSession*> inboundSessions;
Room* getCurrentRoom(); Room* getCurrentRoom();
EventModel* getEventModel(); EventModel* getEventModel();
RoomListSortModel* getRoomListModel(); RoomListSortModel* getRoomListModel();
RoomListSortModel* getDirectoryListModel(); RoomListSortModel* getDirectoryListModel();
MemberModel* getMemberModel(); MemberListSortModel* getMemberModel();
EmoteListModel* getLocalEmoteListModel(); EmoteListModel* getLocalEmoteListModel();
QString getHomeserverURL() const; QString getHomeserverURL() const;
@ -109,10 +128,13 @@ public:
RoomListModel roomListModel, directoryListModel; RoomListModel roomListModel, directoryListModel;
RoomListSortModel roomListSortModel, directoryListSortModel; RoomListSortModel roomListSortModel, directoryListSortModel;
MemberModel memberModel; MemberModel memberModel;
MemberListSortModel memberSortModel;
EmoteListModel localEmoteModel; EmoteListModel localEmoteModel;
Room* currentRoom = nullptr; Room* currentRoom = nullptr;
QString profileName;
signals: signals:
void registerAttempt(bool error, QString description); void registerAttempt(bool error, QString description);
void registerFlow(QJsonObject data); void registerFlow(QJsonObject data);
@ -132,6 +154,7 @@ signals:
private: private:
void consumeEvent(const QJsonObject& event, Room& room, const bool insertFront = true); void consumeEvent(const QJsonObject& event, Room& room, const bool insertFront = true);
void populateEvent(const QJsonObject& event, Event* e);
Community* createCommunity(const QString& id); Community* createCommunity(const QString& id);
QString getMXCThumbnailURL(QString url); QString getMXCThumbnailURL(QString url);

3
include/membermodel.h Normal file → Executable file
View file

@ -11,7 +11,8 @@ public:
enum EventRoles { enum EventRoles {
DisplayNameRole = Qt::UserRole + 1, DisplayNameRole = Qt::UserRole + 1,
AvatarURLRole, AvatarURLRole,
IdRole IdRole,
SectionRole
}; };
int rowCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override;

13
include/network.h Normal file → Executable file
View file

@ -8,9 +8,14 @@
#include "requestsender.h" #include "requestsender.h"
namespace network { class Network {
extern QNetworkAccessManager* manager; public:
extern QString homeserverURL, accessToken; Network() {
manager = new QNetworkAccessManager();
}
QNetworkAccessManager* manager;
QString homeserverURL, accessToken;
template<typename Fn> template<typename Fn>
inline void postJSON(const QString& path, const QJsonObject object, Fn&& fn) { inline void postJSON(const QString& path, const QJsonObject object, Fn&& fn) {
@ -120,4 +125,4 @@ namespace network {
manager->get(request); manager->get(request);
} }
} };

2
include/requestsender.h Normal file → Executable file
View file

@ -23,6 +23,8 @@ public:
void finished(QNetworkReply* reply) { void finished(QNetworkReply* reply) {
if(reply->request().originatingObject() == this) { if(reply->request().originatingObject() == this) {
//qDebug() << reply->errorString();
fn(reply); fn(reply);
deleteLater(); deleteLater();

24
include/room.h Normal file → Executable file
View file

@ -7,6 +7,13 @@
#include "community.h" #include "community.h"
class EncryptionInformation : public QObject {
Q_OBJECT
public:
QString cipherText;
QString sessionId;
};
class Event : public QObject class Event : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -18,10 +25,12 @@ class Event : public QObject
Q_PROPERTY(QString thumbnail READ getThumbnail NOTIFY thumbnailChanged) Q_PROPERTY(QString thumbnail READ getThumbnail NOTIFY thumbnailChanged)
Q_PROPERTY(bool sent READ getSent NOTIFY sentChanged) Q_PROPERTY(bool sent READ getSent NOTIFY sentChanged)
Q_PROPERTY(double sentProgress READ getSentProgress NOTIFY sentProgressChanged) Q_PROPERTY(double sentProgress READ getSentProgress NOTIFY sentProgressChanged)
Q_PROPERTY(QString eventId MEMBER eventId) Q_PROPERTY(QString eventId MEMBER eventId NOTIFY msgChanged)
public: public:
Event(QObject* parent = nullptr) : QObject(parent) {} Event(QObject* parent = nullptr) : QObject(parent) {}
EncryptionInformation* encryptionInfo = nullptr;
void setSender(const QString& id) { void setSender(const QString& id) {
sender = id; sender = id;
emit senderChanged(); emit senderChanged();
@ -203,9 +212,19 @@ class Room : public QObject
Q_PROPERTY(QString notificationCount READ getNotificationCount NOTIFY notificationCountChanged) Q_PROPERTY(QString notificationCount READ getNotificationCount NOTIFY notificationCountChanged)
Q_PROPERTY(bool direct READ getDirect NOTIFY directChanged) Q_PROPERTY(bool direct READ getDirect NOTIFY directChanged)
Q_PROPERTY(int notificationLevel READ getNotificationLevel WRITE setNotificationLevel NOTIFY notificationLevelChanged) Q_PROPERTY(int notificationLevel READ getNotificationLevel WRITE setNotificationLevel NOTIFY notificationLevelChanged)
Q_PROPERTY(bool encrypted READ getEncrypted NOTIFY encryptionChanged)
public: public:
Room(QObject* parent = nullptr) : QObject(parent) {} Room(QObject* parent = nullptr) : QObject(parent) {}
void setEncrypted() {
this->encrypted = true;
emit encryptionChanged();
}
bool getEncrypted() const {
return encrypted;
}
void setId(const QString& id) { void setId(const QString& id) {
this->id = id; this->id = id;
emit idChanged(); emit idChanged();
@ -309,6 +328,7 @@ public:
QString prevBatch; QString prevBatch;
QList<Member*> members; QList<Member*> members;
QMap<QString, int> powerLevelList;
private: private:
QString id, name, topic, avatar; QString id, name, topic, avatar;
@ -318,6 +338,7 @@ private:
unsigned int highlightCount = 0, notificationCount = 0; unsigned int highlightCount = 0, notificationCount = 0;
bool direct = false; bool direct = false;
int notificationLevel = 1; int notificationLevel = 1;
bool encrypted = false;
signals: signals:
void idChanged(); void idChanged();
@ -331,4 +352,5 @@ signals:
void notificationCountChanged(); void notificationCountChanged();
void directChanged(); void directChanged();
void notificationLevelChanged(); void notificationLevelChanged();
void encryptionChanged();
}; };

17
include/roomlistsortmodel.h Normal file → Executable file
View file

@ -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();
}
};

2
qml.qrc Normal file → Executable file
View file

@ -10,6 +10,8 @@
<file alias="RoomSettings.qml">qml/RoomSettings.qml</file> <file alias="RoomSettings.qml">qml/RoomSettings.qml</file>
<file alias="Profile.qml">qml/Profile.qml</file> <file alias="Profile.qml">qml/Profile.qml</file>
<file alias="Community.qml">qml/Community.qml</file> <file alias="Community.qml">qml/Community.qml</file>
<file alias="ToolBarButton.qml">qml/ToolBarButton.qml</file>
<file alias="RoundedImage.qml">qml/RoundedImage.qml</file>
<file alias="Communities.qml">qml/Communities.qml</file> <file alias="Communities.qml">qml/Communities.qml</file>
<file alias="Directory.qml">qml/Directory.qml</file> <file alias="Directory.qml">qml/Directory.qml</file>
<file alias="InviteDialog.qml">qml/InviteDialog.qml</file> <file alias="InviteDialog.qml">qml/InviteDialog.qml</file>

2
qml/BackButton.qml Normal file → Executable file
View file

@ -27,7 +27,7 @@ Rectangle {
text: "ESC" text: "ESC"
color: "grey" color: myPalette.text
} }
Shortcut { Shortcut {

869
qml/Client.qml Normal file → Executable file

File diff suppressed because it is too large Load diff

4
qml/Dialog.qml Normal file → Executable file
View file

@ -22,7 +22,7 @@ Popup {
text: title text: title
color: "white" color: myPalette.text
} }
Text { Text {
@ -32,7 +32,7 @@ Popup {
anchors.top: titleLabel.bottom anchors.top: titleLabel.bottom
color: "white" color: myPalette.text
} }
Repeater { Repeater {

25
qml/Directory.qml Normal file → Executable file
View file

@ -8,9 +8,7 @@ import trinity.matrix 1.0
Rectangle { Rectangle {
id: roomDirectory id: roomDirectory
color: Qt.rgba(0.1, 0.1, 0.1, 1.0) color: myPalette.window
Component.onCompleted: matrix.loadDirectory()
Rectangle { Rectangle {
width: 700 width: 700
@ -40,14 +38,25 @@ Rectangle {
font.pointSize: 25 font.pointSize: 25
font.bold: true 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 { ListView {
width: parent.width width: parent.width
height: parent.height - backButton.height height: parent.height - backButton.height
anchors.top: directoryLabel.bottom anchors.top: serverEdit.bottom
anchors.topMargin: 10 anchors.topMargin: 10
model: matrix.publicRooms model: matrix.publicRooms
@ -60,7 +69,7 @@ Rectangle {
color: "transparent" color: "transparent"
Image { RoundedImage {
id: roomAvatar id: roomAvatar
width: 32 width: 32
@ -81,7 +90,7 @@ Rectangle {
font.bold: true font.bold: true
color: "white" color: myPalette.text
} }
Text { Text {
@ -98,7 +107,7 @@ Rectangle {
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white" color: myPalette.text
} }
MouseArea { MouseArea {

61
qml/Profile.qml Normal file → Executable file
View file

@ -18,7 +18,7 @@ Popup {
Component.onCompleted: matrix.updateMemberCommunities(member) Component.onCompleted: matrix.updateMemberCommunities(member)
Image { RoundedImage {
id: profileAvatar id: profileAvatar
width: 64 width: 64
@ -38,7 +38,7 @@ Popup {
font.pointSize: 22 font.pointSize: 22
color: "white" color: myPalette.text
} }
Text { Text {
@ -50,7 +50,40 @@ Popup {
text: member.id 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 { TabBar {
@ -61,12 +94,22 @@ Popup {
id: profileTabs id: profileTabs
TabButton {
text: "Security"
}
TabButton { TabButton {
text: "Communities" text: "Communities"
} }
TabButton {
text: "Sessions"
}
} }
SwipeView { SwipeView {
interactive: false
height: parent.height - profileNameLabel.height - profileTabs.height height: parent.height - profileNameLabel.height - profileTabs.height
width: parent.width width: parent.width
@ -75,6 +118,8 @@ Popup {
currentIndex: profileTabs.currentIndex currentIndex: profileTabs.currentIndex
Item { Item {
id: communityTab
ListView { ListView {
id: communityList id: communityList
@ -90,7 +135,7 @@ Popup {
color: "transparent" color: "transparent"
Image { RoundedImage {
id: communityAvatar id: communityAvatar
width: 32 width: 32
@ -110,7 +155,7 @@ Popup {
text: display.name text: display.name
color: "white" color: myPalette.text
} }
MouseArea { MouseArea {
@ -135,11 +180,15 @@ Popup {
text: "This member does not have any public communities." text: "This member does not have any public communities."
color: "white" color: myPalette.text
visible: !member.publicCommunities || member.publicCommunities.length == 0 visible: !member.publicCommunities || member.publicCommunities.length == 0
} }
} }
} }
Item {
id: sessionsTab
}
} }
} }

41
qml/RoomSettings.qml Normal file → Executable file
View file

@ -3,35 +3,48 @@ import QtQuick.Controls 2.3
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import QtQuick.Shapes 1.0 import QtQuick.Shapes 1.0
Rectangle { Popup {
id: roomSettings 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 property var room
Rectangle { Rectangle {
width: 700 width: parent.width
height: parent.height height: parent.height
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: "transparent" color: "transparent"
Button {
id: backButton
text: "Back"
onClicked: stack.pop()
}
TabBar { TabBar {
id: bar id: bar
anchors.top: backButton.bottom TabButton {
text: "General"
}
TabButton { TabButton {
text: "Overview" text: "Security & Privacy"
}
TabButton {
text: "Roles & Permissions"
}
TabButton {
text: "Notifications"
}
TabButton {
text: "Advanced"
} }
} }
@ -53,7 +66,7 @@ Rectangle {
Label { Label {
id: nameLabel id: nameLabel
text: "Name" text: "Room Name"
} }
TextField { TextField {
@ -67,7 +80,7 @@ Rectangle {
Label { Label {
id: topicLabel id: topicLabel
text: "Topic" text: "Room Topic"
anchors.top: nameField.bottom anchors.top: nameField.bottom
} }

36
qml/RoundedImage.qml Executable file
View file

@ -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)
}
}
}
}
}

32
qml/Settings.qml Normal file → Executable file
View file

@ -3,11 +3,12 @@ import QtQuick.Controls 2.3
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import QtQuick.Shapes 1.0 import QtQuick.Shapes 1.0
import QtQuick.Dialogs 1.2 import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.15
Rectangle { Rectangle {
id: settings id: settings
color: Qt.rgba(0.1, 0.1, 0.1, 1.0) color: myPalette.window
Component.onCompleted: matrix.updateAccountInformation() Component.onCompleted: matrix.updateAccountInformation()
@ -19,11 +20,13 @@ Rectangle {
color: "transparent" color: "transparent"
Button { BackButton {
id: backButton id: backButton
text: "Back" anchors.top: parent.top
onClicked: stack.pop() anchors.topMargin: 15
anchors.right: parent.right
} }
TabBar { TabBar {
@ -50,7 +53,7 @@ Rectangle {
} }
} }
SwipeView { StackLayout {
id: settingsStack id: settingsStack
anchors.top: bar.bottom anchors.top: bar.bottom
@ -155,12 +158,27 @@ Rectangle {
id: notificationsTab id: notificationsTab
CheckBox { 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 { Item {
id: appearanceTab id: appearanceTab
CheckBox {
text: "Show developer options"
}
} }
Item { Item {
@ -220,7 +238,7 @@ Rectangle {
text: display.name text: display.name
color: "white" color: myPalette.text
} }
ToolButton { ToolButton {

42
qml/ToolBarButton.qml Executable file
View file

@ -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))
}
}

59
qml/main.qml Normal file → Executable file
View file

@ -9,7 +9,9 @@ ApplicationWindow {
visible: true visible: true
width: 640 width: 640
height: 480 height: 480
title: "Trinity" title: "Trinity " + matrix.profileName
SystemPalette { id: myPalette; colorGroup: SystemPalette.Active }
property var showDialog: function(title, description, buttons) { property var showDialog: function(title, description, buttons) {
var popup = Qt.createComponent("qrc:/Dialog.qml") var popup = Qt.createComponent("qrc:/Dialog.qml")
@ -24,17 +26,60 @@ ApplicationWindow {
} }
Component.onCompleted: { Component.onCompleted: {
if(matrix.settingsValid()) { connect.onAccountChange()
desktop.showTrayIcon(false) }
stack.push("qrc:/Client.qml")
} else { Connections {
desktop.showTrayIcon(true) id: connect
stack.push("qrc:/Login.qml")
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 { StackView {
id: stack id: stack
anchors.fill: parent 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
}
}
} }
} }

337
src/encryption.cpp Executable file
View file

@ -0,0 +1,337 @@
#include "encryption.h"
#include <stdlib.h>
#include <QDebug>
#include <unistd.h>
#include <fcntl.h>
#include <QJsonDocument>
#include <cstring>
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<std::uint8_t> 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<std::uint8_t>(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<std::uint8_t>(length);
// now create another buffer for olm_group_decrypt
auto tmp_buffer = std::vector<std::uint8_t>(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<std::uint8_t>(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<std::uint8_t> Encryption::decrypt(OlmInboundGroupSession* session, std::string cipherText) {
// because olm_group_decrypt_max_plaintext_length DESTROYS the buffer, why?
auto tmp_plaintext_buffer = std::vector<std::uint8_t>(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<std::uint8_t>(length);
// now create another buffer for olm_group_decrypt
auto tmp_buffer = std::vector<std::uint8_t>(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<std::uint8_t>(size);
std::memcpy(output.data(), result_buffer.data(), size);
return output;
}

64
src/main.cpp Normal file → Executable file
View file

@ -9,6 +9,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QApplication> #include <QApplication>
#include <QtQuick/QQuickWindow> #include <QtQuick/QQuickWindow>
#include <QtWebEngine/QtWebEngine>
#include "eventmodel.h" #include "eventmodel.h"
#include "membermodel.h" #include "membermodel.h"
@ -19,23 +20,58 @@
#include "community.h" #include "community.h"
#include "roomlistsortmodel.h" #include "roomlistsortmodel.h"
#include "emote.h" #include "emote.h"
#include "appcore.h"
QNetworkAccessManager* network::manager; void AppCore::addAccount(QString profileName) {
QString network::homeserverURL, network::accessToken; 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[]) { int main(int argc, char* argv[]) {
QApplication app2(argc, argv);
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QCoreApplication::setApplicationName("Trinity"); 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<RequestSender>(); qRegisterMetaType<RequestSender>();
network::manager = new QNetworkAccessManager();
network::homeserverURL = "https://matrix.org";
// matrix // matrix
qmlRegisterUncreatableType<AppCore>("trinity.matrix", 1, 0, "AppCore", "");
qmlRegisterUncreatableType<EventModel>("trinity.matrix", 1, 0, "EventModel", ""); qmlRegisterUncreatableType<EventModel>("trinity.matrix", 1, 0, "EventModel", "");
qmlRegisterUncreatableType<MatrixCore>("trinity.matrix", 1, 0, "MatrixCore", ""); qmlRegisterUncreatableType<MatrixCore>("trinity.matrix", 1, 0, "MatrixCore", "");
qmlRegisterUncreatableType<Room>("trinity.matrix", 1, 0, "Room", ""); qmlRegisterUncreatableType<Room>("trinity.matrix", 1, 0, "Room", "");
@ -51,7 +87,15 @@ int main(int argc, char* argv[]) {
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
QQmlContext* context = new QQmlContext(engine.rootContext(), &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; Desktop desktop;
QSystemTrayIcon* trayIcon = new QSystemTrayIcon(); QSystemTrayIcon* trayIcon = new QSystemTrayIcon();
@ -59,7 +103,7 @@ int main(int argc, char* argv[]) {
desktop.icon = trayIcon; desktop.icon = trayIcon;
context->setContextProperty("desktop", &desktop); context->setContextProperty("desktop", &desktop);
context->setContextProperty("matrix", &matrix); context->setContextProperty("app", core);
QQmlComponent component(&engine); QQmlComponent component(&engine);
component.loadUrl(QUrl("qrc:/main.qml")); component.loadUrl(QUrl("qrc:/main.qml"));

657
src/matrixcore.cpp Normal file → Executable file
View file

@ -10,18 +10,31 @@
#include <QDir> #include <QDir>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QMimeData> #include <QMimeData>
#include <fstream>
#include <iterator>
#include "network.h" #include "network.h"
#include "community.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; QSettings settings;
settings.beginGroup(profileName);
homeserverURL = settings.value("homeserver", "matrix.org").toString(); homeserverURL = settings.value("homeserver", "matrix.org").toString();
userId = settings.value("userId").toString(); userId = settings.value("userId").toString();
network::homeserverURL = "https://" + homeserverURL; network->homeserverURL = "https://" + homeserverURL;
deviceId = settings.value("deviceId").toString();
if(settings.contains("accessToken")) 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.setName("Empty");
emptyRoom.setTopic("There is nothing here."); emptyRoom.setTopic("There is nothing here.");
@ -33,6 +46,13 @@ MatrixCore::MatrixCore(QObject* parent) : QObject(parent), roomListModel(rooms),
roomListSortModel.sort(0); roomListSortModel.sort(0);
}); });
memberSortModel.setSourceModel(&memberModel);
memberSortModel.setSortRole(RoomListModel::SectionRole);
connect(this, &MatrixCore::currentRoomChanged, [this] {
memberSortModel.sort(0);
});
directoryListSortModel.setSourceModel(&directoryListModel); directoryListSortModel.setSourceModel(&directoryListModel);
updateAccountInformation(); updateAccountInformation();
@ -74,7 +94,7 @@ void MatrixCore::registerAccount(const QString &username, const QString &passwor
{"password", password} {"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()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(reply->error()) { if(reply->error()) {
@ -103,12 +123,14 @@ void MatrixCore::registerAccount(const QString &username, const QString &passwor
emit registerAttempt(true, document.object()["error"].toString()); emit registerAttempt(true, document.object()["error"].toString());
} }
} else { } else {
network::accessToken = "Bearer " + document.object()["access_token"].toString(); network->accessToken = "Bearer " + document.object()["access_token"].toString();
QSettings settings; QSettings settings;
settings.beginGroup(profileName);
settings.setValue("accessToken", document.object()["access_token"].toString()); settings.setValue("accessToken", document.object()["access_token"].toString());
settings.setValue("userId", document.object()["user_id"].toString()); settings.setValue("userId", document.object()["user_id"].toString());
settings.setValue("deviceId", document.object()["device_id"].toString()); settings.setValue("deviceId", document.object()["device_id"].toString());
settings.endGroup();
emit registerAttempt(false, ""); emit registerAttempt(false, "");
} }
@ -123,26 +145,83 @@ void MatrixCore::login(const QString& username, const QString& password) {
{"initial_device_display_name", "Trinity"} {"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()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(reply->error()) { if(reply->error()) {
emit loginAttempt(true, document.object()["error"].toString()); emit loginAttempt(true, document.object()["error"].toString());
} else { } else {
network::accessToken = "Bearer " + document.object()["access_token"].toString(); network->accessToken = "Bearer " + document.object()["access_token"].toString();
QSettings settings; QSettings settings;
settings.beginGroup(profileName);
settings.setValue("accessToken", document.object()["access_token"].toString()); settings.setValue("accessToken", document.object()["access_token"].toString());
settings.setValue("userId", document.object()["user_id"].toString()); settings.setValue("userId", document.object()["user_id"].toString());
settings.setValue("deviceId", document.object()["device_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, ""); emit loginAttempt(false, "");
} }
}); });
} }
void MatrixCore::logout() { void MatrixCore::logout() {
network::post("/_matrix/client/r0/logout"); network->post("/_matrix/client/r0/logout");
QSettings settings; QSettings settings;
settings.remove("accessToken"); settings.remove("accessToken");
@ -152,7 +231,7 @@ void MatrixCore::logout() {
} }
void MatrixCore::updateAccountInformation() { 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()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
displayName = document.object()["displayname"].toString(); displayName = document.object()["displayname"].toString();
@ -167,26 +246,98 @@ void MatrixCore::setDisplayName(const QString& name) {
{"displayname", 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(); emit displayNameChanged();
}); });
} }
void MatrixCore::sync() { void MatrixCore::sync() {
if(network::accessToken.isEmpty()) if(network->accessToken.isEmpty())
return; return;
QString url = "/_matrix/client/r0/sync"; QString url = "/_matrix/client/r0/sync";
if(!nextBatch.isEmpty()) if(!nextBatch.isEmpty())
url += "?since=" + nextBatch; url += "?since=" + nextBatch;
network::get(url, [this](QNetworkReply* reply) { network->get(url, [this](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(!document.object()["next_batch"].isNull()) if(!document.object()["next_batch"].isNull())
nextBatch = document.object()["next_batch"].toString(); 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<const char*>(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<const char*>(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(); roomListModel.beginInsertRoom();
Room* room = new Room(this); Room* room = new Room(this);
@ -201,45 +352,65 @@ void MatrixCore::sync() {
room->setNotificationLevel(1); room->setNotificationLevel(1);
settings.endGroup(); settings.endGroup();
network::get("/_matrix/client/r0/rooms/" + id + "/state/m.room.name", [this, room](QNetworkReply* reply) { if(autofill_data) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.name", [this, room](QNetworkReply* reply) {
if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
room->setGuestDenied(true); if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") {
return; room->setGuestDenied(true);
} else if(document.object()["errcode"].toString() == "M_NOT_FOUND") return;
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) { network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.topic", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") {
room->setGuestDenied(true); room->setGuestDenied(true);
return; 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) { network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.avatar", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") { if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") {
room->setGuestDenied(true); room->setGuestDenied(true);
return; return;
} }
if(document.object().contains("url")) { if(document.object().contains("url")) {
const QString imageId = document.object()["url"].toString().remove("mxc://"); const QString imageId = document.object()["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); 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); rooms.push_back(room);
idToRoom.insert(id, room); idToRoom.insert(id, room);
@ -255,7 +426,8 @@ void MatrixCore::sync() {
for(const auto id : document.object()["rooms"].toObject()["invite"].toObject().keys()) { for(const auto id : document.object()["rooms"].toObject()["invite"].toObject().keys()) {
if(!invitedRooms.count(id)) { 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()) { for(auto event : document.object()["rooms"].toObject()["invite"].toObject()[id].toObject()["invite_state"].toObject()["events"].toArray()) {
const QString type = event.toObject()["type"].toString(); const QString type = event.toObject()["type"].toString();
@ -266,7 +438,7 @@ void MatrixCore::sync() {
room->setName(event.toObject()["content"].toObject()["name"].toString()); room->setName(event.toObject()["content"].toObject()["name"].toString());
} else if(type == "m.room.avatar") { } else if(type == "m.room.avatar") {
const QString imageId = event.toObject()["content"].toObject()["url"].toString().remove("mxc://"); 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); roomListModel.updateRoom(room);
@ -393,6 +565,10 @@ void MatrixCore::sync() {
}); });
} }
bool MatrixCore::isInitialSyncComplete() {
return !firstSync;
}
void MatrixCore::sendMessage(Room* room, const QString& message) { void MatrixCore::sendMessage(Room* room, const QString& message) {
if(message.isEmpty()) if(message.isEmpty())
return; return;
@ -437,8 +613,9 @@ void MatrixCore::sendMessage(Room* room, const QString& message) {
shouldSendAsMarkdown = strlen(formatted) > 8 + message.length(); shouldSendAsMarkdown = strlen(formatted) > 8 + message.length();
} }
QJsonObject messageObject;
if(shouldSendAsMarkdown) { if(shouldSendAsMarkdown) {
const QJsonObject messageObject { messageObject = QJsonObject {
{"msgtype", "m.text"}, {"msgtype", "m.text"},
{"formatted_body", formatted}, {"formatted_body", formatted},
{"body", message}, {"body", message},
@ -446,15 +623,80 @@ void MatrixCore::sendMessage(Room* room, const QString& message) {
}; };
e->setMsg(formatted); e->setMsg(formatted);
network::putJSON("/_matrix/client/r0/rooms/" + room->getId() + "/send/m.room.message/" + QRandomGenerator::global()->generate(), messageObject, onMessageFeedbackReceived);
} else { } else {
const QJsonObject messageObject { messageObject = QJsonObject {
{"msgtype", "m.text"}, {"msgtype", "m.text"},
{"body", message} {"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", ""} {"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; auto& events = currentRoom->events;
for(int i = 0; i < events.size(); i++) { for(int i = 0; i < events.size(); i++) {
if(events[i]->eventId == eventId) { if(events[i]->eventId == eventId) {
@ -506,7 +748,7 @@ void MatrixCore::uploadAttachment(Room* room, const QString& path) {
e->setMsg(fileName); e->setMsg(fileName);
e->setMsgType(mimeType.name().contains("image") ? "image" : "file"); 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()) { if(!reply->error()) {
e->setSent(true); e->setSent(true);
@ -524,7 +766,7 @@ void MatrixCore::uploadAttachment(Room* room, const QString& path) {
{"info", infoObject} {"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](qint64 sent, qint64 total) {
e->setSentProgress((double)sent / (double)total); e->setSentProgress((double)sent / (double)total);
@ -540,7 +782,7 @@ void MatrixCore::startDirectChat(const QString& id) {
{"invite", QJsonArray{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) { void MatrixCore::setTyping(Room* room) {
@ -549,11 +791,11 @@ void MatrixCore::setTyping(Room* room) {
{"timeout", 15000} {"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) { 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()) { if(!reply->error()) {
//check if its by an invite //check if its by an invite
if(invitedRooms.contains(id)) { if(invitedRooms.contains(id)) {
@ -576,7 +818,7 @@ void MatrixCore::joinRoom(const QString& id) {
} }
void MatrixCore::leaveRoom(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) { void MatrixCore::inviteToRoom(Room* room, const QString& userId) {
@ -584,14 +826,14 @@ void MatrixCore::inviteToRoom(Room* room, const QString& userId) {
{"user_id", 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) { void MatrixCore::updateMembers(Room* room) {
if(!room) if(!room)
return; 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 QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
const QJsonArray& chunk = document.object()["chunk"].toArray(); const QJsonArray& chunk = document.object()["chunk"].toArray();
@ -620,7 +862,7 @@ void MatrixCore::updateMembers(Room* room) {
if(!memberJson["content"].toObject()["avatar_url"].isNull()) { if(!memberJson["content"].toObject()["avatar_url"].isNull()) {
const QString imageId = memberJson["content"].toObject()["avatar_url"].toString().remove("mxc://"); 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); idToMember.insert(id, m);
@ -648,7 +890,7 @@ void MatrixCore::readMessageHistory(Room* room) {
if(!room || room->prevBatch.isEmpty()) if(!room || room->prevBatch.isEmpty())
return; 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()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
room->prevBatch = document.object()["end"].toString(); room->prevBatch = document.object()["end"].toString();
@ -674,7 +916,7 @@ void MatrixCore::updateMemberCommunities(Member* member) {
{"user_ids", userIdsArray} {"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()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
for(const auto id : document.object()["users"].toObject()[member->getId()].toArray()) { for(const auto id : document.object()["users"].toObject()[member->getId()].toArray()) {
@ -698,22 +940,23 @@ void MatrixCore::updateMemberCommunities(Member* member) {
} }
bool MatrixCore::settingsValid() { bool MatrixCore::settingsValid() {
QSettings settings; return !network->accessToken.isEmpty() && !network->homeserverURL.isEmpty();
return settings.contains("accessToken");
} }
void MatrixCore::setHomeserver(const QString& url) { 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()) { if(!reply->error()) {
homeserverURL = url; homeserverURL = url;
QSettings settings; QSettings settings;
settings.beginGroup(profileName);
settings.setValue("homeserver", url); settings.setValue("homeserver", url);
settings.endGroup();
} }
network::homeserverURL = "https://" + homeserverURL; network->homeserverURL = "https://" + homeserverURL;
emit homeserverChanged(reply->error() == 0, reply->errorString()); emit homeserverChanged(reply->error() == 0, reply->errorString());
}); });
@ -798,10 +1041,10 @@ QString MatrixCore::getUsername() const {
return id.remove('@').split(':')[0]; return id.remove('@').split(':')[0];
} }
void MatrixCore::loadDirectory() { void MatrixCore::loadDirectory(const QString& homeserver) {
const QJsonObject bodyObject; 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()); const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(publicRooms.size() != document.object()["chunk"].toArray().size()) { if(publicRooms.size() != document.object()["chunk"].toArray().size()) {
@ -820,7 +1063,7 @@ void MatrixCore::loadDirectory() {
if(!roomObject["avatar_url"].isNull()) { if(!roomObject["avatar_url"].isNull()) {
const QString imageId = roomObject["avatar_url"].toString().remove("mxc://"); 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()); r->setTopic(roomObject["topic"].toString());
@ -850,7 +1093,7 @@ void MatrixCore::readUpTo(Room* room, const int index) {
if(index < 0) if(index < 0)
return; 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) { void MatrixCore::setMarkdownEnabled(const bool enabled) {
@ -878,8 +1121,8 @@ EmoteListModel* MatrixCore::getLocalEmoteListModel() {
return &localEmoteModel; return &localEmoteModel;
} }
MemberModel* MatrixCore::getMemberModel() { MemberListSortModel* MatrixCore::getMemberModel() {
return &memberModel; return &memberSortModel;
} }
QString MatrixCore::getHomeserverURL() const { QString MatrixCore::getHomeserverURL() const {
@ -910,7 +1153,7 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i
}; };
bool found = false; 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++) { for(size_t i = 0; i < unsentMessages.size(); i++) {
if(event["sender"].toString() == userId && unsentMessages[i]->getRoom() == room.getId()) { if(event["sender"].toString() == userId && unsentMessages[i]->getRoom() == room.getId()) {
found = true; found = true;
@ -920,9 +1163,13 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i
eventModel.updateEvent(unsentMessages[i]); eventModel.updateEvent(unsentMessages[i]);
unsentMessages.removeAt(i); unsentMessages.removeAt(i);
return;
} }
} }
} else if(eventType == "m.room.member") { }
if(eventType == "m.room.member") {
// avoid events tied to us // avoid events tied to us
if(event["state_key"].toString() == userId) if(event["state_key"].toString() == userId)
return; return;
@ -935,11 +1182,75 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i
if(!event["content"].toObject()["avatar_url"].isNull()) { if(!event["content"].toObject()["avatar_url"].isNull()) {
const QString imageId = event["content"].toObject()["avatar_url"].toString().remove("mxc://"); 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); 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<const char*>(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 } else
return; return;
@ -948,50 +1259,9 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i
return; return;
if(!found && eventType == "m.room.message") { if(!found && eventType == "m.room.message") {
const QString msgType = event["content"].toObject()["msgtype"].toString();
Event* e = new Event(&room); Event* e = new Event(&room);
e->timestamp = QDateTime(QDate::currentDate(), populateEvent(event, e);
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 + ":", "<img src='file://" + emote->path + "' width=22 height=22/>");
}
e->setMsg(msg);
addEvent(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 + ":", "<img src='file://" + emote->path + "' width=22 height=22/>");
}
e->setMsg(msg);
}
Community* MatrixCore::createCommunity(const QString& id) { Community* MatrixCore::createCommunity(const QString& id) {
Community* community = new Community(this); Community* community = new Community(this);
community->setId(id); 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 QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
const QJsonObject& profile = document.object()["profile"].toObject(); const QJsonObject& profile = document.object()["profile"].toObject();
@ -1013,7 +1323,7 @@ Community* MatrixCore::createCommunity(const QString& id) {
if(!profile["avatar_url"].isNull()) { if(!profile["avatar_url"].isNull()) {
const QString imageId = profile["avatar_url"].toString().remove("mxc://"); 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()); community->setShortDescription(profile["short_description"].toString());
@ -1027,12 +1337,12 @@ Community* MatrixCore::createCommunity(const QString& id) {
QString MatrixCore::getMXCThumbnailURL(QString url) { QString MatrixCore::getMXCThumbnailURL(QString url) {
const QString imageId = url.remove("mxc://"); 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) { QString MatrixCore::getMXCMediaURL(QString url) {
const QString imageId = url.remove("mxc://"); 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 { QString MatrixCore::getDisplayName() const {
@ -1054,3 +1364,102 @@ QString MatrixCore::getTypingText() const {
bool MatrixCore::getMarkdownEnabled() const { bool MatrixCore::getMarkdownEnabled() const {
return markdownEnabled; 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();
}

12
src/membermodel.cpp Normal file → Executable file
View file

@ -20,8 +20,17 @@ QVariant MemberModel::data(const QModelIndex &index, int role) const {
return room->members.at(index.row())->getDisplayName(); return room->members.at(index.row())->getDisplayName();
else if(role == AvatarURLRole) else if(role == AvatarURLRole)
return room->members.at(index.row())->getAvatar(); return room->members.at(index.row())->getAvatar();
else else if(role == IdRole)
return room->members.at(index.row())->getId(); 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<int, QByteArray> MemberModel::roleNames() const { QHash<int, QByteArray> MemberModel::roleNames() const {
@ -29,6 +38,7 @@ QHash<int, QByteArray> MemberModel::roleNames() const {
roles[DisplayNameRole] = "displayName"; roles[DisplayNameRole] = "displayName";
roles[AvatarURLRole] = "avatarURL"; roles[AvatarURLRole] = "avatarURL";
roles[IdRole] = "id"; roles[IdRole] = "id";
roles[SectionRole] = "section";
return roles; return roles;
} }

18
src/roomlistsortmodel.cpp Normal file → Executable file
View file

@ -2,6 +2,7 @@
#include "room.h" #include "room.h"
#include "roomlistmodel.h" #include "roomlistmodel.h"
#include "membermodel.h"
bool RoomListSortModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { bool RoomListSortModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
const QString sectionLeft = sourceModel()->data(left, RoomListModel::SectionRole).toString(); const QString sectionLeft = sourceModel()->data(left, RoomListModel::SectionRole).toString();
@ -15,3 +16,20 @@ bool RoomListSortModel::lessThan(const QModelIndex& left, const QModelIndex& rig
return false; 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;
}