Archived
1
Fork 0
This repository has been archived on 2025-04-12. You can view files and clone it, but cannot push or open issues or pull requests.
trinity/src/matrixcore.cpp
Joshua Goins a825c8886d 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.
2022-03-01 16:20:32 -05:00

1465 lines
54 KiB
C++
Executable file

#include "matrixcore.h"
#include <cmark.h>
#include <QJsonArray>
#include <QRandomGenerator>
#include <QSettings>
#include <QPixmap>
#include <QStandardPaths>
#include <QDir>
#include <QMimeDatabase>
#include <QMimeData>
#include <fstream>
#include <iterator>
#include "network.h"
#include "community.h"
MatrixCore::MatrixCore(QString profileName, QObject* parent) : QObject(parent), profileName(profileName), roomListModel(rooms), directoryListModel(publicRooms), eventModel(*this) {
network = new Network();
encryption = new Encryption();
QSettings settings;
settings.beginGroup(profileName);
homeserverURL = settings.value("homeserver", "matrix.org").toString();
userId = settings.value("userId").toString();
network->homeserverURL = "https://" + homeserverURL;
deviceId = settings.value("deviceId").toString();
if(settings.contains("accessToken"))
network->accessToken = "Bearer " + settings.value("accessToken").toString();
if(settings.contains("accountPickle")) {
encryption->loadDeviceKeys(settings.value("accountPickle").toString());
qDebug() << "testing identity key: " << encryption->identityKey;
}
settings.endGroup();
emptyRoom.setName("Empty");
emptyRoom.setTopic("There is nothing here.");
roomListSortModel.setSourceModel(&roomListModel);
roomListSortModel.setSortRole(RoomListModel::SectionRole);
connect(this, &MatrixCore::roomListChanged, [this] {
roomListSortModel.sort(0);
});
memberSortModel.setSourceModel(&memberModel);
memberSortModel.setSortRole(RoomListModel::SectionRole);
connect(this, &MatrixCore::currentRoomChanged, [this] {
memberSortModel.sort(0);
});
directoryListSortModel.setSourceModel(&directoryListModel);
updateAccountInformation();
QString appDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
if(!QDir(appDir).exists())
QDir().mkdir(appDir);
QString emotesDir = appDir + "/emotes";
if(!QDir(emotesDir).exists())
QDir().mkdir(emotesDir);
if(QDir(emotesDir).exists()) {
for(auto emote : QDir(emotesDir).entryInfoList()) {
// TODO: add support for more than just .png emotes
if(emote.fileName().contains(".png")) {
Emote* e = new Emote();
e->name = emote.fileName().remove(".png");
e->path = emote.absoluteFilePath();
emotes.push_back(e);
}
}
}
localEmoteModel.setList(&emotes);
}
void MatrixCore::registerAccount(const QString &username, const QString &password, const QString& session, const QString& type) {
QJsonObject authObject;
if(!session.isEmpty()) {
authObject["type"] = type;
authObject["session"] = session;
}
const QJsonObject registerObject {
{"auth", authObject},
{"username", username},
{"password", password}
};
network->postJSON("/_matrix/client/r0/register?kind=user", registerObject, [this](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(reply->error()) {
if(document.object().contains("flows")) {
const QString stage = document.object()["flows"].toArray()[0].toObject()["stages"].toArray()[0].toString();
if(stage == "m.login.recaptcha") {
const QJsonObject data {
{"public_key", document.object()["params"].toObject()["m.login.recaptcha"].toObject()["public_key"].toString()},
{"session", document.object()["session"].toString()},
{"type", "m.login.recaptcha"}
};
emit registerFlow(data);
} else if(stage == "m.login.dummy") {
const QJsonObject data {
{"session", document.object()["session"].toString()},
{"type", "m.login.dummy"}
};
emit registerFlow(data);
} else {
emit registerAttempt(true, "Unknown stage type " + stage);
}
} else {
emit registerAttempt(true, document.object()["error"].toString());
}
} else {
network->accessToken = "Bearer " + document.object()["access_token"].toString();
QSettings settings;
settings.beginGroup(profileName);
settings.setValue("accessToken", document.object()["access_token"].toString());
settings.setValue("userId", document.object()["user_id"].toString());
settings.setValue("deviceId", document.object()["device_id"].toString());
settings.endGroup();
emit registerAttempt(false, "");
}
});
}
void MatrixCore::login(const QString& username, const QString& password) {
const QJsonObject loginObject {
{"type", "m.login.password"},
{"user", username},
{"password", password},
{"initial_device_display_name", "Trinity"}
};
network->postJSON("/_matrix/client/r0/login", loginObject, [this](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(reply->error()) {
emit loginAttempt(true, document.object()["error"].toString());
} else {
network->accessToken = "Bearer " + document.object()["access_token"].toString();
QSettings settings;
settings.beginGroup(profileName);
settings.setValue("accessToken", document.object()["access_token"].toString());
settings.setValue("userId", document.object()["user_id"].toString());
settings.setValue("deviceId", document.object()["device_id"].toString());
// upload keys
encryption->createNewDeviceKeys();
QJsonObject keysObject {
{"curve25519:" + document.object()["device_id"].toString(), encryption->identityKey["curve25519"]},
{"ed25519:" + document.object()["device_id"].toString(), encryption->identityKey["ed25519"]}
};
QJsonObject deviceKeysObject {
{"user_id", document.object()["user_id"].toString()},
{"device_id", document.object()["device_id"].toString()},
{"algorithms", QJsonArray({"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"})},
{"keys", keysObject}
};
QJsonObject signature {
{"ed25519:" + document.object()["device_id"].toString(), encryption->signMessage(QJsonDocument(deviceKeysObject).toJson(QJsonDocument::Compact)) }};
deviceKeysObject["signatures"] = QJsonObject {
{document.object()["user_id"].toString(), signature}};
QJsonObject oneTimeKeyObject;
auto one_time_keys = encryption->generateOneTimeKeys(encryption->getRecommendedNumberOfOneTimeKeys());
for(auto key : one_time_keys["curve25519"].toObject().keys()) {
QJsonObject keyObject {
{"key", one_time_keys["curve25519"].toObject()[key]}
};
QJsonObject signature {
{"ed25519:" + document.object()["device_id"].toString(), encryption->signMessage(QJsonDocument(keyObject).toJson(QJsonDocument::Compact)) }};
keyObject["signatures"] = QJsonObject {
{document.object()["user_id"].toString(), signature}};
oneTimeKeyObject["signed_curve25519:" + key] = keyObject;
}
QJsonObject masterKeysObject {
{"device_keys", deviceKeysObject},
{"one_time_keys", oneTimeKeyObject}
};
//qDebug() << masterKeysObject;
auto keys = encryption->saveDeviceKeys();
settings.setValue("accountPickle", keys);
settings.endGroup();
network->postJSON("/_matrix/client/r0/keys/upload", masterKeysObject, [this](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
qDebug() << "KEY UPLOAD RESULT: " << document;
});
emit loginAttempt(false, "");
}
});
}
void MatrixCore::logout() {
network->post("/_matrix/client/r0/logout");
QSettings settings;
settings.remove("accessToken");
settings.remove("deviceId");
settings.remove("userId");
settings.sync();
}
void MatrixCore::updateAccountInformation() {
network->get("/_matrix/client/r0/profile/" + userId + "/displayname", [this](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
displayName = document.object()["displayname"].toString();
emit displayNameChanged();
});
}
void MatrixCore::setDisplayName(const QString& name) {
displayName = name;
const QJsonObject displayNameObject {
{"displayname", name}
};
network->putJSON("/_matrix/client/r0/profile/" + userId + "/displayname", displayNameObject, [this, name](QNetworkReply* reply) {
emit displayNameChanged();
});
}
void MatrixCore::sync() {
if(network->accessToken.isEmpty())
return;
QString url = "/_matrix/client/r0/sync";
if(!nextBatch.isEmpty())
url += "?since=" + nextBatch;
network->get(url, [this](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(!document.object()["next_batch"].isNull())
nextBatch = document.object()["next_batch"].toString();
//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();
Room* room = new Room(this);
room->setId(id);
room->setJoinState(joinState);
QSettings settings;
settings.beginGroup(id);
if(settings.contains("notificationLevel"))
room->setNotificationLevel(settings.value("notificationLevel").toInt(), true);
else
room->setNotificationLevel(1);
settings.endGroup();
if(autofill_data) {
network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.name", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") {
room->setGuestDenied(true);
return;
} else if(document.object()["errcode"].toString() == "M_NOT_FOUND")
return;
room->setName(document.object()["name"].toString());
roomListModel.updateRoom(room);
});
network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.topic", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") {
room->setGuestDenied(true);
return;
}
room->setTopic(document.object()["topic"].toString());
roomListModel.updateRoom(room);
});
network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.avatar", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(document.object()["errcode"].toString() == "M_GUEST_ACCESS_FORBIDDEN") {
room->setGuestDenied(true);
return;
}
if(document.object().contains("url")) {
const QString imageId = document.object()["url"].toString().remove("mxc://");
room->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale");
}
roomListModel.updateRoom(room);
});
network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.power_levels", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
for(auto user : document.object()["users"].toObject().keys()) {
room->powerLevelList[user] = document.object()["users"].toObject()[user].toInt();
}
});
network->get("/_matrix/client/r0/rooms/" + id + "/state/m.room.encryption", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(document.object().contains("algorithm")) {
room->setEncrypted();
}
//qDebug() << document;
});
}
rooms.push_back(room);
idToRoom.insert(id, room);
roomListModel.endInsertRoom();
emit roomListChanged();
updateMembers(room);
return room;
};
for(const auto id : document.object()["rooms"].toObject()["invite"].toObject().keys()) {
if(!invitedRooms.count(id)) {
Room* room = createRoom(id, "Invited", false);
room->setGuestDenied(true);
for(auto event : document.object()["rooms"].toObject()["invite"].toObject()[id].toObject()["invite_state"].toObject()["events"].toArray()) {
const QString type = event.toObject()["type"].toString();
if(type == "m.room.member")
room->setInvitedBy(event.toObject()["sender"].toString());
else if(type == "m.room.name") {
room->setName(event.toObject()["content"].toObject()["name"].toString());
} else if(type == "m.room.avatar") {
const QString imageId = event.toObject()["content"].toObject()["url"].toString().remove("mxc://");
room->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale");
}
roomListModel.updateRoom(room);
}
invitedRooms.push_back(id);
}
}
for(const auto id : document.object()["rooms"].toObject()["join"].toObject().keys()) {
if(!joinedRooms.count(id)) {
createRoom(id, "Joined");
joinedRooms.push_back(id);
}
}
for(const auto id : document.object()["rooms"].toObject()["leave"].toObject().keys()) {
if(joinedRooms.count(id)) {
Room* room = resolveRoomId(id);
room->setJoinState("left");
joinedRooms.removeOne(id);
rooms.removeOne(room);
roomListModel.fullUpdate();
}
}
unsigned int i = 0;
for(const auto& room : document.object()["rooms"].toObject()["join"].toObject()) {
Room* roomState = nullptr;
for(auto& r : rooms) {
if(r->getId() == document.object()["rooms"].toObject()["join"].toObject().keys()[i])
roomState = r;
}
if(!roomState)
continue;
if(firstSync)
roomState->prevBatch = room.toObject()["timeline"].toObject()["prev_batch"].toString();
const int highlightCount = room.toObject()["unread_notifications"].toObject()["highlight_count"].toInt();
const int notificationCount = room.toObject()["unread_notifications"].toObject()["notification_count"].toInt();
if(highlightCount != roomState->getHighlightCount()) {
roomState->setNotificationCount(highlightCount);
roomListModel.updateRoom(roomState);
}
if(notificationCount != roomState->getNotificationCount()) {
roomState->setNotificationCount(notificationCount);
roomListModel.updateRoom(roomState);
}
for(const auto event : room.toObject()["ephemeral"].toObject()["events"].toArray()) {
const QString eventType = event.toObject()["type"].toString();
if(eventType == "m.typing") {
auto typing = event.toObject()["content"].toObject()["user_ids"].toArray();
QString typingText;
int trueSize = 0;
if(typing.size() < 4) {
for(int i = 0; i < typing.size(); i++) {
if(typing[i].toString() == userId)
continue;
const Member* member = resolveMemberId(typing[i].toString());
if(!member)
continue;
typingText += member->getDisplayName();
if(i != typing.size() - 1)
typingText += ", ";
trueSize++;
}
typingText += " is";
} else {
typingText = "Several people are";
}
if(trueSize != 0)
this->typingText = typingText + " typing...";
else
this->typingText.clear();
emit typingTextChanged();
}
}
for(const auto event : room.toObject()["timeline"].toObject()["events"].toArray())
consumeEvent(event.toObject(), *roomState);
i++;
}
for(const auto& id : document.object()["groups"].toObject()["join"].toObject().keys()) {
if(!joinedCommunitiesIds.count(id)) {
Community* community = nullptr;
if(!idToCommunity.count(id))
community = createCommunity(id);
else
community = idToCommunity[id];
community->setJoinState("Joined");
joinedCommunities.push_back(community);
joinedCommunitiesIds.push_back(community->getId());
emit joinedCommunitiesChanged();
}
}
if(firstSync) {
firstSync = false;
emit initialSyncFinished();
}
emit syncFinished();
});
}
bool MatrixCore::isInitialSyncComplete() {
return !firstSync;
}
void MatrixCore::sendMessage(Room* room, const QString& message) {
if(message.isEmpty())
return;
Event* e = new Event(room);
e->setSender(userId);
e->timestamp = QDateTime::currentDateTime();
e->setMsg(message);
e->setRoom(room->getId());
e->setSent(false);
e->setMsgType("text");
QString msg = e->getMsg();
for(const auto& emote : emotes) {
msg.replace(":" + emote->name + ":", "<img src='file://" + emote->path + "' width=22 height=22/>");
}
e->setMsg(msg);
eventModel.beginUpdate(0);
room->events.push_front(e);
eventModel.endUpdate();
unsentMessages.push_back(e);
const auto onMessageFeedbackReceived = [this, e](QNetworkReply* reply) {
if(!reply->error()) {
for(size_t i = 0; i < unsentMessages.size(); i++) {
if(unsentMessages[i] == e)
e->setSent(true);
}
} else {
qDebug() << reply->readAll();
}
};
bool shouldSendAsMarkdown = false;
char* formatted = nullptr;
if(markdownEnabled) {
formatted = cmark_markdown_to_html(message.toStdString().c_str(), message.length(), CMARK_OPT_DEFAULT | CMARK_OPT_HARDBREAKS);
shouldSendAsMarkdown = strlen(formatted) > 8 + message.length();
}
QJsonObject messageObject;
if(shouldSendAsMarkdown) {
messageObject = QJsonObject {
{"msgtype", "m.text"},
{"formatted_body", formatted},
{"body", message},
{"format", "org.matrix.custom.html"}
};
e->setMsg(formatted);
} else {
messageObject = QJsonObject {
{"msgtype", "m.text"},
{"body", message}
};
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);
}
}
void MatrixCore::removeMessage(const QString& eventId) {
const QJsonObject reasonObject {
{"reason", ""}
};
network->putJSON("/_matrix/client/r0/rooms/" + currentRoom->getId() + "/redact/" + eventId + "/" + QString::number(QRandomGenerator::global()->generate()), reasonObject, [this, eventId](QNetworkReply* reply) {
auto& events = currentRoom->events;
for(int i = 0; i < events.size(); i++) {
if(events[i]->eventId == eventId) {
eventModel.beginRemoveEvent(i, 0);
events.removeAt(i);
eventModel.endRemoveEvent();
}
}
});
}
void MatrixCore::uploadAttachment(Room* room, const QString& path) {
Event* e = new Event(room);
e->setSender(userId);
e->timestamp = QDateTime::currentDateTime();
e->setRoom(room->getId());
e->setSent(false);
eventModel.beginUpdate(0);
room->events.push_front(e);
eventModel.endUpdate();
unsentMessages.push_back(e);
QMimeDatabase mimeDb;
QMimeType mimeType = mimeDb.mimeTypeForFile(path);
QString filepath = path;
filepath.remove("file://");
QFile f(filepath);
f.open(QFile::ReadOnly);
const QString fileName = QFileInfo(f.fileName()).fileName();
const qint64 fileSize = f.size();
e->setAttachment(path);
e->setThumbnail(path);
e->setMsg(fileName);
e->setMsgType(mimeType.name().contains("image") ? "image" : "file");
network->postBinary("/_matrix/media/r0/upload?filename=" + f.fileName(), f.readAll(), mimeType.name(), [this, mimeType, fileName, fileSize, e](QNetworkReply* reply) {
if(!reply->error()) {
e->setSent(true);
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
QJsonObject infoObject {
{"mimetype", mimeType.name()},
{"size", fileSize},
};
const QJsonObject imageObject {
{"msgtype", mimeType.name().contains("image") ? "m.image" : "m.file"},
{"body", fileName},
{"url", document.object()["content_uri"].toString()},
{"info", infoObject}
};
network->putJSON("/_matrix/client/r0/rooms/" + currentRoom->getId() + "/send/m.room.message/" + QString::number(QRandomGenerator::global()->generate()), imageObject);
}
}, [e](qint64 sent, qint64 total) {
e->setSentProgress((double)sent / (double)total);
});
}
void MatrixCore::startDirectChat(const QString& id) {
const QJsonObject roomObject {
{"visibility", "private"},
{"creation_content", QJsonObject{{"m.federate", false}}},
{"preset", "private_chat"},
{"is_direct", true},
{"invite", QJsonArray{id}}
};
network->postJSON("/_matrix/client/r0/createRoom", roomObject, [](QNetworkReply*) {});
}
void MatrixCore::setTyping(Room* room) {
const QJsonObject typingObject {
{"typing", true},
{"timeout", 15000}
};
network->putJSON("/_matrix/client/r0/rooms/" + room->getId() + "/typing/" + userId, typingObject);
}
void MatrixCore::joinRoom(const QString& id) {
network->post("/_matrix/client/r0/rooms/" + id + "/join", [this, id](QNetworkReply* reply) {
if(!reply->error()) {
//check if its by an invite
if(invitedRooms.contains(id)) {
invitedRooms.removeOne(id);
joinedRooms.push_back(id);
for(const auto roomObject : rooms) {
if(roomObject->getId() == id) {
roomObject->setJoinState("Joined");
roomObject->setGuestDenied(false);
emit roomListChanged();
return;
}
}
}
}
});
}
void MatrixCore::leaveRoom(const QString& id) {
network->post("/_matrix/client/r0/rooms/" + id + "/leave");
}
void MatrixCore::inviteToRoom(Room* room, const QString& userId) {
const QJsonObject inviteObject {
{"user_id", userId}
};
network->postJSON("/_matrix/client/r0/rooms/" + room->getId() + "/invite", inviteObject, [](QNetworkReply*) {});
}
void MatrixCore::updateMembers(Room* room) {
if(!room)
return;
network->get("/_matrix/client/r0/rooms/" + room->getId() + "/members", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
const QJsonArray& chunk = document.object()["chunk"].toArray();
size_t realSize = 0;
for(const auto& member : chunk) {
if(member.toObject()["content"].toObject()["membership"].toString() == "join")
realSize++;
}
if(room->members.size() != realSize) {
room->members.clear();
room->members.reserve(realSize);
for(const auto& member : chunk) {
const QJsonObject& memberJson = member.toObject();
if(memberJson["content"].toObject()["membership"].toString() == "join") {
const QString& id = memberJson["state_key"].toString();
Member* m = nullptr;
if(!idToMember.contains(id)) {
m = new Member(this);
m->setId(id);
m->setDisplayName(memberJson["content"].toObject()["displayname"].toString());
if(!memberJson["content"].toObject()["avatar_url"].isNull()) {
const QString imageId = memberJson["content"].toObject()["avatar_url"].toString().remove("mxc://");
m->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale");
}
idToMember.insert(id, m);
} else {
m = idToMember[id];
}
if(currentRoom == room) {
eventModel.updateEventsByMember(id);
memberModel.beginUpdate(0);
}
room->members.push_back(m);
if(currentRoom == room)
memberModel.endUpdate();
}
}
}
});
}
void MatrixCore::readMessageHistory(Room* room) {
if(!room || room->prevBatch.isEmpty())
return;
network->get("/_matrix/client/r0/rooms/" + room->getId() + "/messages?from=" + room->prevBatch + "&dir=b", [this, room](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
room->prevBatch = document.object()["end"].toString();
traversingHistory = true;
for(const auto event : document.object()["chunk"].toArray())
consumeEvent(event.toObject(), *room, false);
traversingHistory = false;
});
}
void MatrixCore::updateMemberCommunities(Member* member) {
if(!member)
return;
const QJsonArray userIdsArray {
{member->getId()}
};
const QJsonObject userIdsObject {
{"user_ids", userIdsArray}
};
network->postJSON("/_matrix/client/r0/publicised_groups", userIdsObject, [this, member](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
for(const auto id : document.object()["users"].toObject()[member->getId()].toArray()) {
bool found = false;
for(const auto community : member->getPublicCommunities()) {
if(community->getId() == id.toString())
found = true;
}
if(!found) {
Community* community = nullptr;
if(!idToCommunity.count(id.toString()))
community = createCommunity(id.toString());
else
community = idToCommunity[id.toString()];
member->addCommunity(community);
}
}
});
}
bool MatrixCore::settingsValid() {
return !network->accessToken.isEmpty() && !network->homeserverURL.isEmpty();
}
void MatrixCore::setHomeserver(const QString& url) {
network->homeserverURL = "https://" + url;
network->get("/_matrix/client/versions", [this, url](QNetworkReply* reply) {
if(!reply->error()) {
homeserverURL = url;
QSettings settings;
settings.beginGroup(profileName);
settings.setValue("homeserver", url);
settings.endGroup();
}
network->homeserverURL = "https://" + homeserverURL;
emit homeserverChanged(reply->error() == 0, reply->errorString());
});
}
void MatrixCore::changeCurrentRoom(Room* room) {
currentRoom = room;
eventModel.setRoom(room);
eventModel.fullUpdate();
memberModel.setRoom(room);
memberModel.fullUpdate();
emit currentRoomChanged();
}
void MatrixCore::changeCurrentRoom(const unsigned int index) {
if(index < rooms.size())
changeCurrentRoom(rooms[index]);
else
changeCurrentRoom(&emptyRoom);
}
void MatrixCore::addEmote(const QString& url) {
qDebug() << "adding emote " << url;
QString newUrl = url;
newUrl.remove("file://");
QUrl file(newUrl);
QString appDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
if(!QDir(appDir).exists())
QDir().mkdir(appDir);
QString emotesDir = appDir + "/emotes";
if(!QDir(emotesDir).exists())
QDir().mkdir(emotesDir);
QPixmap pixmap(newUrl);
pixmap = pixmap.scaled(22, 22, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
pixmap.save(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/emotes/" + file.fileName());
Emote* emote = new Emote();
emote->path = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/emotes/" + file.fileName();
emote->name = file.fileName().remove(".png");
emotes.push_back(emote);
emit localEmotesChanged();
localEmoteModel.update();
}
void MatrixCore::deleteEmote(Emote* emote) {
emotes.removeOne(emote);
QFile(emote->path).remove();
emit localEmotesChanged();
localEmoteModel.update();
}
Member* MatrixCore::resolveMemberId(const QString& id) const {
return idToMember.value(id);
}
Community* MatrixCore::resolveCommunityId(const QString &id) const {
return idToCommunity.value(id);
}
Room* MatrixCore::resolveRoomId(const QString &id) const {
return idToRoom.value(id);
}
Room* MatrixCore::getRoom(const unsigned int index) const {
return rooms[index];
}
QString MatrixCore::getUsername() const {
QString id = userId;
return id.remove('@').split(':')[0];
}
void MatrixCore::loadDirectory(const QString& homeserver) {
const QJsonObject bodyObject;
network->postJSON("/_matrix/client/r0/publicRooms?server=" + homeserver, bodyObject, [this](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(publicRooms.size() != document.object()["chunk"].toArray().size()) {
publicRooms.clear();
publicRooms.reserve(document.object()["chunk"].toArray().size());
for(const auto room : document.object()["chunk"].toArray()) {
const QJsonObject& roomObject = room.toObject();
const QString& roomId = roomObject["room_id"].toString();
Room* r = nullptr;
if(!idToRoom.contains(roomId)) {
r = new Room(this);
r->setId(roomId);
r->setName(roomObject["name"].toString());
if(!roomObject["avatar_url"].isNull()) {
const QString imageId = roomObject["avatar_url"].toString().remove("mxc://");
r->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale");
}
r->setTopic(roomObject["topic"].toString());
idToRoom.insert(roomId, r);
} else {
r = idToRoom.value(roomId);
}
directoryListModel.beginInsertRoom();
publicRooms.push_back(r);
directoryListModel.endInsertRoom();
emit publicRoomsChanged();
}
}
});
}
void MatrixCore::readUpTo(Room* room, const int index) {
if(!room)
return;
if(room->events.size() == 0)
return;
if(index < 0)
return;
network->post("/_matrix/client/r0/rooms/" + room->getId() + "/receipt/m.read/" + room->events[index]->eventId);
}
void MatrixCore::setMarkdownEnabled(const bool enabled) {
markdownEnabled = enabled;
emit markdownEnabledChanged();
}
Room* MatrixCore::getCurrentRoom() {
return currentRoom != nullptr ? currentRoom : &emptyRoom;
}
EventModel* MatrixCore::getEventModel() {
return &eventModel;
}
RoomListSortModel* MatrixCore::getRoomListModel() {
return &roomListSortModel;
}
RoomListSortModel* MatrixCore::getDirectoryListModel() {
return &directoryListSortModel;
}
EmoteListModel* MatrixCore::getLocalEmoteListModel() {
return &localEmoteModel;
}
MemberListSortModel* MatrixCore::getMemberModel() {
return &memberSortModel;
}
QString MatrixCore::getHomeserverURL() const {
return homeserverURL;
}
void MatrixCore::consumeEvent(const QJsonObject& event, Room& room, const bool insertFront) {
const QString eventType = event["type"].toString();
const auto addEvent = [&room, insertFront, this](Event* object) {
if(insertFront) {
if(&room == currentRoom)
eventModel.beginUpdate(0);
room.events.push_front(object);
if(&room == currentRoom)
eventModel.endHistory();
} else {
if(&room == currentRoom)
eventModel.beginHistory(0);
room.events.push_back(object);
if(&room == currentRoom)
eventModel.endHistory();
}
};
bool found = false;
if(eventType == "m.room.message" || eventType == "m.room.encrypted") {
for(size_t i = 0; i < unsentMessages.size(); i++) {
if(event["sender"].toString() == userId && unsentMessages[i]->getRoom() == room.getId()) {
found = true;
unsentMessages[i]->setSent(true);
if(currentRoom == &room)
eventModel.updateEvent(unsentMessages[i]);
unsentMessages.removeAt(i);
return;
}
}
}
if(eventType == "m.room.member") {
// avoid events tied to us
if(event["state_key"].toString() == userId)
return;
if(event["content"].toObject().contains("is_direct"))
room.setDirect(event["content"].toObject()["is_direct"].toBool());
if(room.getDirect()) {
room.setName(event["content"].toObject()["displayname"].toString());
if(!event["content"].toObject()["avatar_url"].isNull()) {
const QString imageId = event["content"].toObject()["avatar_url"].toString().remove("mxc://");
room.setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale");
}
}
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
return;
// don't show redacted messages
if(event["unsigned"].toObject().keys().contains("redacted_because"))
return;
if(!found && eventType == "m.room.message") {
Event* e = new Event(&room);
populateEvent(event, e);
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->eventId = event["event_id"].toString();
if(msgType == "m.text" && !event["content"].toObject().contains("formatted_body")) {
e->setMsgType("text");
e->setMsg(event["content"].toObject()["body"].toString());
} else if(msgType == "m.text" && event["content"].toObject().contains("formatted_body")) {
e->setMsgType("text");
e->setMsg(event["content"].toObject()["formatted_body"].toString());
} else if(msgType == "m.image") {
e->setMsgType("image");
e->setAttachment(getMXCMediaURL(event["content"].toObject()["url"].toString()));
e->setAttachmentSize(event["content"].toObject()["info"].toObject()["size"].toInt());
if(event["content"].toObject()["info"].toObject().contains("thumbnail_url"))
e->setThumbnail(getMXCThumbnailURL(event["content"].toObject()["info"].toObject()["thumbnail_url"].toString()));
else
e->setThumbnail(getMXCMediaURL(event["content"].toObject()["url"].toString()));
e->setMsg(event["content"].toObject()["body"].toString());
} else if(msgType == "m.file") {
e->setMsgType("file");
e->setAttachment(getMXCMediaURL(event["content"].toObject()["url"].toString()));
e->setAttachmentSize(event["content"].toObject()["info"].toObject()["size"].toInt());
e->setMsg(event["content"].toObject()["body"].toString());
} else
e->setMsg(event["content"].toObject()["body"].toString());
QString msg = e->getMsg();
for(const auto& emote : emotes) {
msg.replace(":" + emote->name + ":", "<img src='file://" + emote->path + "' width=22 height=22/>");
}
e->setMsg(msg);
}
Community* MatrixCore::createCommunity(const QString& id) {
Community* community = new Community(this);
community->setId(id);
network->get("/_matrix/client/r0/groups/" + community->getId() + "/summary", [this, community](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
const QJsonObject& profile = document.object()["profile"].toObject();
community->setName(profile["name"].toString());
if(!profile["avatar_url"].isNull()) {
const QString imageId = profile["avatar_url"].toString().remove("mxc://");
community->setAvatar(network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale");
}
community->setShortDescription(profile["short_description"].toString());
community->setLongDescription(profile["long_description"].toString());
idToCommunity.insert(community->getId(), community);
});
return community;
}
QString MatrixCore::getMXCThumbnailURL(QString url) {
const QString imageId = url.remove("mxc://");
return network->homeserverURL + "/_matrix/media/r0/thumbnail/" + imageId + "?width=64&height=64&method=scale";
}
QString MatrixCore::getMXCMediaURL(QString url) {
const QString imageId = url.remove("mxc://");
return network->homeserverURL + "/_matrix/media/v1/download/" + imageId;
}
QString MatrixCore::getDisplayName() const {
return displayName;
}
QVariantList MatrixCore::getJoinedCommunitiesList() const {
QVariantList list;
for(const auto community : joinedCommunities)
list.push_back(QVariant::fromValue(community));
return list;
}
QString MatrixCore::getTypingText() const {
return typingText;
}
bool MatrixCore::getMarkdownEnabled() const {
return markdownEnabled;
}
void MatrixCore::sendKeyToDevice(QString roomId, QString senderCurveIdentity, QString senderEdIdentity, QString session_id, QString session_key, QString user_id, QString device_id)
{
// why we would we send ourselves a key?
if(device_id == deviceId)
return;
QJsonObject claimObject {
{"timeout", 10000},
{"one_time_keys", QJsonObject {
{user_id, QJsonObject {
{device_id, "signed_curve25519"}
}}
}}
};
network->postJSON("/_matrix/client/r0/keys/claim", claimObject, [this, device_id, user_id, senderCurveIdentity, senderEdIdentity, roomId, session_id, session_key](QNetworkReply* reply) {
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
std::string identityKey = senderCurveIdentity.toStdString();
// we couldnt claim a key, skip
if(document.object()["one_time_keys"].toObject()[user_id].toObject()[device_id].toObject().keys().empty())
return;
std::string oneTimeKey = document.object()["one_time_keys"].toObject()[user_id].toObject()[device_id].toObject()[document.object()["one_time_keys"].toObject()[user_id].toObject()[device_id].toObject().keys()[0]].toObject()["key"].toString().toStdString();
auto session = encryption->beginOutboundOlmSession(identityKey, oneTimeKey);
// construct m.room.key event
const QJsonObject roomKeyObject {
{"content", QJsonObject {
{"algorithm", "m.megolm.v1.aes-sha2"},
{"room_id", roomId},
{"session_id", session_id},
{"session_key", session_key}
}},
{"type", "m.room_key"},
{"keys", QJsonObject {
{"ed25519", encryption->identityKey["ed25519"]}
}},
{"sender", userId},
{"sender_device", deviceId},
{"recipient", user_id},
{"recipient_keys", QJsonObject {
{"ed25519", senderEdIdentity}
}}
};
// construct the m.room.encrypted event
const QJsonObject roomEncryptedObject {
{"algorithm", "m.olm.v1.curve25519-aes-sha2"},
{"ciphertext", QJsonObject {
{identityKey.c_str(), QJsonObject {
{"body", encryption->encrypt(session, QString(QJsonDocument(roomKeyObject).toJson(QJsonDocument::Compact)).toStdString()).c_str()},
{"type", (int)olm_encrypt_message_type(session)}
}}}},
{"sender_key", encryption->identityKey["curve25519"]},
};
const QJsonObject sendToDeviceObject {
{"messages", QJsonObject {
{ user_id, QJsonObject {
{ device_id, roomEncryptedObject}
}}
}}
};
network->putJSON(QString("/_matrix/client/r0/sendToDevice/m.room.encrypted/") + QString::number(QRandomGenerator::global()->generate()), sendToDeviceObject, [](QNetworkReply* reply) {
//qDebug() << "REPLY FROM KEY SEND: " << reply->readAll();
});
});
}
void MatrixCore::createOrLoadSession() {
if(currentSession != nullptr)
return;
QSettings settings;
settings.beginGroup(profileName);
if(settings.contains("sessionPickle")) {
currentSession = encryption->loadSession(settings.value("sessionPickle").toString());
currentSessionId = encryption->getGroupSessionId(currentSession).c_str();
currentSessionKey = encryption->getGroupSessionKey(currentSession).c_str();
} else {
currentSession = encryption->beginOutboundSession();
auto session_id = encryption->getGroupSessionId(currentSession);
auto session_key = encryption->getGroupSessionKey(currentSession);
//qDebug () << "CREATING NEW SESSION WITH ID " << session_id.c_str();
currentSessionId = session_id.c_str();
currentSessionKey = session_key.c_str();
}
settings.endGroup();
}