File indexing completed on 2024-09-15 04:28:33
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 ¤tPassword, 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"