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 {

601
qml/Client.qml Normal file → Executable file
View file

@ -1,22 +1,130 @@
import QtQuick 2.10 import QtQuick 2.15
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import QtQuick.Dialogs 1.2 import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.1
import trinity.matrix 1.0 import trinity.matrix 1.0
Rectangle { Rectangle {
id: client id: client
color: Qt.rgba(0.05, 0.05, 0.05, 1.0) color: myPalette.window
property bool shouldScroll: false property bool shouldScroll: false
ListView { property var openProfile: function(member) {
var popup = Qt.createComponent("qrc:/Profile.qml")
var popupContainer = popup.createObject(window, {"parent": window, "member": member})
popupContainer.open()
}
property var openRoom: function(room) {
var popup = Qt.createComponent("qrc:/RoomSettings.qml")
var popupContainer = popup.createObject(window, {"parent": window, "room": room})
popupContainer.open()
}
Rectangle {
anchors.fill: parent
visible: !matrix.initialSyncComplete
z: 1000
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
BusyIndicator {
id: syncIndicator
anchors.centerIn: parent
}
Text {
anchors.top: syncIndicator.bottom
anchors.horizontalCenter: parent.horizontalCenter
text: "Synchronizing events..."
}
Button {
anchors.bottom: parent.bottom
anchors.bottomMargin: 15
anchors.horizontalCenter: parent.horizontalCenter
text: "Log out"
}
}
Rectangle {
id: channels id: channels
width: 180 width: 180
height: parent.height
anchors.right: rightArea.left anchors.left: parent.left
anchors.left: client.left anchors.top: parent.top
anchors.bottom: parent.bottom
Rectangle {
id: profileRect
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 45
color: myPalette.mid
Text {
text: "Placeholder name"
}
Button {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 10
text: "Switch"
onClicked: accountMenu.popup()
Menu {
id: accountMenu
Repeater {
model: app.accounts
MenuItem {
text: modelData.profileName
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 model: matrix.roomListModel
@ -36,27 +144,26 @@ Rectangle {
text: section text: section
color: Qt.rgba(0.8, 0.8, 0.8, 1.0) color: myPalette.text
textFormat: Text.PlainText textFormat: Text.PlainText
} }
} }
delegate: Rectangle { delegate: Rectangle {
width: parent.width anchors.left: parent.left
anchors.right: parent.right
height: 25 height: 25
property bool selected: channels.currentIndex === matrix.roomListModel.getOriginalIndex(index) property bool selected: channelListView.currentIndex === matrix.roomListModel.getOriginalIndex(index)
color: selected ? "white" : "transparent" color: selected ? myPalette.highlight : "transparent"
radius: 5 radius: 5
Image { RoundedImage {
id: roomAvatar id: roomAvatar
cache: true
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 5 anchors.topMargin: 5
anchors.left: parent.left anchors.left: parent.left
@ -65,24 +172,7 @@ Rectangle {
width: 18 width: 18
height: 18 height: 18
sourceSize.width: 18
sourceSize.height: 18
source: avatarURL ? avatarURL : "placeholder.png" source: avatarURL ? avatarURL : "placeholder.png"
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)
}
}
}
} }
Text { Text {
@ -92,10 +182,13 @@ Rectangle {
anchors.left: roomAvatar.right anchors.left: roomAvatar.right
anchors.leftMargin: 5 anchors.leftMargin: 5
anchors.right: parent.right
anchors.rightMargin: 5
color: selected ? "black" : (highlightCount > 0 ? "red" : (notificationCount > 0 ? "blue" : "white")) color: selected ? "white" : (highlightCount > 0 ? "red" : (notificationCount > 0 ? "blue" : myPalette.text))
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight
} }
MouseArea { MouseArea {
@ -110,7 +203,7 @@ Rectangle {
if(!selected) { if(!selected) {
var originalIndex = matrix.roomListModel.getOriginalIndex(index) var originalIndex = matrix.roomListModel.getOriginalIndex(index)
matrix.changeCurrentRoom(originalIndex) matrix.changeCurrentRoom(originalIndex)
channels.currentIndex = originalIndex channelListView.currentIndex = originalIndex
} }
} else } else
contextMenu.popup() contextMenu.popup()
@ -172,9 +265,9 @@ Rectangle {
MenuSeparator {} MenuSeparator {}
MenuItem { MenuItem {
text: "Room Settings" text: "Settings"
onReleased: stack.push("qrc:/RoomSettings.qml", {"room": matrix.getRoom(matrix.roomListModel.getOriginalIndex(index))}) onReleased: openRoom(matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)))
} }
MenuSeparator {} MenuSeparator {}
@ -204,12 +297,22 @@ Rectangle {
} }
} }
Button {
width: parent.width
anchors.bottom: directoryButton.top
text: "Switch account"
onClicked: app.addAccount("test")
}
Button { Button {
id: communitiesButton id: communitiesButton
width: channels.width width: parent.width
anchors.bottom: channels.bottom anchors.bottom: parent.bottom
text: "Communities" text: "Communities"
@ -219,7 +322,7 @@ Rectangle {
Button { Button {
id: directoryButton id: directoryButton
width: channels.width width: parent.width
anchors.bottom: communitiesButton.top anchors.bottom: communitiesButton.top
@ -227,29 +330,74 @@ Rectangle {
onClicked: stack.push("qrc:/Directory.qml") onClicked: stack.push("qrc:/Directory.qml")
} }
}
Rectangle { Rectangle {
id: rightArea id: rightArea
height: parent.height height: parent.height
width: parent.width - channels.width
anchors.left: channels.right 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 { Rectangle {
id: roomHeader id: roomHeader
height: 45 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 id: channelAvatar
cache: true
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 15 anchors.leftMargin: 15
@ -257,32 +405,13 @@ Rectangle {
width: 33 width: 33
height: 33 height: 33
sourceSize.width: 33
sourceSize.height: 33
fillMode: Image.PreserveAspectFit
source: matrix.currentRoom.avatar ? matrix.currentRoom.avatar : "placeholder.png" 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 { Text {
id: channelTitle id: channelTitle
font.pointSize: 15 font.pointSize: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 15 anchors.leftMargin: 15
@ -290,22 +419,31 @@ Rectangle {
text: matrix.currentRoom.name text: matrix.currentRoom.name
color: "white" color: myPalette.text
textFormat: Text.PlainText textFormat: Text.PlainText
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onReleased: openRoom(matrix.currentRoom)
}
} }
Text { Text {
id: channelTopic id: channelTopic
width: showMemberListButton.x - x
font.pointSize: 12 font.pointSize: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
anchors.left: channelTitle.right anchors.left: channelTitle.right
anchors.leftMargin: 5 anchors.leftMargin: 10
anchors.right: audioCallButton.left
text: { text: {
if(matrix.currentRoom.direct) if(matrix.currentRoom.direct)
@ -317,7 +455,7 @@ Rectangle {
return matrix.currentRoom.topic return matrix.currentRoom.topic
} }
color: "gray" color: myPalette.text
elide: Text.ElideRight elide: Text.ElideRight
@ -332,105 +470,94 @@ Rectangle {
textFormat: Text.PlainText textFormat: Text.PlainText
} }
ToolButton { ToolBarButton {
id: showMemberListButton id: videoCallButton
width: 25 anchors.verticalCenter: parent.verticalCenter
height: 25
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.verticalCenter: parent.verticalCenter
anchors.right: settingsButton.left anchors.right: settingsButton.left
anchors.rightMargin: 10 anchors.rightMargin: 10
onClicked: { onPressed: {
if(memberList.width == 0) if(memberList.width == 0)
memberList.width = 200 memberList.width = 200
else else
memberList.width = 0 memberList.width = 0
} }
ToolTip.visible: hovered name: "Room Info"
ToolTip.text: "Member List" toolIcon: "icons/memberlist.png"
isActivated: memberList.width == 200
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 { ToolBarButton {
anchors.fill: parent
source: memberListButtonImage
color: parent.hovered ? "white" : (memberList.width == 200 ? "white" : Qt.rgba(0.8, 0.8, 0.8, 1.0))
}
}
ToolButton {
id: settingsButton id: settingsButton
width: 25
height: 25
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 15 anchors.rightMargin: 15
onClicked: stack.push("qrc:/Settings.qml") onPressed: stack.push("qrc:/Settings.qml")
name: "Settings"
ToolTip.visible: hovered toolIcon: "icons/settings.png"
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)
}
} }
} }
Rectangle { Rectangle {
id: messagesArea id: messagesArea
width: parent.width - memberList.width
height: parent.height - roomHeader.height
anchors.top: roomHeader.bottom anchors.top: roomHeader.bottom
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.right: memberList.left
Rectangle { Rectangle {
height: parent.height - messageInputParent.height anchors.top: parent.top
width: parent.width anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: messageInputParent.top
clip: true clip: true
color: Qt.rgba(0.1, 0.1, 0.1, 1.0) color: myPalette.light
ListView { ListView {
id: messages id: messages
@ -441,7 +568,10 @@ Rectangle {
cacheBuffer: 200 cacheBuffer: 200
delegate: Rectangle { delegate: Rectangle {
width: parent.width anchors.left: messages.contentItem.left
anchors.right: messages.contentItem.right
anchors.margins: 10
height: (condense ? 5 : 25) + messageArea.height height: (condense ? 5 : 25) + messageArea.height
color: "transparent" color: "transparent"
@ -451,47 +581,31 @@ Rectangle {
property var eventId: display.eventId property var eventId: display.eventId
property var msg: display.msg property var msg: display.msg
Image { RoundedImage {
id: avatar id: avatar
width: 33 width: 33
height: 33 height: 33
cache: true
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 5 anchors.topMargin: 5
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 5 anchors.leftMargin: 5
sourceSize.width: 33 source: sender == null ? "placeholder.png"
sourceSize.height: 33 : (sender.avatarURL ? sender.avatarURL : "placeholder.png")
source: sender.avatarURL ? sender.avatarURL : "placeholder.png"
visible: !condense 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 { Text {
id: senderText 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.left: avatar.right
anchors.leftMargin: 10 anchors.leftMargin: 10
@ -499,10 +613,23 @@ Rectangle {
textFormat: Text.PlainText 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 {
text: condense ? "" : timestamp text: condense ? "" : timestamp
color: "gray" color: myPalette.dark
anchors.left: senderText.right anchors.left: senderText.right
anchors.leftMargin: 10 anchors.leftMargin: 10
@ -524,27 +651,27 @@ Rectangle {
return preview.height return preview.height
} }
width: parent.width
anchors.left: condense ? parent.left : avatar.right anchors.left: condense ? parent.left : avatar.right
anchors.leftMargin: condense ? 48 : 10 anchors.leftMargin: condense ? 48 : 10
anchors.right: parent.right
color: "transparent" color: "transparent"
TextEdit { TextEdit {
id: message id: message
text: display.msg
width: parent.width width: parent.width
wrapMode: Text.Wrap text: display.msg
wrapMode: Text.WordWrap
textFormat: Text.RichText textFormat: Text.RichText
readOnly: true readOnly: true
selectByMouse: true selectByMouse: true
color: display.sent ? "white" : "gray" color: display.sent ? myPalette.text : myPalette.mid
visible: display.msgType === "text" visible: display.msgType === "text"
} }
@ -696,35 +823,10 @@ Rectangle {
onContentHeightChanged: { onContentHeightChanged: {
var index = indexAt(0, contentY + height - 5) var index = indexAt(0, contentY + height - 5)
matrix.readUpTo(matrix.currentRoom, index) matrix.readUpTo(matrix.currentRoom, index)
}
}
Rectangle { //print(contentHeight + "," + height);
id: overlay if(contentHeight < height) {
matrix.readMessageHistory(matrix.currentRoom)
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)
} }
} }
} }
@ -733,12 +835,12 @@ Rectangle {
Rectangle { Rectangle {
id: messageInputParent id: messageInputParent
anchors.top: messages.parent.bottom anchors.bottom: parent.bottom
width: parent.width width: parent.width
height: 55 height: 55
color: Qt.rgba(0.1, 0.1, 0.1, 1.0) color: "transparent"
ToolButton { ToolButton {
id: attachButton id: attachButton
@ -775,7 +877,7 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 5 anchors.rightMargin: 5
placeholderText: "Message " + matrix.currentRoom.name placeholderText: "Send a " + (matrix.currentRoom.encrypted ? "encrypted" : "unencrypted") + " message to " + matrix.currentRoom.name
Keys.onReturnPressed: { Keys.onReturnPressed: {
if(event.modifiers & Qt.ShiftModifier) { if(event.modifiers & Qt.ShiftModifier) {
@ -836,7 +938,7 @@ Rectangle {
anchors.bottom: messageInputParent.bottom anchors.bottom: messageInputParent.bottom
color: "white" color: myPalette.text
text: matrix.typingText text: matrix.typingText
@ -849,53 +951,66 @@ Rectangle {
id: memberList id: memberList
anchors.top: roomHeader.bottom 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 width: matrix.currentRoom.direct ? 0 : 200
height: parent.height - roomHeader.height height: parent.height - roomHeader.height
ListView { ListView {
id: memberListView
model: matrix.memberModel 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 { delegate: Rectangle {
width: parent.width width: memberListView.contentItem.width
height: 50 height: 50
color: "transparent" color: "transparent"
property string memberId: id property string memberId: id
Image { RoundedImage {
id: memberAvatar id: memberAvatar
cache: true width: 33
height: 33
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 10 anchors.leftMargin: 10
sourceSize.width: 33
sourceSize.height: 33
source: avatarURL ? avatarURL : "placeholder.png" 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 { Text {
@ -904,7 +1019,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: "white" color: myPalette.text
text: displayName text: displayName
@ -914,9 +1029,17 @@ Rectangle {
MouseArea { MouseArea {
anchors.fill: parent 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 { Menu {
@ -925,12 +1048,7 @@ Rectangle {
MenuItem { MenuItem {
text: "Profile" text: "Profile"
onReleased: { onReleased: openProfile(matrix.resolveMemberId(id))
var popup = Qt.createComponent("qrc:/Profile.qml")
var popupContainer = popup.createObject(client, {"parent": client, "member": matrix.resolveMemberId(id)})
popupContainer.open()
}
} }
MenuItem { MenuItem {
@ -970,15 +1088,14 @@ Rectangle {
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
} }
}
Button { Button {
id: inviteButton id: inviteButton
width: memberList.width anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.bottom: memberList.bottom anchors.right: parent.right
anchors.right: memberList.right anchors.margins: 8
text: "Invite to room" text: "Invite to room"
@ -990,6 +1107,7 @@ Rectangle {
} }
} }
} }
}
Timer { Timer {
id: syncTimer id: syncTimer
@ -1021,21 +1139,24 @@ Rectangle {
Connections { Connections {
target: matrix target: matrix
onSyncFinished: {
function onSyncFinished() {
syncTimer.start() syncTimer.start()
if(shouldScroll) if(shouldScroll)
messages.positionViewAtEnd() messages.positionViewAtEnd()
} }
onInitialSyncFinished: matrix.changeCurrentRoom(0) function onInitialSyncFinished() {
matrix.changeCurrentRoom(0)
}
onCurrentRoomChanged: { function onCurrentRoomChanged() {
if(messages.contentY < messages.originY + 5) if(messages.contentY < messages.originY + 5)
matrix.readMessageHistory(matrix.currentRoom) matrix.readMessageHistory(matrix.currentRoom)
} }
onMessage: { function onMessage(room, sender, content) {
var notificationLevel = room.notificationLevel var notificationLevel = room.notificationLevel
var shouldDisplay = false var shouldDisplay = false

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

51
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: {
connect.onAccountChange()
}
Connections {
id: connect
target: app
function onAccountChange() {
if(matrix.settingsValid()) { if(matrix.settingsValid()) {
desktop.showTrayIcon(false) desktop.showTrayIcon(false)
stack.push("qrc:/Client.qml") stack.replace("qrc:/Client.qml")
} else { } else {
desktop.showTrayIcon(true) desktop.showTrayIcon(true)
stack.push("qrc:/Login.qml") 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"));

545
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,7 +352,8 @@ 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) {
network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.name", [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);
@ -214,7 +366,7 @@ void MatrixCore::sync() {
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);
@ -226,7 +378,7 @@ void MatrixCore::sync() {
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);
@ -235,12 +387,31 @@ void MatrixCore::sync() {
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,16 +1259,21 @@ 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()));
addEvent(e);
if(!firstSync && !traversingHistory)
emit message(&room, e->getSender(), e->getMsg());
}
}
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->setSender(event["sender"].toString());
e->eventId = event["event_id"].toString(); e->eventId = event["event_id"].toString();
@ -992,19 +1308,13 @@ void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool i
} }
e->setMsg(msg); e->setMsg(msg);
addEvent(e);
if(!firstSync && !traversingHistory)
emit message(&room, e->getSender(), e->getMsg());
}
} }
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;
}