File indexing completed on 2024-04-28 04:59:46

0001 // SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
0002 // SPDX-License-Identifier: GPL-2.0-or-later
0003 
0004 #include "neochatconnection.h"
0005 
0006 #include <QImageReader>
0007 
0008 #include "controller.h"
0009 #include "jobs/neochatchangepasswordjob.h"
0010 #include "jobs/neochatdeactivateaccountjob.h"
0011 #include "roommanager.h"
0012 
0013 #include <Quotient/connection.h>
0014 #include <Quotient/quotient_common.h>
0015 #include <qt6keychain/keychain.h>
0016 
0017 #include <KLocalizedString>
0018 
0019 #include <Quotient/csapi/content-repo.h>
0020 #include <Quotient/csapi/profile.h>
0021 #include <Quotient/database.h>
0022 #include <Quotient/jobs/downloadfilejob.h>
0023 #include <Quotient/qt_connection_util.h>
0024 #include <Quotient/room.h>
0025 #include <Quotient/settings.h>
0026 #include <Quotient/user.h>
0027 
0028 #ifdef HAVE_KUNIFIEDPUSH
0029 #include <QCoroNetwork>
0030 #include <Quotient/csapi/pusher.h>
0031 #include <Quotient/networkaccessmanager.h>
0032 #endif
0033 
0034 using namespace Quotient;
0035 using namespace Qt::StringLiterals;
0036 
0037 NeoChatConnection::NeoChatConnection(QObject *parent)
0038     : Connection(parent)
0039 {
0040     connectSignals();
0041 }
0042 
0043 NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
0044     : Connection(server, parent)
0045 {
0046     connectSignals();
0047 }
0048 
0049 void NeoChatConnection::connectSignals()
0050 {
0051     connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
0052         if (type == QLatin1String("org.kde.neochat.account_label")) {
0053             Q_EMIT labelChanged();
0054         }
0055     });
0056     connect(this, &NeoChatConnection::syncDone, this, [this] {
0057         setIsOnline(true);
0058     });
0059     connect(this, &NeoChatConnection::networkError, this, [this]() {
0060         setIsOnline(false);
0061     });
0062     connect(this, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
0063         if (job->error() == BaseJob::UserConsentRequired) {
0064             Q_EMIT userConsentRequired(job->errorUrl());
0065         }
0066     });
0067     connect(this, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
0068         if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
0069             RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
0070         }
0071     });
0072     connect(this, &NeoChatConnection::directChatsListChanged, this, [this](DirectChatsMap additions, DirectChatsMap removals) {
0073         Q_EMIT directChatInvitesChanged();
0074         for (const auto &chatId : additions) {
0075             if (const auto chat = room(chatId)) {
0076                 connect(chat, &Room::unreadStatsChanged, this, [this]() {
0077                     Q_EMIT directChatNotificationsChanged();
0078                 });
0079             }
0080         }
0081         for (const auto &chatId : removals) {
0082             if (const auto chat = room(chatId)) {
0083                 disconnect(chat, &Room::unreadStatsChanged, this, nullptr);
0084             }
0085         }
0086     });
0087     connect(this, &NeoChatConnection::joinedRoom, this, [this](Room *room) {
0088         if (room->isDirectChat()) {
0089             connect(room, &Room::unreadStatsChanged, this, [this]() {
0090                 Q_EMIT directChatNotificationsChanged();
0091             });
0092         }
0093     });
0094     connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
0095         Q_UNUSED(room)
0096         if (prev && prev->isDirectChat()) {
0097             Q_EMIT directChatInvitesChanged();
0098         }
0099     });
0100 }
0101 
0102 void NeoChatConnection::logout(bool serverSideLogout)
0103 {
0104     SettingsGroup(QStringLiteral("Accounts")).remove(userId());
0105 
0106     QKeychain::DeletePasswordJob job(qAppName());
0107     job.setAutoDelete(true);
0108     job.setKey(userId());
0109     QEventLoop loop;
0110     QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
0111     job.start();
0112     loop.exec();
0113 
0114     if (Controller::instance().accounts().count() > 1) {
0115         // Only set the connection if the the account being logged out is currently active
0116         if (this == Controller::instance().activeConnection()) {
0117             Controller::instance().setActiveConnection(dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().accounts()[0]));
0118         }
0119     } else {
0120         Controller::instance().setActiveConnection(nullptr);
0121     }
0122     if (!serverSideLogout) {
0123         return;
0124     }
0125     Connection::logout();
0126 }
0127 
0128 bool NeoChatConnection::setAvatar(const QUrl &avatarSource)
0129 {
0130     QString decoded = avatarSource.path();
0131     if (decoded.isEmpty()) {
0132         callApi<SetAvatarUrlJob>(user()->id(), avatarSource);
0133         return true;
0134     }
0135     if (QImageReader(decoded).read().isNull()) {
0136         return false;
0137     } else {
0138         return user()->setAvatar(decoded);
0139     }
0140 }
0141 
0142 QVariantList NeoChatConnection::getSupportedRoomVersions() const
0143 {
0144     const auto &roomVersions = availableRoomVersions();
0145     QVariantList supportedRoomVersions;
0146     for (const auto &v : roomVersions) {
0147         QVariantMap roomVersionMap;
0148         roomVersionMap.insert("id"_ls, v.id);
0149         roomVersionMap.insert("status"_ls, v.status);
0150         roomVersionMap.insert("isStable"_ls, v.isStable());
0151         supportedRoomVersions.append(roomVersionMap);
0152     }
0153     return supportedRoomVersions;
0154 }
0155 
0156 void NeoChatConnection::changePassword(const QString &currentPassword, const QString &newPassword)
0157 {
0158     auto job = callApi<NeochatChangePasswordJob>(newPassword, false);
0159     connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword] {
0160         if (job->error() == 103) {
0161             QJsonObject replyData = job->jsonData();
0162             QJsonObject authData;
0163             authData["session"_ls] = replyData["session"_ls];
0164             authData["password"_ls] = currentPassword;
0165             authData["type"_ls] = "m.login.password"_ls;
0166             authData["user"_ls] = user()->id();
0167             QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, user()->id()}};
0168             authData["identifier"_ls] = identifier;
0169             NeochatChangePasswordJob *innerJob = callApi<NeochatChangePasswordJob>(newPassword, false, authData);
0170             connect(innerJob, &BaseJob::success, this, [this]() {
0171                 Q_EMIT passwordStatus(PasswordStatus::Success);
0172             });
0173             connect(innerJob, &BaseJob::failure, this, [innerJob, this]() {
0174                 Q_EMIT passwordStatus(innerJob->jsonData()["errcode"_ls] == "M_FORBIDDEN"_ls ? PasswordStatus::Wrong : PasswordStatus::Other);
0175             });
0176         }
0177     });
0178 }
0179 
0180 void NeoChatConnection::setLabel(const QString &label)
0181 {
0182     QJsonObject json{
0183         {"account_label"_ls, label},
0184     };
0185     setAccountData("org.kde.neochat.account_label"_ls, json);
0186     Q_EMIT labelChanged();
0187 }
0188 
0189 QString NeoChatConnection::label() const
0190 {
0191     return accountDataJson("org.kde.neochat.account_label"_ls)["account_label"_ls].toString();
0192 }
0193 
0194 void NeoChatConnection::deactivateAccount(const QString &password)
0195 {
0196     auto job = callApi<NeoChatDeactivateAccountJob>();
0197     connect(job, &BaseJob::result, this, [this, job, password] {
0198         if (job->error() == 103) {
0199             QJsonObject replyData = job->jsonData();
0200             QJsonObject authData;
0201             authData["session"_ls] = replyData["session"_ls];
0202             authData["password"_ls] = password;
0203             authData["type"_ls] = "m.login.password"_ls;
0204             authData["user"_ls] = user()->id();
0205             QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, user()->id()}};
0206             authData["identifier"_ls] = identifier;
0207             auto innerJob = callApi<NeoChatDeactivateAccountJob>(authData);
0208             connect(innerJob, &BaseJob::success, this, [this]() {
0209                 logout(false);
0210             });
0211         }
0212     });
0213 }
0214 
0215 void NeoChatConnection::createRoom(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
0216 {
0217     QList<CreateRoomJob::StateEvent> initialStateEvents;
0218     if (!parent.isEmpty()) {
0219         initialStateEvents.append(CreateRoomJob::StateEvent{
0220             "m.space.parent"_ls,
0221             QJsonObject{
0222                 {"canonical"_ls, true},
0223                 {"via"_ls, QJsonArray{domain()}},
0224             },
0225             parent,
0226         });
0227     }
0228 
0229     const auto job = Connection::createRoom(Connection::PublishRoom, QString(), name, topic, QStringList(), {}, {}, {}, initialStateEvents);
0230     if (!parent.isEmpty()) {
0231         connect(job, &Quotient::CreateRoomJob::success, this, [this, parent, setChildParent, job]() {
0232             if (setChildParent) {
0233                 if (auto parentRoom = room(parent)) {
0234                     parentRoom->setState(QLatin1String("m.space.child"), job->roomId(), QJsonObject{{QLatin1String("via"), QJsonArray{domain()}}});
0235                 }
0236             }
0237         });
0238     }
0239     connect(job, &CreateRoomJob::failure, this, [job] {
0240         Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()), {});
0241     });
0242     connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
0243         RoomManager::instance().resolveResource(room->id());
0244     });
0245 }
0246 
0247 void NeoChatConnection::createSpace(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
0248 {
0249     QList<CreateRoomJob::StateEvent> initialStateEvents;
0250     if (!parent.isEmpty()) {
0251         initialStateEvents.append(CreateRoomJob::StateEvent{
0252             "m.space.parent"_ls,
0253             QJsonObject{
0254                 {"canonical"_ls, true},
0255                 {"via"_ls, QJsonArray{domain()}},
0256             },
0257             parent,
0258         });
0259     }
0260 
0261     const auto job = Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, initialStateEvents, {}, QJsonObject{{"type"_ls, "m.space"_ls}});
0262     if (!parent.isEmpty()) {
0263         connect(job, &Quotient::CreateRoomJob::success, this, [this, parent, setChildParent, job]() {
0264             if (setChildParent) {
0265                 if (auto parentRoom = room(parent)) {
0266                     parentRoom->setState(QLatin1String("m.space.child"), job->roomId(), QJsonObject{{QLatin1String("via"), QJsonArray{domain()}}});
0267                 }
0268             }
0269         });
0270     }
0271     connect(job, &CreateRoomJob::failure, this, [job] {
0272         Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()), {});
0273     });
0274     connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
0275         RoomManager::instance().resolveResource(room->id());
0276     });
0277 }
0278 
0279 bool NeoChatConnection::directChatExists(Quotient::User *user)
0280 {
0281     return directChats().contains(user);
0282 }
0283 
0284 void NeoChatConnection::openOrCreateDirectChat(const QString &userId)
0285 {
0286     if (auto user = this->user(userId)) {
0287         openOrCreateDirectChat(user);
0288     } else {
0289         qWarning() << "openOrCreateDirectChat: Couldn't get user object for ID " << userId << ", unable to open/request direct chat.";
0290     }
0291 }
0292 
0293 void NeoChatConnection::openOrCreateDirectChat(User *user)
0294 {
0295     const auto existing = directChats();
0296 
0297     if (existing.contains(user)) {
0298         const auto room = this->room(existing.value(user));
0299         if (room) {
0300             RoomManager::instance().resolveResource(room->id());
0301             return;
0302         }
0303     }
0304     requestDirectChat(user);
0305 }
0306 
0307 qsizetype NeoChatConnection::directChatNotifications() const
0308 {
0309     qsizetype notifications = 0;
0310     QStringList added; // The same ID can be in the list multiple times.
0311     for (const auto &chatId : directChats()) {
0312         if (!added.contains(chatId)) {
0313             if (const auto chat = room(chatId)) {
0314                 notifications += chat->notificationCount();
0315                 added += chatId;
0316             }
0317         }
0318     }
0319     return notifications;
0320 }
0321 
0322 bool NeoChatConnection::directChatInvites() const
0323 {
0324     auto inviteRooms = rooms(JoinState::Invite);
0325     for (const auto inviteRoom : inviteRooms) {
0326         if (inviteRoom->isDirectChat()) {
0327             return true;
0328         }
0329     }
0330     return false;
0331 }
0332 
0333 QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
0334 {
0335 #ifdef HAVE_KUNIFIEDPUSH
0336     QUrl gatewayEndpoint(endpoint);
0337     gatewayEndpoint.setPath(QStringLiteral("/_matrix/push/v1/notify"));
0338 
0339     QNetworkRequest checkGateway(gatewayEndpoint);
0340     auto reply = co_await NetworkAccessManager::instance()->get(checkGateway);
0341 
0342     // We want to check if this UnifiedPush server has a Matrix gateway
0343     // This is because Matrix does not natively support UnifiedPush
0344     const auto &replyJson = QJsonDocument::fromJson(reply->readAll()).object();
0345 
0346     if (replyJson["unifiedpush"_L1]["gateway"_L1].toString() == QStringLiteral("matrix")) {
0347         callApi<PostPusherJob>(endpoint,
0348                                QStringLiteral("http"),
0349                                QStringLiteral("org.kde.neochat"),
0350                                QStringLiteral("NeoChat"),
0351                                deviceId(),
0352                                QString(), // profileTag is intentionally left empty for now, it's optional
0353                                QStringLiteral("en-US"),
0354                                PostPusherJob::PusherData{QUrl::fromUserInput(gatewayEndpoint.toString()), QStringLiteral(" ")},
0355                                false);
0356 
0357         qInfo() << "Registered for push notifications";
0358     } else {
0359         qWarning() << "There's no gateway, not setting up push notifications.";
0360     }
0361 #else
0362     co_return;
0363 #endif
0364 }
0365 
0366 QString NeoChatConnection::deviceKey() const
0367 {
0368     return edKeyForUserDevice(userId(), deviceId());
0369 }
0370 
0371 QString NeoChatConnection::encryptionKey() const
0372 {
0373     auto query = database()->prepareQuery(QStringLiteral("SELECT curveKey FROM tracked_devices WHERE matrixId=:matrixId AND deviceid=:deviceId LIMIT 1;"));
0374     query.bindValue(QStringLiteral(":matrixId"), userId());
0375     query.bindValue(QStringLiteral(":deviceId"), deviceId());
0376     database()->execute(query);
0377     if (!query.next()) {
0378         return {};
0379     }
0380     return query.value(0).toString();
0381 }
0382 
0383 bool NeoChatConnection::isOnline() const
0384 {
0385     return m_isOnline;
0386 }
0387 
0388 void NeoChatConnection::setIsOnline(bool isOnline)
0389 {
0390     if (isOnline == m_isOnline) {
0391         return;
0392     }
0393     m_isOnline = isOnline;
0394     Q_EMIT isOnlineChanged();
0395 }
0396 
0397 #include "moc_neochatconnection.cpp"