#include "matrixcore.h" #include #include #include #include #include #include #include #include #include #include #include #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(decryptedMsg.data()), decryptedMsg.size())); auto id = document.object()["content"].toObject()["session_id"].toString(); qDebug() << "NEW KEY " << id << " = " << document; // create new inbound session, append to list auto sess = encryption->beginInboundSession(document.object()["content"].toObject()["session_key"].toString().toStdString()); inboundSessions[id] = sess; // if we recieved a new key, let's see if we can decrypt some old messages! for(auto room : rooms) { if(room->getId() == document.object()["content"].toObject()["room_id"].toString()) { for(auto event : room->events) { if(event->encryptionInfo != nullptr && event->encryptionInfo->sessionId == id) { auto msg = encryption->decrypt(sess, event->encryptionInfo->cipherText.toStdString()); const QJsonDocument document = QJsonDocument::fromJson(QByteArray(reinterpret_cast(msg.data()), msg.size())); populateEvent(document.object(), event); emit event->msgChanged(); } } } } } } const auto createRoom = [this](const QString id, const QString joinState, bool autofill_data = true) { roomListModel.beginInsertRoom(); Room* room = new Room(this); 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 + ":", ""); } 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(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 + ":", ""); } 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(); }