1 | // SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> |
2 | // SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> |
3 | // SPDX-FileCopyrightText: 2019 Ville Ranki <ville.ranki@iki.fi> |
4 | // SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> |
5 | // SPDX-License-Identifier: LGPL-2.1-or-later |
6 | |
7 | #include "connection.h" |
8 | |
9 | #include "logging_categories_p.h" |
10 | |
11 | #include "connection_p.h" |
12 | #include "connectiondata.h" |
13 | #include "qt_connection_util.h" |
14 | #include "room.h" |
15 | #include "settings.h" |
16 | #include "user.h" |
17 | |
18 | // NB: since Qt 6, moc_connection.cpp needs Room and User fully defined |
19 | #include "moc_connection.cpp" // NOLINT(bugprone-suspicious-include) |
20 | |
21 | #include "csapi/account-data.h" |
22 | #include "csapi/capabilities.h" |
23 | #include "csapi/joining.h" |
24 | #include "csapi/leaving.h" |
25 | #include "csapi/logout.h" |
26 | #include "csapi/room_send.h" |
27 | #include "csapi/to_device.h" |
28 | #include "csapi/voip.h" |
29 | #include "csapi/wellknown.h" |
30 | #include "csapi/whoami.h" |
31 | |
32 | #include "events/directchatevent.h" |
33 | #include "jobs/downloadfilejob.h" |
34 | #include "jobs/mediathumbnailjob.h" |
35 | #include "jobs/syncjob.h" |
36 | |
37 | #ifdef Quotient_E2EE_ENABLED |
38 | # include "connectionencryptiondata_p.h" |
39 | # include "database.h" |
40 | |
41 | # include "e2ee/qolminboundsession.h" |
42 | #endif // Quotient_E2EE_ENABLED |
43 | |
44 | #if QT_VERSION_MAJOR >= 6 |
45 | # include <qt6keychain/keychain.h> |
46 | #else |
47 | # include <qt5keychain/keychain.h> |
48 | #endif |
49 | |
50 | #include <QtCore/QCoreApplication> |
51 | #include <QtCore/QDir> |
52 | #include <QtCore/QElapsedTimer> |
53 | #include <QtCore/QFile> |
54 | #include <QtCore/QMimeDatabase> |
55 | #include <QtCore/QRegularExpression> |
56 | #include <QtCore/QStandardPaths> |
57 | #include <QtCore/QStringBuilder> |
58 | #include <QtNetwork/QDnsLookup> |
59 | |
60 | using namespace Quotient; |
61 | |
62 | // This is very much Qt-specific; STL iterators don't have key() and value() |
63 | template <typename HashT, typename Pred> |
64 | HashT remove_if(HashT& hashMap, Pred pred) |
65 | { |
66 | HashT removals; |
67 | for (auto it = hashMap.begin(); it != hashMap.end();) { |
68 | if (pred(it)) { |
69 | removals.insert(it.key(), it.value()); |
70 | it = hashMap.erase(it); |
71 | } else |
72 | ++it; |
73 | } |
74 | return removals; |
75 | } |
76 | |
77 | Connection::Connection(const QUrl& server, QObject* parent) |
78 | : QObject(parent) |
79 | , d(makeImpl<Private>(args: std::make_unique<ConnectionData>(args: server))) |
80 | { |
81 | #ifdef Quotient_E2EE_ENABLED |
82 | //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount); |
83 | #endif |
84 | d->q = this; // All d initialization should occur before this line |
85 | setObjectName(server.toString()); |
86 | } |
87 | |
88 | Connection::Connection(QObject* parent) : Connection({}, parent) {} |
89 | |
90 | Connection::~Connection() |
91 | { |
92 | qCDebug(MAIN) << "deconstructing connection object for" << userId(); |
93 | stopSync(); |
94 | } |
95 | |
96 | void Connection::resolveServer(const QString& mxid) |
97 | { |
98 | if (isJobPending(job: d->resolverJob)) |
99 | d->resolverJob->abandon(); |
100 | |
101 | auto maybeBaseUrl = QUrl::fromUserInput(userInput: serverPart(mxId: mxid)); |
102 | maybeBaseUrl.setScheme("https"_ls ); // Instead of the Qt-default "http" |
103 | if (maybeBaseUrl.isEmpty() || !maybeBaseUrl.isValid()) { |
104 | emit resolveError(error: tr(s: "%1 is not a valid homeserver address" ) |
105 | .arg(a: maybeBaseUrl.toString())); |
106 | return; |
107 | } |
108 | |
109 | qCDebug(MAIN) << "Finding the server" << maybeBaseUrl.host(); |
110 | |
111 | const auto& oldBaseUrl = d->data->baseUrl(); |
112 | d->data->setBaseUrl(maybeBaseUrl); // Temporarily set it for this one call |
113 | d->resolverJob = callApi<GetWellknownJob>(); |
114 | // Connect to finished() to make sure baseUrl is restored in any case |
115 | connect(sender: d->resolverJob, signal: &BaseJob::finished, context: this, slot: [this, maybeBaseUrl, oldBaseUrl] { |
116 | // Revert baseUrl so that setHomeserver() below triggers signals |
117 | // in case the base URL actually changed |
118 | d->data->setBaseUrl(oldBaseUrl); |
119 | if (d->resolverJob->error() == BaseJob::Abandoned) |
120 | return; |
121 | |
122 | if (d->resolverJob->error() != BaseJob::NotFound) { |
123 | if (!d->resolverJob->status().good()) { |
124 | qCWarning(MAIN) |
125 | << "Fetching .well-known file failed, FAIL_PROMPT" ; |
126 | emit resolveError(error: tr(s: "Failed resolving the homeserver" )); |
127 | return; |
128 | } |
129 | const QUrl baseUrl { d->resolverJob->data().homeserver.baseUrl }; |
130 | if (baseUrl.isEmpty()) { |
131 | qCWarning(MAIN) << "base_url not provided, FAIL_PROMPT" ; |
132 | emit resolveError( |
133 | error: tr(s: "The homeserver base URL is not provided" )); |
134 | return; |
135 | } |
136 | if (!baseUrl.isValid()) { |
137 | qCWarning(MAIN) << "base_url invalid, FAIL_ERROR" ; |
138 | emit resolveError(error: tr(s: "The homeserver base URL is invalid" )); |
139 | return; |
140 | } |
141 | qCInfo(MAIN) << ".well-known URL for" << maybeBaseUrl.host() << "is" |
142 | << baseUrl.toString(); |
143 | setHomeserver(baseUrl); |
144 | } else { |
145 | qCInfo(MAIN) << "No .well-known file, using" << maybeBaseUrl |
146 | << "for base URL" ; |
147 | setHomeserver(maybeBaseUrl); |
148 | } |
149 | Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver() |
150 | }); |
151 | } |
152 | |
153 | inline UserIdentifier makeUserIdentifier(const QString& id) |
154 | { |
155 | return { QStringLiteral("m.id.user" ), .additionalProperties: { { QStringLiteral("user" ), id } } }; |
156 | } |
157 | |
158 | inline UserIdentifier make3rdPartyIdentifier(const QString& medium, |
159 | const QString& address) |
160 | { |
161 | return { QStringLiteral("m.id.thirdparty" ), |
162 | .additionalProperties: { { QStringLiteral("medium" ), medium }, |
163 | { QStringLiteral("address" ), address } } }; |
164 | } |
165 | |
166 | void Connection::loginWithPassword(const QString& userId, |
167 | const QString& password, |
168 | const QString& initialDeviceName, |
169 | const QString& deviceId) |
170 | { |
171 | d->checkAndConnect(userId, connectFn: [=,this] { |
172 | d->loginToServer(loginArgs: LoginFlows::Password.type, loginArgs: makeUserIdentifier(id: userId), |
173 | loginArgs: password, /*token*/ loginArgs: QString(), loginArgs: deviceId, loginArgs: initialDeviceName); |
174 | }, flow: LoginFlows::Password); |
175 | } |
176 | |
177 | SsoSession* Connection::(const QString& initialDeviceName, |
178 | const QString& deviceId) |
179 | { |
180 | return new SsoSession(this, initialDeviceName, deviceId); |
181 | } |
182 | |
183 | void Connection::loginWithToken(const QString& loginToken, |
184 | const QString& initialDeviceName, |
185 | const QString& deviceId) |
186 | { |
187 | Q_ASSERT(d->data->baseUrl().isValid() && d->loginFlows.contains(LoginFlows::Token)); |
188 | d->loginToServer(loginArgs: LoginFlows::Token.type, |
189 | loginArgs: none /*user is encoded in loginToken*/, loginArgs: QString() /*password*/, |
190 | loginArgs: loginToken, loginArgs: deviceId, loginArgs: initialDeviceName); |
191 | } |
192 | |
193 | void Connection::assumeIdentity(const QString& mxId, const QString& accessToken, |
194 | [[maybe_unused]] const QString& deviceId) |
195 | { |
196 | assumeIdentity(mxId, accessToken); |
197 | } |
198 | |
199 | void Connection::assumeIdentity(const QString& mxId, const QString& accessToken) |
200 | { |
201 | d->checkAndConnect(userId: mxId, connectFn: [this, mxId, accessToken] { |
202 | d->data->setToken(accessToken.toLatin1()); |
203 | auto* job = callApi<GetTokenOwnerJob>(); |
204 | connect(sender: job, signal: &BaseJob::success, context: this, slot: [this, job, mxId] { |
205 | if (mxId != job->userId()) |
206 | qCWarning(MAIN).nospace() |
207 | << "The access_token owner (" << job->userId() |
208 | << ") is different from passed MXID (" << mxId << ")!" ; |
209 | d->data->setDeviceId(job->deviceId()); |
210 | d->completeSetup(mxId: job->userId()); |
211 | }); |
212 | connect(sender: job, signal: &BaseJob::failure, context: this, slot: [this, job] { |
213 | if (job->error() == BaseJob::StatusCode::NetworkError) |
214 | emit networkError(message: job->errorString(), details: job->rawDataSample(), |
215 | retriesTaken: job->maxRetries(), nextRetryInMilliseconds: -1); |
216 | else |
217 | emit loginError(message: job->errorString(), details: job->rawDataSample()); |
218 | }); |
219 | }); |
220 | } |
221 | |
222 | void Connection::reloadCapabilities() |
223 | { |
224 | d->capabilitiesJob = callApi<GetCapabilitiesJob>(runningPolicy: BackgroundRequest); |
225 | connect(sender: d->capabilitiesJob, signal: &BaseJob::success, context: this, slot: [this] { |
226 | d->capabilities = d->capabilitiesJob->capabilities(); |
227 | |
228 | if (d->capabilities.roomVersions) { |
229 | qCDebug(MAIN) << "Room versions:" << defaultRoomVersion() |
230 | << "is default, full list:" << availableRoomVersions(); |
231 | emit capabilitiesLoaded(); |
232 | for (auto* r: std::as_const(t&: d->roomMap)) |
233 | r->checkVersion(); |
234 | } else |
235 | qCWarning(MAIN) |
236 | << "The server returned an empty set of supported versions;" |
237 | " disabling version upgrade recommendations to reduce noise" ; |
238 | }); |
239 | connect(sender: d->capabilitiesJob, signal: &BaseJob::failure, context: this, slot: [this] { |
240 | if (d->capabilitiesJob->error() == BaseJob::IncorrectRequest) |
241 | qCDebug(MAIN) << "Server doesn't support /capabilities;" |
242 | " version upgrade recommendations won't be issued" ; |
243 | }); |
244 | } |
245 | |
246 | bool Connection::loadingCapabilities() const |
247 | { |
248 | // (Ab)use the fact that room versions cannot be omitted after |
249 | // the capabilities have been loaded (see reloadCapabilities() above). |
250 | return !d->capabilities.roomVersions; |
251 | } |
252 | |
253 | void Connection::Private::saveAccessTokenToKeychain() const |
254 | { |
255 | qCDebug(MAIN) << "Saving access token to keychain for" << q->userId(); |
256 | auto job = new QKeychain::WritePasswordJob(qAppName()); |
257 | job->setKey(q->userId()); |
258 | job->setBinaryData(data->accessToken()); |
259 | job->start(); |
260 | QObject::connect(sender: job, signal: &QKeychain::Job::finished, context: q, slot: [job] { |
261 | if (job->error() == QKeychain::Error::NoError) |
262 | return; |
263 | qWarning(catFunc: MAIN).noquote() |
264 | << "Could not save access token to the keychain:" |
265 | << qUtf8Printable(job->errorString()); |
266 | // TODO: emit a signal |
267 | }); |
268 | |
269 | } |
270 | |
271 | void Connection::Private::dropAccessToken() |
272 | { |
273 | // TODO: emit a signal on important (i.e. access denied) keychain errors |
274 | using namespace QKeychain; |
275 | qCDebug(MAIN) << "Removing access token from keychain for" << q->userId(); |
276 | auto job = new DeletePasswordJob(qAppName()); |
277 | job->setKey(q->userId()); |
278 | job->start(); |
279 | QObject::connect(sender: job, signal: &Job::finished, context: q, slot: [job] { |
280 | if (job->error() == Error::NoError |
281 | || job->error() == Error::EntryNotFound) |
282 | return; |
283 | qWarning(catFunc: MAIN).noquote() |
284 | << "Could not delete access token from the keychain:" |
285 | << qUtf8Printable(job->errorString()); |
286 | }); |
287 | |
288 | auto pickleJob = new DeletePasswordJob(qAppName()); |
289 | pickleJob->setKey(q->userId() + "-Pickle"_ls ); |
290 | pickleJob->start(); |
291 | QObject::connect(sender: job, signal: &Job::finished, context: q, slot: [job] { |
292 | if (job->error() == Error::NoError |
293 | || job->error() == Error::EntryNotFound) |
294 | return; |
295 | qWarning(catFunc: MAIN).noquote() |
296 | << "Could not delete account pickle from the keychain:" |
297 | << qUtf8Printable(job->errorString()); |
298 | }); |
299 | |
300 | data->setToken({}); |
301 | } |
302 | |
303 | template <typename... LoginArgTs> |
304 | void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) |
305 | { |
306 | auto loginJob = |
307 | q->callApi<LoginJob>(std::forward<LoginArgTs>(loginArgs)...); |
308 | connect(loginJob, &BaseJob::success, q, [this, loginJob] { |
309 | data->setToken(loginJob->accessToken().toLatin1()); |
310 | data->setDeviceId(loginJob->deviceId()); |
311 | completeSetup(mxId: loginJob->userId()); |
312 | saveAccessTokenToKeychain(); |
313 | #ifdef Quotient_E2EE_ENABLED |
314 | if (encryptionData) |
315 | encryptionData->database.clear(); |
316 | #endif |
317 | }); |
318 | connect(loginJob, &BaseJob::failure, q, [this, loginJob] { |
319 | emit q->loginError(message: loginJob->errorString(), details: loginJob->rawDataSample()); |
320 | }); |
321 | } |
322 | |
323 | void Connection::Private::completeSetup(const QString& mxId, bool mock) |
324 | { |
325 | data->setUserId(mxId); |
326 | if (!mock) |
327 | q->user()->load(); // Load the local user's profile |
328 | q->setObjectName(data->userId() % u'/' % data->deviceId()); |
329 | qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() |
330 | << "by user" << data->userId() |
331 | << "from device" << data->deviceId(); |
332 | connect(qApp, signal: &QCoreApplication::aboutToQuit, context: q, slot: &Connection::saveState); |
333 | |
334 | static auto callOnce [[maybe_unused]] = // |
335 | (qInfo(catFunc: MAIN) << "The library is built" |
336 | << (E2EE_Enabled ? "with" : "without" ) |
337 | << "end-to-end encryption (E2EE)" , |
338 | 0); |
339 | #ifdef Quotient_E2EE_ENABLED |
340 | if (useEncryption) { |
341 | if (auto&& maybeEncryptionData = |
342 | _impl::ConnectionEncryptionData::setup(q, mock)) { |
343 | encryptionData = std::move(*maybeEncryptionData); |
344 | } else { |
345 | Q_ASSERT(false); |
346 | useEncryption = false; |
347 | emit q->encryptionChanged(false); |
348 | } |
349 | } else |
350 | qCInfo(E2EE) << "End-to-end encryption (E2EE) support is off for" |
351 | << q->objectName(); |
352 | #endif |
353 | |
354 | emit q->stateChanged(); |
355 | emit q->connected(); |
356 | if (!mock) |
357 | q->reloadCapabilities(); |
358 | } |
359 | |
360 | void Connection::Private::checkAndConnect(const QString& userId, |
361 | const std::function<void()>& connectFn, |
362 | const std::optional<LoginFlow>& flow) |
363 | { |
364 | if (data->baseUrl().isValid() && (!flow || loginFlows.contains(t: *flow))) { |
365 | q->setObjectName(userId % u"(?)" ); |
366 | connectFn(); |
367 | return; |
368 | } |
369 | // Not good to go, try to ascertain the homeserver URL and flows |
370 | if (userId.startsWith(c: u'@') && userId.indexOf(c: u':') != -1) { |
371 | q->setObjectName(userId % u"(?)" ); |
372 | q->resolveServer(mxid: userId); |
373 | if (flow) |
374 | connectSingleShot(sender: q, signal: &Connection::loginFlowsChanged, context: q, |
375 | slot: [this, flow, connectFn] { |
376 | if (loginFlows.contains(t: *flow)) |
377 | connectFn(); |
378 | else |
379 | emit q->loginError( |
380 | message: tr(s: "Unsupported login flow" ), |
381 | details: tr(s: "The homeserver at %1 does not support" |
382 | " the login flow '%2'" ) |
383 | .arg(args: data->baseUrl().toDisplayString(), |
384 | args: flow->type)); |
385 | }); |
386 | else |
387 | connectSingleShot(sender: q, signal: &Connection::homeserverChanged, context: q, slot: connectFn); |
388 | } else |
389 | emit q->resolveError(error: tr(s: "Please provide the fully-qualified user id" |
390 | " (such as @user:example.org) so that the" |
391 | " homeserver could be resolved; the current" |
392 | " homeserver URL(%1) is not good" ) |
393 | .arg(a: data->baseUrl().toDisplayString())); |
394 | } |
395 | |
396 | void Connection::logout() |
397 | { |
398 | // If there's an ongoing sync job, stop it (this also suspends sync loop) |
399 | const auto wasSyncing = bool(d->syncJob); |
400 | if (wasSyncing) |
401 | { |
402 | d->syncJob->abandon(); |
403 | d->syncJob = nullptr; |
404 | } |
405 | |
406 | d->logoutJob = callApi<LogoutJob>(); |
407 | emit stateChanged(); // isLoggedIn() == false from now |
408 | |
409 | connect(sender: d->logoutJob, signal: &LogoutJob::finished, context: this, slot: [this, wasSyncing] { |
410 | if (d->logoutJob->status().good() |
411 | || d->logoutJob->error() == BaseJob::Unauthorised |
412 | || d->logoutJob->error() == BaseJob::ContentAccessError) { |
413 | if (d->syncLoopConnection) |
414 | disconnect(d->syncLoopConnection); |
415 | SettingsGroup("Accounts"_ls ).remove(key: userId()); |
416 | d->dropAccessToken(); |
417 | emit loggedOut(); |
418 | deleteLater(); |
419 | } else { // logout() somehow didn't proceed - restore the session state |
420 | emit stateChanged(); |
421 | if (wasSyncing) |
422 | syncLoopIteration(); // Resume sync loop (or a single sync) |
423 | } |
424 | }); |
425 | } |
426 | |
427 | void Connection::sync(int timeout) |
428 | { |
429 | if (d->syncJob) { |
430 | qCInfo(MAIN) << d->syncJob << "is already running" ; |
431 | return; |
432 | } |
433 | if (!isLoggedIn()) { |
434 | qCWarning(MAIN) << "Not logged in, not going to sync" ; |
435 | return; |
436 | } |
437 | |
438 | d->syncTimeout = timeout; |
439 | Filter filter; |
440 | filter.room.timeline.limit.emplace(val: 100); |
441 | filter.room.state.lazyLoadMembers.emplace(args&: d->lazyLoading); |
442 | auto job = d->syncJob = |
443 | callApi<SyncJob>(runningPolicy: BackgroundRequest, jobArgs: d->data->lastEvent(), jobArgs&: filter, |
444 | jobArgs&: timeout); |
445 | connect(sender: job, signal: &SyncJob::success, context: this, slot: [this, job] { |
446 | onSyncSuccess(data: job->takeData()); |
447 | d->syncJob = nullptr; |
448 | emit syncDone(); |
449 | }); |
450 | connect(sender: job, signal: &SyncJob::retryScheduled, context: this, |
451 | slot: [this, job](int retriesTaken, int nextInMilliseconds) { |
452 | emit networkError(message: job->errorString(), details: job->rawDataSample(), |
453 | retriesTaken, nextRetryInMilliseconds: nextInMilliseconds); |
454 | }); |
455 | connect(sender: job, signal: &SyncJob::failure, context: this, slot: [this, job] { |
456 | // SyncJob persists with retries on transient errors; if it fails, |
457 | // there's likely something serious enough to stop the loop. |
458 | stopSync(); |
459 | if (job->error() == BaseJob::Unauthorised) { |
460 | qCWarning(SYNCJOB) |
461 | << "Sync job failed with Unauthorised - login expired?" ; |
462 | emit loginError(message: job->errorString(), details: job->rawDataSample()); |
463 | } else |
464 | emit syncError(message: job->errorString(), details: job->rawDataSample()); |
465 | }); |
466 | } |
467 | |
468 | void Connection::syncLoop(int timeout) |
469 | { |
470 | if (d->syncLoopConnection && d->syncTimeout == timeout) { |
471 | qCInfo(MAIN) << "Attempt to run sync loop but there's one already " |
472 | "running; nothing will be done" ; |
473 | return; |
474 | } |
475 | std::swap(a&: d->syncTimeout, b&: timeout); |
476 | if (d->syncLoopConnection) { |
477 | qCInfo(MAIN) << "Timeout for next syncs changed from" << timeout // |
478 | << "to" << d->syncTimeout; |
479 | } else { |
480 | d->syncLoopConnection = connect(sender: this, signal: &Connection::syncDone, |
481 | context: this, slot: &Connection::syncLoopIteration, |
482 | type: Qt::QueuedConnection); |
483 | syncLoopIteration(); // initial sync to start the loop |
484 | } |
485 | } |
486 | |
487 | void Connection::syncLoopIteration() |
488 | { |
489 | if (isLoggedIn()) |
490 | sync(timeout: d->syncTimeout); |
491 | else |
492 | qCInfo(MAIN) << "Logged out, sync loop will stop now" ; |
493 | } |
494 | |
495 | QJsonObject toJson(const DirectChatsMap& directChats) |
496 | { |
497 | QJsonObject json; |
498 | for (auto it = directChats.begin(); it != directChats.end();) { |
499 | QJsonArray roomIds; |
500 | const auto* user = it.key(); |
501 | for (; it != directChats.end() && it.key() == user; ++it) |
502 | roomIds.append(value: *it); |
503 | json.insert(key: user->id(), value: roomIds); |
504 | } |
505 | return json; |
506 | } |
507 | |
508 | void Connection::onSyncSuccess(SyncData&& data, bool fromCache) |
509 | { |
510 | #ifdef Quotient_E2EE_ENABLED |
511 | if (d->encryptionData) { |
512 | d->encryptionData->onSyncSuccess(data); |
513 | } |
514 | #endif |
515 | d->consumeToDeviceEvents(toDeviceEvents: data.takeToDeviceEvents()); |
516 | d->data->setLastEvent(data.nextBatch()); |
517 | d->consumeRoomData(roomDataList: data.takeRoomData(), fromCache); |
518 | d->consumeAccountData(accountDataEvents: data.takeAccountData()); |
519 | d->consumePresenceData(presenceData: data.takePresenceData()); |
520 | #ifdef Quotient_E2EE_ENABLED |
521 | if(d->encryptionData && d->encryptionData->encryptionUpdateRequired) { |
522 | d->encryptionData->loadOutdatedUserDevices(); |
523 | d->encryptionData->encryptionUpdateRequired = false; |
524 | } |
525 | #endif |
526 | Q_UNUSED(std::move(data)) // Tell static analysers `data` is consumed now |
527 | } |
528 | |
529 | void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, |
530 | bool fromCache) |
531 | { |
532 | for (auto&& roomData: roomDataList) { |
533 | const auto forgetIdx = roomIdsToForget.indexOf(str: roomData.roomId); |
534 | if (forgetIdx != -1) { |
535 | roomIdsToForget.removeAt(i: forgetIdx); |
536 | if (roomData.joinState == JoinState::Leave) { |
537 | qDebug(catFunc: MAIN) |
538 | << "Room" << roomData.roomId |
539 | << "has been forgotten, ignoring /sync response for it" ; |
540 | continue; |
541 | } |
542 | qWarning(catFunc: MAIN) << "Room" << roomData.roomId |
543 | << "has just been forgotten but /sync returned it in" |
544 | << terse << roomData.joinState |
545 | << "state - suspiciously fast turnaround" ; |
546 | } |
547 | if (auto* r = q->provideRoom(roomId: roomData.roomId, joinState: roomData.joinState)) { |
548 | pendingStateRoomIds.removeOne(t: roomData.roomId); |
549 | // Update rooms one by one, giving time to update the UI. |
550 | QMetaObject::invokeMethod( |
551 | object: r, |
552 | function: [r, rd = std::move(roomData), fromCache] () mutable { |
553 | r->updateData(data: std::move(rd), fromCache); |
554 | }, |
555 | type: Qt::QueuedConnection); |
556 | } |
557 | } |
558 | } |
559 | |
560 | void Connection::Private::consumeAccountData(Events&& accountDataEvents) |
561 | { |
562 | // After running this loop, the account data events not saved in |
563 | // accountData (see the end of the loop body) are auto-cleaned away |
564 | for (auto&& eventPtr: accountDataEvents) { |
565 | switchOnType(event: *eventPtr, |
566 | fn1: [this](const DirectChatEvent& dce) { |
567 | // https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events |
568 | const auto& usersToDCs = dce.usersToDirectChats(); |
569 | DirectChatsMap remoteRemovals = |
570 | remove_if(hashMap&: directChats, pred: [&usersToDCs, this](auto it) { |
571 | return !( |
572 | usersToDCs.contains(it.key()->id(), it.value()) |
573 | || dcLocalAdditions.contains(it.key(), it.value())); |
574 | }); |
575 | remove_if(hashMap&: directChatUsers, pred: [&remoteRemovals](auto it) { |
576 | return remoteRemovals.contains(it.value(), it.key()); |
577 | }); |
578 | // Remove from dcLocalRemovals what the server already has. |
579 | remove_if(hashMap&: dcLocalRemovals, pred: [&remoteRemovals](auto it) { |
580 | return remoteRemovals.contains(it.key(), it.value()); |
581 | }); |
582 | if (MAIN().isDebugEnabled()) |
583 | for (auto it = remoteRemovals.begin(); |
584 | it != remoteRemovals.end(); ++it) { |
585 | qCDebug(MAIN) |
586 | << it.value() << "is no more a direct chat with" |
587 | << it.key()->id(); |
588 | } |
589 | |
590 | DirectChatsMap remoteAdditions; |
591 | for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) { |
592 | if (auto* u = q->user(uId: it.key())) { |
593 | if (!directChats.contains(key: u, value: it.value()) |
594 | && !dcLocalRemovals.contains(key: u, value: it.value())) { |
595 | Q_ASSERT(!directChatUsers.contains(it.value(), u)); |
596 | remoteAdditions.insert(key: u, value: it.value()); |
597 | directChats.insert(key: u, value: it.value()); |
598 | directChatUsers.insert(key: it.value(), value: u); |
599 | qCDebug(MAIN) << "Marked room" << it.value() |
600 | << "as a direct chat with" << u->id(); |
601 | } |
602 | } else |
603 | qCWarning(MAIN) |
604 | << "Couldn't get a user object for" << it.key(); |
605 | } |
606 | // Remove from dcLocalAdditions what the server already has. |
607 | remove_if(hashMap&: dcLocalAdditions, pred: [&remoteAdditions](auto it) { |
608 | return remoteAdditions.contains(it.key(), it.value()); |
609 | }); |
610 | if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) |
611 | emit q->directChatsListChanged(additions: remoteAdditions, |
612 | removals: remoteRemovals); |
613 | }, |
614 | // catch-all, passing eventPtr for a possible take-over |
615 | fns: [this, &eventPtr](const Event& accountEvent) { |
616 | if (is<IgnoredUsersEvent>(e: accountEvent)) |
617 | qCDebug(MAIN) |
618 | << "Users ignored by" << data->userId() << "updated:" |
619 | << QStringList(q->ignoredUsers().values()).join(sep: u','); |
620 | |
621 | auto& currentData = accountData[accountEvent.matrixType()]; |
622 | // A polymorphic event-specific comparison might be a bit |
623 | // more efficient; maaybe do it another day |
624 | if (!currentData |
625 | || currentData->contentJson() != accountEvent.contentJson()) { |
626 | currentData = std::move(eventPtr); |
627 | qCDebug(MAIN) << "Updated account data of type" |
628 | << currentData->matrixType(); |
629 | emit q->accountDataChanged(type: currentData->matrixType()); |
630 | } |
631 | }); |
632 | } |
633 | if (!dcLocalAdditions.isEmpty() || !dcLocalRemovals.isEmpty()) { |
634 | qDebug(catFunc: MAIN) << "Sending updated direct chats to the server:" |
635 | << dcLocalRemovals.size() << "removal(s)," |
636 | << dcLocalAdditions.size() << "addition(s)" ; |
637 | q->callApi<SetAccountDataJob>(jobArgs: data->userId(), QStringLiteral("m.direct" ), |
638 | jobArgs: toJson(directChats)); |
639 | dcLocalAdditions.clear(); |
640 | dcLocalRemovals.clear(); |
641 | } |
642 | } |
643 | |
644 | void Connection::Private::consumePresenceData(Events&& presenceData) |
645 | { |
646 | // To be implemented |
647 | } |
648 | |
649 | void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) |
650 | { |
651 | #ifdef Quotient_E2EE_ENABLED |
652 | if (encryptionData) |
653 | encryptionData->consumeToDeviceEvents(std::move(toDeviceEvents)); |
654 | #endif |
655 | } |
656 | |
657 | void Connection::stopSync() |
658 | { |
659 | // If there's a sync loop, break it |
660 | disconnect(d->syncLoopConnection); |
661 | if (d->syncJob) // If there's an ongoing sync job, stop it too |
662 | { |
663 | if (d->syncJob->status().code == BaseJob::Pending) |
664 | d->syncJob->abandon(); |
665 | d->syncJob = nullptr; |
666 | } |
667 | } |
668 | |
669 | QString Connection::nextBatchToken() const { return d->data->lastEvent(); } |
670 | |
671 | JoinRoomJob* Connection::joinRoom(const QString& roomAlias, |
672 | const QStringList& serverNames) |
673 | { |
674 | auto* const job = callApi<JoinRoomJob>(jobArgs: roomAlias, jobArgs: serverNames); |
675 | // Upon completion, ensure a room object is created in case it hasn't come |
676 | // with a sync yet. If the room object is not there, provideRoom() will |
677 | // create it in Join state. finished() is used here instead of success() |
678 | // to overtake clients that may add their own slots to finished(). |
679 | connect(sender: job, signal: &BaseJob::finished, context: this, slot: [this, job] { |
680 | if (job->status().good()) |
681 | provideRoom(roomId: job->roomId()); |
682 | }); |
683 | return job; |
684 | } |
685 | |
686 | LeaveRoomJob* Connection::leaveRoom(Room* room) |
687 | { |
688 | const auto& roomId = room->id(); |
689 | const auto job = callApi<LeaveRoomJob>(jobArgs: roomId); |
690 | if (room->joinState() == JoinState::Invite) { |
691 | // Workaround matrix-org/synapse#2181 - if the room is in invite state |
692 | // the invite may have been cancelled but Synapse didn't send it in |
693 | // `/sync`. See also #273 for the discussion in the library context. |
694 | d->pendingStateRoomIds.push_back(t: roomId); |
695 | connect(sender: job, signal: &LeaveRoomJob::success, context: this, slot: [this, roomId] { |
696 | if (d->pendingStateRoomIds.removeOne(t: roomId)) { |
697 | qCDebug(MAIN) << "Forcing the room to Leave status" ; |
698 | provideRoom(roomId, joinState: JoinState::Leave); |
699 | } |
700 | }); |
701 | } |
702 | return job; |
703 | } |
704 | |
705 | inline auto splitMediaId(const QString& mediaId) |
706 | { |
707 | auto idParts = mediaId.split(sep: u'/'); |
708 | Q_ASSERT_X(idParts.size() == 2, __FUNCTION__, |
709 | qPrintable("'"_ls % mediaId |
710 | % "' doesn't look like 'serverName/localMediaId'"_ls )); |
711 | return idParts; |
712 | } |
713 | |
714 | QUrl Connection::makeMediaUrl(QUrl mxcUrl) const |
715 | { |
716 | Q_ASSERT(mxcUrl.scheme() == "mxc"_ls ); |
717 | QUrlQuery q(mxcUrl.query()); |
718 | q.addQueryItem(QStringLiteral("user_id" ), value: userId()); |
719 | mxcUrl.setQuery(q); |
720 | return mxcUrl; |
721 | } |
722 | |
723 | MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId, |
724 | QSize requestedSize, |
725 | RunningPolicy policy) |
726 | { |
727 | auto idParts = splitMediaId(mediaId); |
728 | return callApi<MediaThumbnailJob>(runningPolicy: policy, jobArgs&: idParts.front(), jobArgs&: idParts.back(), |
729 | jobArgs&: requestedSize); |
730 | } |
731 | |
732 | MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, QSize requestedSize, |
733 | RunningPolicy policy) |
734 | { |
735 | return getThumbnail(mediaId: url.authority() + url.path(), requestedSize, policy); |
736 | } |
737 | |
738 | MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, int requestedWidth, |
739 | int requestedHeight, |
740 | RunningPolicy policy) |
741 | { |
742 | return getThumbnail(url, requestedSize: QSize(requestedWidth, requestedHeight), policy); |
743 | } |
744 | |
745 | UploadContentJob* |
746 | Connection::uploadContent(QIODevice* contentSource, const QString& filename, |
747 | const QString& overrideContentType) |
748 | { |
749 | Q_ASSERT(contentSource != nullptr); |
750 | auto contentType = overrideContentType; |
751 | if (contentType.isEmpty()) { |
752 | contentType = QMimeDatabase() |
753 | .mimeTypeForFileNameAndData(fileName: filename, device: contentSource) |
754 | .name(); |
755 | if (!contentSource->open(mode: QIODevice::ReadOnly)) { |
756 | qCWarning(MAIN) << "Couldn't open content source" << filename |
757 | << "for reading:" << contentSource->errorString(); |
758 | return nullptr; |
759 | } |
760 | } |
761 | return callApi<UploadContentJob>(jobArgs&: contentSource, jobArgs: filename, jobArgs&: contentType); |
762 | } |
763 | |
764 | UploadContentJob* Connection::uploadFile(const QString& fileName, |
765 | const QString& overrideContentType) |
766 | { |
767 | auto sourceFile = new QFile(fileName); |
768 | return uploadContent(contentSource: sourceFile, filename: QFileInfo(*sourceFile).fileName(), |
769 | overrideContentType); |
770 | } |
771 | |
772 | GetContentJob* Connection::getContent(const QString& mediaId) |
773 | { |
774 | auto idParts = splitMediaId(mediaId); |
775 | return callApi<GetContentJob>(jobArgs&: idParts.front(), jobArgs&: idParts.back()); |
776 | } |
777 | |
778 | GetContentJob* Connection::getContent(const QUrl& url) |
779 | { |
780 | return getContent(mediaId: url.authority() + url.path()); |
781 | } |
782 | |
783 | DownloadFileJob* Connection::downloadFile(const QUrl& url, |
784 | const QString& localFilename) |
785 | { |
786 | auto mediaId = url.authority() + url.path(); |
787 | auto idParts = splitMediaId(mediaId); |
788 | auto* job = |
789 | callApi<DownloadFileJob>(jobArgs&: idParts.front(), jobArgs&: idParts.back(), jobArgs: localFilename); |
790 | return job; |
791 | } |
792 | |
793 | #ifdef Quotient_E2EE_ENABLED |
794 | DownloadFileJob* Connection::downloadFile( |
795 | const QUrl& url, const EncryptedFileMetadata& fileMetadata, |
796 | const QString& localFilename) |
797 | { |
798 | auto mediaId = url.authority() + url.path(); |
799 | auto idParts = splitMediaId(mediaId); |
800 | return callApi<DownloadFileJob>(idParts.front(), idParts.back(), |
801 | fileMetadata, localFilename); |
802 | } |
803 | #endif |
804 | |
805 | CreateRoomJob* |
806 | Connection::createRoom(RoomVisibility visibility, const QString& alias, |
807 | const QString& name, const QString& topic, |
808 | QStringList invites, const QString& presetName, |
809 | const QString& roomVersion, bool isDirect, |
810 | const QVector<CreateRoomJob::StateEvent>& initialState, |
811 | const QVector<CreateRoomJob::Invite3pid>& invite3pids, |
812 | const QJsonObject& creationContent) |
813 | { |
814 | invites.removeOne(t: userId()); // The creator is by definition in the room |
815 | auto job = callApi<CreateRoomJob>(jobArgs: visibility == PublishRoom |
816 | ? QStringLiteral("public" ) |
817 | : QStringLiteral("private" ), |
818 | jobArgs: alias, jobArgs: name, jobArgs: topic, jobArgs&: invites, jobArgs: invite3pids, |
819 | jobArgs: roomVersion, jobArgs: creationContent, |
820 | jobArgs: initialState, jobArgs: presetName, jobArgs&: isDirect); |
821 | connect(sender: job, signal: &BaseJob::success, context: this, slot: [this, job, invites, isDirect] { |
822 | auto* room = provideRoom(roomId: job->roomId(), joinState: JoinState::Join); |
823 | if (!room) { |
824 | Q_ASSERT_X(room, "Connection::createRoom" , |
825 | "Failed to create a room" ); |
826 | return; |
827 | } |
828 | emit createdRoom(room); |
829 | if (isDirect) |
830 | for (const auto& i : invites) |
831 | addToDirectChats(room, user: user(uId: i)); |
832 | }); |
833 | return job; |
834 | } |
835 | |
836 | void Connection::requestDirectChat(const QString& userId) |
837 | { |
838 | doInDirectChat(userId, operation: [this](Room* r) { emit directChatAvailable(directChat: r); }); |
839 | } |
840 | |
841 | void Connection::requestDirectChat(User* u) |
842 | { |
843 | doInDirectChat(u, operation: [this](Room* r) { emit directChatAvailable(directChat: r); }); |
844 | } |
845 | |
846 | void Connection::doInDirectChat(const QString& userId, |
847 | const std::function<void(Room*)>& operation) |
848 | { |
849 | if (auto* u = user(uId: userId)) |
850 | doInDirectChat(u, operation); |
851 | else |
852 | qCCritical(MAIN) |
853 | << "Connection::doInDirectChat: Couldn't get a user object for" |
854 | << userId; |
855 | } |
856 | |
857 | void Connection::doInDirectChat(User* u, |
858 | const std::function<void(Room*)>& operation) |
859 | { |
860 | Q_ASSERT(u); |
861 | const auto& otherUserId = u->id(); |
862 | // There can be more than one DC; find the first valid (existing and |
863 | // not left), and delete inexistent (forgotten?) ones along the way. |
864 | DirectChatsMap removals; |
865 | for (auto it = d->directChats.constFind(key: u); |
866 | it != d->directChats.cend() && it.key() == u; ++it) { |
867 | const auto& roomId = *it; |
868 | if (auto r = room(roomId, states: JoinState::Join)) { |
869 | Q_ASSERT(r->id() == roomId); |
870 | // A direct chat with yourself should only involve yourself :) |
871 | if (otherUserId == userId() && r->totalMemberCount() > 1) |
872 | continue; |
873 | qCDebug(MAIN) << "Requested direct chat with" << otherUserId |
874 | << "is already available as" << r->id(); |
875 | operation(r); |
876 | return; |
877 | } |
878 | if (auto ir = invitation(roomId)) { |
879 | Q_ASSERT(ir->id() == roomId); |
880 | auto j = joinRoom(roomAlias: ir->id()); |
881 | connect(sender: j, signal: &BaseJob::success, context: this, |
882 | slot: [this, roomId, otherUserId, operation] { |
883 | qCDebug(MAIN) |
884 | << "Joined the already invited direct chat with" |
885 | << otherUserId << "as" << roomId; |
886 | operation(room(roomId, states: JoinState::Join)); |
887 | }); |
888 | return; |
889 | } |
890 | // Avoid reusing previously left chats but don't remove them |
891 | // from direct chat maps, either. |
892 | if (room(roomId, states: JoinState::Leave)) |
893 | continue; |
894 | |
895 | qCWarning(MAIN) << "Direct chat with" << otherUserId << "known as room" |
896 | << roomId << "is not valid and will be discarded" ; |
897 | // Postpone actual deletion until we finish iterating d->directChats. |
898 | removals.insert(key: it.key(), value: it.value()); |
899 | // Add to the list of updates to send to the server upon the next sync. |
900 | d->dcLocalRemovals.insert(key: it.key(), value: it.value()); |
901 | } |
902 | if (!removals.isEmpty()) { |
903 | for (auto it = removals.cbegin(); it != removals.cend(); ++it) { |
904 | d->directChats.remove(key: it.key(), value: it.value()); |
905 | d->directChatUsers.remove(key: it.value(), |
906 | value: const_cast<User*>(it.key())); // FIXME |
907 | } |
908 | emit directChatsListChanged(additions: {}, removals); |
909 | } |
910 | |
911 | auto j = createDirectChat(userId: otherUserId); |
912 | connect(sender: j, signal: &BaseJob::success, context: this, slot: [this, j, otherUserId, operation] { |
913 | qCDebug(MAIN) << "Direct chat with" << otherUserId << "has been created as" |
914 | << j->roomId(); |
915 | operation(room(roomId: j->roomId(), states: JoinState::Join)); |
916 | }); |
917 | } |
918 | |
919 | CreateRoomJob* Connection::createDirectChat(const QString& userId, |
920 | const QString& topic, |
921 | const QString& name) |
922 | { |
923 | return createRoom(visibility: UnpublishRoom, alias: {}, name, topic, invites: { userId }, |
924 | QStringLiteral("trusted_private_chat" ), roomVersion: {}, isDirect: true); |
925 | } |
926 | |
927 | ForgetRoomJob* Connection::forgetRoom(const QString& id) |
928 | { |
929 | // To forget is hard :) First we should ensure the local user is not |
930 | // in the room (by leaving it, if necessary); once it's done, the /forget |
931 | // endpoint can be called; and once this is through, the local Room object |
932 | // (if any existed) is deleted. At the same time, we still have to |
933 | // (basically immediately) return a pointer to ForgetRoomJob. Therefore |
934 | // a ForgetRoomJob is created in advance and can be returned in a probably |
935 | // not-yet-started state (it will start once /leave completes). |
936 | auto forgetJob = new ForgetRoomJob(id); |
937 | auto room = d->roomMap.value(key: { id, false }); |
938 | if (!room) |
939 | room = d->roomMap.value(key: { id, true }); |
940 | if (room && room->joinState() != JoinState::Leave) { |
941 | auto leaveJob = leaveRoom(room); |
942 | connect(sender: leaveJob, signal: &BaseJob::result, context: this, |
943 | slot: [this, leaveJob, forgetJob, room] { |
944 | if (leaveJob->error() == BaseJob::Success |
945 | || leaveJob->error() == BaseJob::NotFound) { |
946 | run(job: forgetJob); |
947 | // If the matching /sync response hasn't arrived yet, |
948 | // mark the room for explicit deletion |
949 | if (room->joinState() != JoinState::Leave) |
950 | d->roomIdsToForget.push_back(t: room->id()); |
951 | } else { |
952 | qCWarning(MAIN).nospace() |
953 | << "Error leaving room " << room->objectName() |
954 | << ": " << leaveJob->errorString(); |
955 | forgetJob->abandon(); |
956 | } |
957 | }); |
958 | } else |
959 | run(job: forgetJob); |
960 | connect(sender: forgetJob, signal: &BaseJob::result, context: this, slot: [this, id, forgetJob] { |
961 | // Leave room in case of success, or room not known by server |
962 | if (forgetJob->error() == BaseJob::Success |
963 | || forgetJob->error() == BaseJob::NotFound) |
964 | d->removeRoom(roomId: id); // Delete the room from roomMap |
965 | else |
966 | qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": " |
967 | << forgetJob->errorString(); |
968 | }); |
969 | return forgetJob; |
970 | } |
971 | |
972 | SendToDeviceJob* Connection::sendToDevices( |
973 | const QString& eventType, const UsersToDevicesToContent& contents) |
974 | { |
975 | return callApi<SendToDeviceJob>(runningPolicy: BackgroundRequest, jobArgs: eventType, |
976 | jobArgs: generateTxnId(), jobArgs: contents); |
977 | } |
978 | |
979 | SendMessageJob* Connection::sendMessage(const QString& roomId, |
980 | const RoomEvent& event) |
981 | { |
982 | const auto txnId = event.transactionId().isEmpty() ? generateTxnId() |
983 | : event.transactionId(); |
984 | return callApi<SendMessageJob>(jobArgs: roomId, jobArgs: event.matrixType(), jobArgs: txnId, |
985 | jobArgs: event.contentJson()); |
986 | } |
987 | |
988 | QUrl Connection::homeserver() const { return d->data->baseUrl(); } |
989 | |
990 | QString Connection::domain() const { return userId().section(asep: u':', astart: 1); } |
991 | |
992 | bool Connection::isUsable() const { return !loginFlows().isEmpty(); } |
993 | |
994 | QVector<GetLoginFlowsJob::LoginFlow> Connection::loginFlows() const |
995 | { |
996 | return d->loginFlows; |
997 | } |
998 | |
999 | bool Connection::supportsPasswordAuth() const |
1000 | { |
1001 | return d->loginFlows.contains(t: LoginFlows::Password); |
1002 | } |
1003 | |
1004 | bool Connection::supportsSso() const |
1005 | { |
1006 | return d->loginFlows.contains(t: LoginFlows::SSO); |
1007 | } |
1008 | |
1009 | Room* Connection::room(const QString& roomId, JoinStates states) const |
1010 | { |
1011 | Room* room = d->roomMap.value(key: { roomId, false }, defaultValue: nullptr); |
1012 | if (states.testFlag(flag: JoinState::Join) && room |
1013 | && room->joinState() == JoinState::Join) |
1014 | return room; |
1015 | |
1016 | if (states.testFlag(flag: JoinState::Invite)) |
1017 | if (Room* invRoom = invitation(roomId)) |
1018 | return invRoom; |
1019 | |
1020 | if (states.testFlag(flag: JoinState::Leave) && room |
1021 | && room->joinState() == JoinState::Leave) |
1022 | return room; |
1023 | |
1024 | return nullptr; |
1025 | } |
1026 | |
1027 | Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const |
1028 | { |
1029 | const auto id = d->roomAliasMap.value(key: roomAlias); |
1030 | if (!id.isEmpty()) |
1031 | return room(roomId: id, states); |
1032 | |
1033 | qCWarning(MAIN) << "Room for alias" << roomAlias |
1034 | << "is not found under account" << userId(); |
1035 | return nullptr; |
1036 | } |
1037 | |
1038 | bool Connection::roomSucceeds(const QString& maybePredecessorId, |
1039 | const QString& maybeSuccessorId) const |
1040 | { |
1041 | static constexpr auto AnyJoinStateMask = JoinState::Invite | JoinState::Join |
1042 | | JoinState::Knock |
1043 | | JoinState::Leave; |
1044 | |
1045 | for (auto r = room(roomId: maybePredecessorId, states: AnyJoinStateMask); r != nullptr;) { |
1046 | const auto& currentSuccId = r->successorId(); // Search forward |
1047 | if (currentSuccId.isEmpty()) |
1048 | break; |
1049 | if (currentSuccId == maybeSuccessorId) |
1050 | return true; |
1051 | r = room(roomId: currentSuccId, states: AnyJoinStateMask); |
1052 | } |
1053 | for (auto r = room(roomId: maybeSuccessorId, states: AnyJoinStateMask); r != nullptr;) { |
1054 | const auto& currentPredId = r->predecessorId(); // Search backward |
1055 | if (currentPredId.isEmpty()) |
1056 | break; |
1057 | if (currentPredId == maybePredecessorId) |
1058 | return true; |
1059 | r = room(roomId: currentPredId, states: AnyJoinStateMask); |
1060 | } |
1061 | return false; // Can't ascertain succession |
1062 | } |
1063 | |
1064 | void Connection::updateRoomAliases(const QString& roomId, |
1065 | const QStringList& previousRoomAliases, |
1066 | const QStringList& roomAliases) |
1067 | { |
1068 | for (const auto& a : previousRoomAliases) |
1069 | if (d->roomAliasMap.remove(key: a) == 0) |
1070 | qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)" ; |
1071 | |
1072 | for (const auto& a : roomAliases) { |
1073 | auto& mappedId = d->roomAliasMap[a]; |
1074 | if (!mappedId.isEmpty()) { |
1075 | if (mappedId == roomId) |
1076 | qCDebug(MAIN) |
1077 | << "Alias" << a << "is already mapped to" << roomId; |
1078 | else if (roomSucceeds(maybePredecessorId: roomId, maybeSuccessorId: mappedId)) { |
1079 | qCDebug(MAIN) << "Not remapping alias" << a << "from" |
1080 | << mappedId << "to predecessor" << roomId; |
1081 | continue; |
1082 | } else if (roomSucceeds(maybePredecessorId: mappedId, maybeSuccessorId: roomId)) |
1083 | qCDebug(MAIN) << "Remapping alias" << a << "from" << mappedId |
1084 | << "to successor" << roomId; |
1085 | else |
1086 | qCWarning(MAIN) << "Alias" << a << "will be force-remapped from" |
1087 | << mappedId << "to" << roomId; |
1088 | } |
1089 | mappedId = roomId; |
1090 | } |
1091 | } |
1092 | |
1093 | Room* Connection::invitation(const QString& roomId) const |
1094 | { |
1095 | return d->roomMap.value(key: { roomId, true }, defaultValue: nullptr); |
1096 | } |
1097 | |
1098 | User* Connection::user(const QString& uId) |
1099 | { |
1100 | if (uId.isEmpty()) |
1101 | return nullptr; |
1102 | if (const auto v = d->userMap.value(key: uId, defaultValue: nullptr)) |
1103 | return v; |
1104 | // Before creating a user object, check that the user id is well-formed |
1105 | // (it's faster to just do a lookup above before validation) |
1106 | if (!uId.startsWith(c: u'@') || serverPart(mxId: uId).isEmpty()) { |
1107 | qCCritical(MAIN) << "Malformed userId:" << uId; |
1108 | return nullptr; |
1109 | } |
1110 | auto* user = userFactory()(this, uId); |
1111 | d->userMap.insert(key: uId, value: user); |
1112 | emit newUser(user); |
1113 | return user; |
1114 | } |
1115 | |
1116 | const User* Connection::user() const |
1117 | { |
1118 | return d->userMap.value(key: userId(), defaultValue: nullptr); |
1119 | } |
1120 | |
1121 | User* Connection::user() { return user(uId: userId()); } |
1122 | |
1123 | QString Connection::userId() const { return d->data->userId(); } |
1124 | |
1125 | QString Connection::deviceId() const { return d->data->deviceId(); } |
1126 | |
1127 | QByteArray Connection::accessToken() const |
1128 | { |
1129 | // The logout job needs access token to do its job; so the token is |
1130 | // kept inside d->data but no more exposed to the outside world. |
1131 | return isJobPending(job: d->logoutJob) ? QByteArray() : d->data->accessToken(); |
1132 | } |
1133 | |
1134 | bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } |
1135 | |
1136 | #ifdef Quotient_E2EE_ENABLED |
1137 | QOlmAccount* Connection::olmAccount() const |
1138 | { |
1139 | return d->encryptionData ? &d->encryptionData->olmAccount : nullptr; |
1140 | } |
1141 | #endif // Quotient_E2EE_ENABLED |
1142 | |
1143 | SyncJob* Connection::syncJob() const { return d->syncJob; } |
1144 | |
1145 | int Connection::millisToReconnect() const |
1146 | { |
1147 | return d->syncJob ? d->syncJob->millisToRetry() : 0; |
1148 | } |
1149 | |
1150 | QVector<Room*> Connection::allRooms() const |
1151 | { |
1152 | QVector<Room*> result; |
1153 | result.resize(size: d->roomMap.size()); |
1154 | std::copy(first: d->roomMap.cbegin(), last: d->roomMap.cend(), result: result.begin()); |
1155 | return result; |
1156 | } |
1157 | |
1158 | QVector<Room*> Connection::rooms(JoinStates joinStates) const |
1159 | { |
1160 | QVector<Room*> result; |
1161 | for (auto* r: qAsConst(t&: d->roomMap)) |
1162 | if (joinStates.testFlag(flag: r->joinState())) |
1163 | result.push_back(t: r); |
1164 | return result; |
1165 | } |
1166 | |
1167 | int Connection::roomsCount(JoinStates joinStates) const |
1168 | { |
1169 | // Using int to maintain compatibility with QML |
1170 | // (consider also that QHash<>::size() returns int anyway). |
1171 | return int(std::count_if(first: d->roomMap.cbegin(), last: d->roomMap.cend(), |
1172 | pred: [joinStates](Room* r) { |
1173 | return joinStates.testFlag(flag: r->joinState()); |
1174 | })); |
1175 | } |
1176 | |
1177 | bool Connection::hasAccountData(const QString& type) const |
1178 | { |
1179 | return d->accountData.find(x: type) != d->accountData.cend(); |
1180 | } |
1181 | |
1182 | const EventPtr& Connection::accountData(const QString& type) const |
1183 | { |
1184 | static EventPtr NoEventPtr {}; |
1185 | auto it = d->accountData.find(x: type); |
1186 | return it == d->accountData.end() ? NoEventPtr : it->second; |
1187 | } |
1188 | |
1189 | QJsonObject Connection::accountDataJson(const QString& type) const |
1190 | { |
1191 | const auto& eventPtr = accountData(type); |
1192 | return eventPtr ? eventPtr->contentJson() : QJsonObject(); |
1193 | } |
1194 | |
1195 | void Connection::setAccountData(EventPtr&& event) |
1196 | { |
1197 | d->packAndSendAccountData(event: std::move(event)); |
1198 | } |
1199 | |
1200 | void Connection::setAccountData(const QString& type, const QJsonObject& content) |
1201 | { |
1202 | d->packAndSendAccountData(event: loadEvent<Event>(matrixType: type, otherBasicJsonParams: content)); |
1203 | } |
1204 | |
1205 | QHash<QString, QVector<Room*>> Connection::tagsToRooms() const |
1206 | { |
1207 | QHash<QString, QVector<Room*>> result; |
1208 | for (auto* r : qAsConst(t&: d->roomMap)) { |
1209 | const auto& tagNames = r->tagNames(); |
1210 | for (const auto& tagName : tagNames) |
1211 | result[tagName].push_back(t: r); |
1212 | } |
1213 | for (auto it = result.begin(); it != result.end(); ++it) |
1214 | std::sort(first: it->begin(), last: it->end(), comp: [t = it.key()](Room* r1, Room* r2) { |
1215 | return r1->tags().value(key: t) < r2->tags().value(key: t); |
1216 | }); |
1217 | return result; |
1218 | } |
1219 | |
1220 | QStringList Connection::tagNames() const |
1221 | { |
1222 | QStringList tags({ FavouriteTag }); |
1223 | for (auto* r : qAsConst(t&: d->roomMap)) { |
1224 | const auto& tagNames = r->tagNames(); |
1225 | for (const auto& tag : tagNames) |
1226 | if (tag != LowPriorityTag && !tags.contains(str: tag)) |
1227 | tags.push_back(t: tag); |
1228 | } |
1229 | tags.push_back(t: LowPriorityTag); |
1230 | return tags; |
1231 | } |
1232 | |
1233 | QVector<Room*> Connection::roomsWithTag(const QString& tagName) const |
1234 | { |
1235 | QVector<Room*> rooms; |
1236 | std::copy_if(first: d->roomMap.cbegin(), last: d->roomMap.cend(), |
1237 | result: std::back_inserter(x&: rooms), |
1238 | pred: [&tagName](Room* r) { return r->tags().contains(key: tagName); }); |
1239 | return rooms; |
1240 | } |
1241 | |
1242 | DirectChatsMap Connection::directChats() const |
1243 | { |
1244 | return d->directChats; |
1245 | } |
1246 | |
1247 | // Removes room with given id from roomMap |
1248 | void Connection::Private::removeRoom(const QString& roomId) |
1249 | { |
1250 | for (auto f : { false, true }) |
1251 | if (auto r = roomMap.take(key: { roomId, f })) { |
1252 | qCDebug(MAIN) << "Room" << r->objectName() << "in state" << terse |
1253 | << r->joinState() << "will be deleted" ; |
1254 | emit r->beforeDestruction(r); |
1255 | r->deleteLater(); |
1256 | } |
1257 | } |
1258 | |
1259 | void Connection::addToDirectChats(const Room* room, User* user) |
1260 | { |
1261 | Q_ASSERT(room != nullptr && user != nullptr); |
1262 | if (d->directChats.contains(key: user, value: room->id())) |
1263 | return; |
1264 | Q_ASSERT(!d->directChatUsers.contains(room->id(), user)); |
1265 | d->directChats.insert(key: user, value: room->id()); |
1266 | d->directChatUsers.insert(key: room->id(), value: user); |
1267 | d->dcLocalAdditions.insert(key: user, value: room->id()); |
1268 | emit directChatsListChanged(additions: { { user, room->id() } }, removals: {}); |
1269 | } |
1270 | |
1271 | void Connection::removeFromDirectChats(const QString& roomId, User* user) |
1272 | { |
1273 | Q_ASSERT(!roomId.isEmpty()); |
1274 | if ((user != nullptr && !d->directChats.contains(key: user, value: roomId)) |
1275 | || d->directChats.key(value: roomId) == nullptr) |
1276 | return; |
1277 | |
1278 | DirectChatsMap removals; |
1279 | if (user != nullptr) { |
1280 | d->directChats.remove(key: user, value: roomId); |
1281 | d->directChatUsers.remove(key: roomId, value: user); |
1282 | removals.insert(key: user, value: roomId); |
1283 | d->dcLocalRemovals.insert(key: user, value: roomId); |
1284 | } else { |
1285 | removals = remove_if(hashMap&: d->directChats, |
1286 | pred: [&roomId](auto it) { return it.value() == roomId; }); |
1287 | d->directChatUsers.remove(key: roomId); |
1288 | d->dcLocalRemovals += removals; |
1289 | } |
1290 | emit directChatsListChanged(additions: {}, removals); |
1291 | } |
1292 | |
1293 | bool Connection::isDirectChat(const QString& roomId) const |
1294 | { |
1295 | return d->directChatUsers.contains(key: roomId); |
1296 | } |
1297 | |
1298 | QList<User*> Connection::directChatUsers(const Room* room) const |
1299 | { |
1300 | Q_ASSERT(room != nullptr); |
1301 | return d->directChatUsers.values(key: room->id()); |
1302 | } |
1303 | |
1304 | bool Connection::isIgnored(const QString& userId) const |
1305 | { |
1306 | return ignoredUsers().contains(value: userId); |
1307 | } |
1308 | |
1309 | bool Connection::isIgnored(const User* user) const |
1310 | { |
1311 | Q_ASSERT(user != nullptr); |
1312 | return isIgnored(userId: user->id()); |
1313 | } |
1314 | |
1315 | IgnoredUsersList Connection::ignoredUsers() const |
1316 | { |
1317 | const auto* event = accountData<IgnoredUsersEvent>(); |
1318 | return event ? event->ignoredUsers() : IgnoredUsersList(); |
1319 | } |
1320 | |
1321 | void Connection::addToIgnoredUsers(const User* user) |
1322 | { |
1323 | Q_ASSERT(user != nullptr); |
1324 | |
1325 | auto ignoreList = ignoredUsers(); |
1326 | if (!ignoreList.contains(value: user->id())) { |
1327 | ignoreList.insert(value: user->id()); |
1328 | d->packAndSendAccountData<IgnoredUsersEvent>(content&: ignoreList); |
1329 | emit ignoredUsersListChanged(additions: { { user->id() } }, removals: {}); |
1330 | } |
1331 | } |
1332 | |
1333 | void Connection::removeFromIgnoredUsers(const User* user) |
1334 | { |
1335 | Q_ASSERT(user != nullptr); |
1336 | |
1337 | auto ignoreList = ignoredUsers(); |
1338 | if (ignoreList.remove(value: user->id()) != 0) { |
1339 | d->packAndSendAccountData<IgnoredUsersEvent>(content&: ignoreList); |
1340 | emit ignoredUsersListChanged(additions: {}, removals: { { user->id() } }); |
1341 | } |
1342 | } |
1343 | |
1344 | QMap<QString, User*> Connection::users() const { return d->userMap; } |
1345 | |
1346 | const ConnectionData* Connection::connectionData() const |
1347 | { |
1348 | return d->data.get(); |
1349 | } |
1350 | |
1351 | Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState) |
1352 | { |
1353 | // TODO: This whole function is a strong case for a RoomManager class. |
1354 | Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id" ); |
1355 | |
1356 | // If joinState is empty, all joinState == comparisons below are false. |
1357 | const std::pair roomKey { id, joinState == JoinState::Invite }; |
1358 | auto* room = d->roomMap.value(key: roomKey, defaultValue: nullptr); |
1359 | if (room) { |
1360 | // Leave is a special case because in transition (5a) (see the .h file) |
1361 | // joinState == room->joinState but we still have to preempt the Invite |
1362 | // and emit a signal. For Invite and Join, there's no such problem. |
1363 | if (room->joinState() == joinState && joinState != JoinState::Leave) |
1364 | return room; |
1365 | } else if (!joinState) { |
1366 | // No Join and Leave, maybe Invite? |
1367 | room = d->roomMap.value(key: { id, true }, defaultValue: nullptr); |
1368 | if (room) |
1369 | return room; |
1370 | // No Invite either, setup a new room object in Join state |
1371 | joinState = JoinState::Join; |
1372 | } |
1373 | |
1374 | if (!room) { |
1375 | Q_ASSERT(joinState.has_value()); |
1376 | room = roomFactory()(this, id, *joinState); |
1377 | if (!room) { |
1378 | qCCritical(MAIN) << "Failed to create a room" << id; |
1379 | return nullptr; |
1380 | } |
1381 | d->roomMap.insert(key: roomKey, value: room); |
1382 | connect(sender: room, signal: &Room::beforeDestruction, context: this, |
1383 | slot: &Connection::aboutToDeleteRoom); |
1384 | connect(sender: room, signal: &Room::baseStateLoaded, context: this, slot: [this, room] { |
1385 | emit loadedRoomState(room); |
1386 | if (d->capabilities.roomVersions) |
1387 | room->checkVersion(); |
1388 | // Otherwise, the version will be checked in reloadCapabilities() |
1389 | }); |
1390 | emit newRoom(room); |
1391 | } |
1392 | if (!joinState) |
1393 | return room; |
1394 | |
1395 | if (*joinState == JoinState::Invite) { |
1396 | // prev is either Leave or nullptr |
1397 | auto* prev = d->roomMap.value(key: { id, false }, defaultValue: nullptr); |
1398 | emit invitedRoom(room, prev); |
1399 | } else { |
1400 | room->setJoinState(*joinState); |
1401 | // Preempt the Invite room (if any) with a room in Join/Leave state. |
1402 | auto* prevInvite = d->roomMap.take(key: { id, true }); |
1403 | if (*joinState == JoinState::Join) |
1404 | emit joinedRoom(room, prev: prevInvite); |
1405 | else if (*joinState == JoinState::Leave) |
1406 | emit leftRoom(room, prev: prevInvite); |
1407 | if (prevInvite) { |
1408 | const auto dcUsers = prevInvite->directChatUsers(); |
1409 | for (auto* u : dcUsers) |
1410 | addToDirectChats(room, user: u); |
1411 | qCDebug(MAIN) << "Deleting Invite state for room" |
1412 | << prevInvite->id(); |
1413 | emit prevInvite->beforeDestruction(prevInvite); |
1414 | prevInvite->deleteLater(); |
1415 | } |
1416 | } |
1417 | |
1418 | return room; |
1419 | } |
1420 | |
1421 | #ifdef Quotient_E2EE_ENABLED |
1422 | void Connection::setEncryptionDefault(bool useByDefault) |
1423 | { |
1424 | Private::encryptionDefault = useByDefault; |
1425 | } |
1426 | #endif |
1427 | |
1428 | void Connection::setRoomFactory(room_factory_t f) |
1429 | { |
1430 | _roomFactory = std::move(f); |
1431 | } |
1432 | |
1433 | void Connection::setUserFactory(user_factory_t f) |
1434 | { |
1435 | _userFactory = std::move(f); |
1436 | } |
1437 | |
1438 | room_factory_t Connection::roomFactory() { return _roomFactory; } |
1439 | |
1440 | user_factory_t Connection::userFactory() { return _userFactory; } |
1441 | |
1442 | room_factory_t Connection::_roomFactory = defaultRoomFactory<>; |
1443 | user_factory_t Connection::_userFactory = defaultUserFactory<>; |
1444 | |
1445 | QString Connection::generateTxnId() const |
1446 | { |
1447 | return d->data->generateTxnId(); |
1448 | } |
1449 | |
1450 | void Connection::setHomeserver(const QUrl& url) |
1451 | { |
1452 | if (isJobPending(job: d->resolverJob)) |
1453 | d->resolverJob->abandon(); |
1454 | if (isJobPending(job: d->loginFlowsJob)) |
1455 | d->loginFlowsJob->abandon(); |
1456 | d->loginFlows.clear(); |
1457 | |
1458 | if (homeserver() != url) { |
1459 | d->data->setBaseUrl(url); |
1460 | emit homeserverChanged(baseUrl: homeserver()); |
1461 | } |
1462 | |
1463 | // Whenever a homeserver is updated, retrieve available login flows from it |
1464 | d->loginFlowsJob = callApi<GetLoginFlowsJob>(runningPolicy: BackgroundRequest); |
1465 | connect(sender: d->loginFlowsJob, signal: &BaseJob::result, context: this, slot: [this] { |
1466 | if (d->loginFlowsJob->status().good()) |
1467 | d->loginFlows = d->loginFlowsJob->flows(); |
1468 | else |
1469 | d->loginFlows.clear(); |
1470 | emit loginFlowsChanged(); |
1471 | }); |
1472 | } |
1473 | |
1474 | void Connection::saveRoomState(Room* r) const |
1475 | { |
1476 | Q_ASSERT(r); |
1477 | if (!d->cacheState) |
1478 | return; |
1479 | |
1480 | QFile outRoomFile { stateCacheDir().filePath( |
1481 | fileName: SyncData::fileNameForRoom(roomId: r->id())) }; |
1482 | if (outRoomFile.open(flags: QFile::WriteOnly)) { |
1483 | const auto data = |
1484 | d->cacheToBinary |
1485 | ? QCborValue::fromJsonValue(v: r->toJson()).toCbor() |
1486 | : QJsonDocument(r->toJson()).toJson(format: QJsonDocument::Compact); |
1487 | outRoomFile.write(data: data.data(), len: data.size()); |
1488 | qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); |
1489 | } else { |
1490 | qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() << ":" |
1491 | << outRoomFile.errorString(); |
1492 | } |
1493 | } |
1494 | |
1495 | void Connection::saveState() const |
1496 | { |
1497 | if (!d->cacheState) |
1498 | return; |
1499 | |
1500 | QElapsedTimer et; |
1501 | et.start(); |
1502 | |
1503 | QFile outFile { d->topLevelStatePath() }; |
1504 | if (!outFile.open(flags: QFile::WriteOnly)) { |
1505 | qCWarning(MAIN) << "Error opening" << outFile.fileName() << ":" |
1506 | << outFile.errorString(); |
1507 | qCWarning(MAIN) << "Caching the rooms state disabled" ; |
1508 | d->cacheState = false; |
1509 | return; |
1510 | } |
1511 | |
1512 | QJsonObject rootObj { |
1513 | { QStringLiteral("cache_version" ), |
1514 | QJsonObject { |
1515 | { QStringLiteral("major" ), SyncData::cacheVersion().first }, |
1516 | { QStringLiteral("minor" ), SyncData::cacheVersion().second } } } |
1517 | }; |
1518 | { |
1519 | QJsonObject roomsJson; |
1520 | QJsonObject inviteRoomsJson; |
1521 | for (const auto* r: qAsConst(t&: d->roomMap)) { |
1522 | if (r->joinState() == JoinState::Leave) |
1523 | continue; |
1524 | (r->joinState() == JoinState::Invite ? inviteRoomsJson : roomsJson) |
1525 | .insert(key: r->id(), value: QJsonValue::Null); |
1526 | } |
1527 | |
1528 | QJsonObject roomObj; |
1529 | if (!roomsJson.isEmpty()) |
1530 | roomObj.insert(QStringLiteral("join" ), value: roomsJson); |
1531 | if (!inviteRoomsJson.isEmpty()) |
1532 | roomObj.insert(QStringLiteral("invite" ), value: inviteRoomsJson); |
1533 | |
1534 | rootObj.insert(QStringLiteral("next_batch" ), value: d->data->lastEvent()); |
1535 | rootObj.insert(QStringLiteral("rooms" ), value: roomObj); |
1536 | } |
1537 | { |
1538 | QJsonArray accountDataEvents { |
1539 | Event::basicJson(matrixType: DirectChatEvent::TypeId, content: toJson(directChats: d->directChats)) |
1540 | }; |
1541 | for (const auto& e : d->accountData) |
1542 | accountDataEvents.append( |
1543 | value: Event::basicJson(matrixType: e.first, content: e.second->contentJson())); |
1544 | |
1545 | rootObj.insert(QStringLiteral("account_data" ), |
1546 | value: QJsonObject { |
1547 | { QStringLiteral("events" ), accountDataEvents } }); |
1548 | } |
1549 | |
1550 | #ifdef Quotient_E2EE_ENABLED |
1551 | if (d->encryptionData) { |
1552 | QJsonObject keysJson = toJson(d->encryptionData->oneTimeKeysCount); |
1553 | rootObj.insert(QStringLiteral("device_one_time_keys_count" ), keysJson); |
1554 | } |
1555 | #endif |
1556 | |
1557 | const auto data = |
1558 | d->cacheToBinary ? QCborValue::fromJsonValue(v: rootObj).toCbor() |
1559 | : QJsonDocument(rootObj).toJson(format: QJsonDocument::Compact); |
1560 | qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; |
1561 | |
1562 | outFile.write(data: data.data(), len: data.size()); |
1563 | qCDebug(MAIN) << "State cache saved to" << outFile.fileName(); |
1564 | } |
1565 | |
1566 | void Connection::loadState() |
1567 | { |
1568 | if (!d->cacheState) |
1569 | return; |
1570 | |
1571 | QElapsedTimer et; |
1572 | et.start(); |
1573 | |
1574 | SyncData sync { d->topLevelStatePath() }; |
1575 | if (sync.nextBatch().isEmpty()) // No token means no cache by definition |
1576 | return; |
1577 | |
1578 | if (!sync.unresolvedRooms().isEmpty()) { |
1579 | qCWarning(MAIN) << "State cache incomplete, discarding" ; |
1580 | return; |
1581 | } |
1582 | // TODO: to handle load failures, instead of the above block: |
1583 | // 1. Do initial sync on failed rooms without saving the nextBatch token |
1584 | // 2. Do the sync across all rooms as normal |
1585 | onSyncSuccess(data: std::move(sync), fromCache: true); |
1586 | qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; |
1587 | } |
1588 | |
1589 | QString Connection::stateCachePath() const |
1590 | { |
1591 | return stateCacheDir().path() % u'/'; |
1592 | } |
1593 | |
1594 | QDir Connection::stateCacheDir() const |
1595 | { |
1596 | auto safeUserId = userId(); |
1597 | safeUserId.replace(before: u':', after: u'_'); |
1598 | return cacheLocation(dirName: safeUserId); |
1599 | } |
1600 | |
1601 | bool Connection::cacheState() const { return d->cacheState; } |
1602 | |
1603 | void Connection::setCacheState(bool newValue) |
1604 | { |
1605 | if (d->cacheState != newValue) { |
1606 | d->cacheState = newValue; |
1607 | emit cacheStateChanged(); |
1608 | } |
1609 | } |
1610 | |
1611 | bool Connection::lazyLoading() const { return d->lazyLoading; } |
1612 | |
1613 | void Connection::setLazyLoading(bool newValue) |
1614 | { |
1615 | if (d->lazyLoading != newValue) { |
1616 | d->lazyLoading = newValue; |
1617 | emit lazyLoadingChanged(); |
1618 | } |
1619 | } |
1620 | |
1621 | BaseJob* Connection::run(BaseJob* job, RunningPolicy runningPolicy) |
1622 | { |
1623 | // Reparent to protect from #397, #398 and to prevent BaseJob* from being |
1624 | // garbage-collected if made by or returned to QML/JavaScript. |
1625 | job->setParent(this); |
1626 | connect(sender: job, signal: &BaseJob::failure, context: this, slot: &Connection::requestFailed); |
1627 | job->initiate(connData: d->data.get(), inBackground: runningPolicy & BackgroundRequest); |
1628 | return job; |
1629 | } |
1630 | |
1631 | void Connection::getTurnServers() |
1632 | { |
1633 | auto job = callApi<GetTurnServerJob>(); |
1634 | connect(sender: job, signal: &GetTurnServerJob::success, context: this, |
1635 | slot: [this,job] { emit turnServersChanged(servers: job->data()); }); |
1636 | } |
1637 | |
1638 | const QString Connection::SupportedRoomVersion::StableTag = |
1639 | QStringLiteral("stable" ); |
1640 | |
1641 | QString Connection::defaultRoomVersion() const |
1642 | { |
1643 | return d->capabilities.roomVersions |
1644 | ? d->capabilities.roomVersions->defaultVersion |
1645 | : QString(); |
1646 | } |
1647 | |
1648 | QStringList Connection::stableRoomVersions() const |
1649 | { |
1650 | QStringList l; |
1651 | if (d->capabilities.roomVersions) { |
1652 | const auto& allVersions = d->capabilities.roomVersions->available; |
1653 | for (auto it = allVersions.begin(); it != allVersions.end(); ++it) |
1654 | if (it.value() == SupportedRoomVersion::StableTag) |
1655 | l.push_back(t: it.key()); |
1656 | } |
1657 | return l; |
1658 | } |
1659 | |
1660 | bool Connection::canChangePassword() const |
1661 | { |
1662 | // By default assume we can |
1663 | return d->capabilities.changePassword |
1664 | ? d->capabilities.changePassword->enabled |
1665 | : true; |
1666 | } |
1667 | |
1668 | bool Connection::encryptionEnabled() const |
1669 | { |
1670 | return d->useEncryption; |
1671 | } |
1672 | |
1673 | void Connection::enableEncryption(bool enable) |
1674 | { |
1675 | if (enable == d->useEncryption) |
1676 | return; |
1677 | |
1678 | if (isLoggedIn()) { |
1679 | qWarning(catFunc: E2EE) << "It's only possible to enable/disable E2EE " |
1680 | "before logging in; the account" |
1681 | << objectName() |
1682 | << "is already logged in, the E2EE state will remain" |
1683 | << d->useEncryption; |
1684 | return; |
1685 | } |
1686 | |
1687 | #ifdef Quotient_E2EE_ENABLED |
1688 | d->useEncryption = enable; |
1689 | emit encryptionChanged(enable); |
1690 | #else |
1691 | Q_UNUSED(enable) |
1692 | qWarning(catFunc: E2EE) << "The library is compiled without E2EE support, " |
1693 | "enabling encryption has no effect" ; |
1694 | #endif |
1695 | } |
1696 | |
1697 | inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, |
1698 | const Connection::SupportedRoomVersion& v2) |
1699 | { |
1700 | bool ok1 = false, ok2 = false; |
1701 | const auto vNum1 = v1.id.toFloat(ok: &ok1); |
1702 | const auto vNum2 = v2.id.toFloat(ok: &ok2); |
1703 | return ok1 && ok2 ? vNum1 < vNum2 : v1.id < v2.id; |
1704 | } |
1705 | |
1706 | QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() const |
1707 | { |
1708 | QVector<SupportedRoomVersion> result; |
1709 | if (d->capabilities.roomVersions) { |
1710 | const auto& allVersions = d->capabilities.roomVersions->available; |
1711 | result.reserve(asize: allVersions.size()); |
1712 | for (auto it = allVersions.begin(); it != allVersions.end(); ++it) |
1713 | result.push_back(t: { .id: it.key(), .status: it.value() }); |
1714 | // Put stable versions over unstable; within each group, |
1715 | // sort numeric versions as numbers, the rest as strings. |
1716 | const auto mid = |
1717 | std::partition(first: result.begin(), last: result.end(), |
1718 | pred: std::mem_fn(pm: &SupportedRoomVersion::isStable)); |
1719 | std::sort(first: result.begin(), last: mid, comp: roomVersionLess); |
1720 | std::sort(first: mid, last: result.end(), comp: roomVersionLess); |
1721 | } |
1722 | return result; |
1723 | } |
1724 | |
1725 | #ifdef Quotient_E2EE_ENABLED |
1726 | bool Connection::isQueryingKeys() const |
1727 | { |
1728 | return d->encryptionData |
1729 | && d->encryptionData->currentQueryKeysJob != nullptr; |
1730 | } |
1731 | |
1732 | void Connection::encryptionUpdate(const Room* room, const QList<User*>& invited) |
1733 | { |
1734 | if (d->encryptionData) |
1735 | d->encryptionData->encryptionUpdate(room->users() + invited); |
1736 | } |
1737 | |
1738 | QJsonObject Connection::decryptNotification(const QJsonObject& notification) |
1739 | { |
1740 | if (auto r = room(notification[RoomIdKey].toString())) |
1741 | if (auto event = |
1742 | loadEvent<EncryptedEvent>(notification["event"_ls ].toObject())) |
1743 | if (const auto decrypted = r->decryptMessage(*event)) |
1744 | return decrypted->fullJson(); |
1745 | return {}; |
1746 | } |
1747 | |
1748 | Database* Connection::database() const |
1749 | { |
1750 | return d->encryptionData ? &d->encryptionData->database : nullptr; |
1751 | } |
1752 | |
1753 | UnorderedMap<QByteArray, QOlmInboundGroupSession> |
1754 | Connection::loadRoomMegolmSessions(const Room* room) const |
1755 | { |
1756 | return database()->loadMegolmSessions(room->id()); |
1757 | } |
1758 | |
1759 | void Connection::saveMegolmSession(const Room* room, |
1760 | const QOlmInboundGroupSession& session) const |
1761 | { |
1762 | database()->saveMegolmSession(room->id(), session); |
1763 | } |
1764 | |
1765 | QStringList Connection::devicesForUser(const QString& userId) const |
1766 | { |
1767 | return d->encryptionData->deviceKeys.value(userId).keys(); |
1768 | } |
1769 | |
1770 | QString Connection::edKeyForUserDevice(const QString& userId, |
1771 | const QString& deviceId) const |
1772 | { |
1773 | return d->encryptionData->deviceKeys[userId][deviceId] |
1774 | .keys["ed25519:"_ls + deviceId]; |
1775 | } |
1776 | |
1777 | bool Connection::hasOlmSession(const QString& user, |
1778 | const QString& deviceId) const |
1779 | { |
1780 | return d->encryptionData && d->encryptionData->hasOlmSession(user, deviceId); |
1781 | } |
1782 | |
1783 | void Connection::sendSessionKeyToDevices( |
1784 | const QString& roomId, const QOlmOutboundGroupSession& outboundSession, |
1785 | const QMultiHash<QString, QString>& devices) |
1786 | { |
1787 | Q_ASSERT(d->encryptionData != nullptr); |
1788 | d->encryptionData->sendSessionKeyToDevices(roomId, outboundSession, devices); |
1789 | } |
1790 | |
1791 | Omittable<QOlmOutboundGroupSession> Connection::loadCurrentOutboundMegolmSession( |
1792 | const QString& roomId) const |
1793 | { |
1794 | const auto& db = database(); |
1795 | Q_ASSERT_X( |
1796 | db, __FUNCTION__, |
1797 | "Check encryptionData() or database() before calling this method" ); |
1798 | return db ? db->loadCurrentOutboundMegolmSession(roomId) : none; |
1799 | } |
1800 | |
1801 | void Connection::saveCurrentOutboundMegolmSession( |
1802 | const QString& roomId, const QOlmOutboundGroupSession& session) const |
1803 | { |
1804 | if (const auto& db = database()) |
1805 | db->saveCurrentOutboundMegolmSession(roomId, session); |
1806 | else |
1807 | Q_ASSERT_X( |
1808 | false, __FUNCTION__, |
1809 | "Check encryptionData() or database() before calling this method" ); |
1810 | } |
1811 | |
1812 | KeyVerificationSession* Connection::startKeyVerificationSession( |
1813 | const QString& userId, const QString& deviceId) |
1814 | { |
1815 | if (!d->encryptionData) { |
1816 | qWarning(E2EE) << "E2EE is switched off on" << objectName() |
1817 | << "- you can't start a verification session on it" ; |
1818 | return nullptr; |
1819 | } |
1820 | return d->encryptionData->setupKeyVerificationSession(userId, deviceId, |
1821 | this); |
1822 | } |
1823 | |
1824 | void Connection::sendToDevice(const QString& targetUserId, |
1825 | const QString& targetDeviceId, const Event& event, |
1826 | bool encrypted) |
1827 | { |
1828 | if (encrypted && !d->encryptionData) { |
1829 | qWarning(E2EE) << "E2EE is off for" << objectName() |
1830 | << "- no encrypted to-device message will be sent" ; |
1831 | return; |
1832 | } |
1833 | |
1834 | const auto contentJson = |
1835 | encrypted |
1836 | ? d->encryptionData->assembleEncryptedContent(event.fullJson(), |
1837 | targetUserId, |
1838 | targetDeviceId) |
1839 | : event.contentJson(); |
1840 | sendToDevices(encrypted ? EncryptedEvent::TypeId : event.matrixType(), |
1841 | { { targetUserId, { { targetDeviceId, contentJson } } } }); |
1842 | } |
1843 | |
1844 | bool Connection::isVerifiedSession(const QByteArray& megolmSessionId) const |
1845 | { |
1846 | auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls ); |
1847 | query.bindValue(":sessionId"_ls , megolmSessionId); |
1848 | database()->execute(query); |
1849 | if (!query.next()) { |
1850 | return false; |
1851 | } |
1852 | auto olmSessionId = query.value("olmSessionId"_ls ).toString(); |
1853 | query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls ); |
1854 | query.bindValue(":sessionId"_ls , olmSessionId.toLatin1()); |
1855 | database()->execute(query); |
1856 | if (!query.next()) { |
1857 | return false; |
1858 | } |
1859 | auto curveKey = query.value("senderKey"_ls ).toString(); |
1860 | query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls ); |
1861 | query.bindValue(":curveKey"_ls , curveKey); |
1862 | database()->execute(query); |
1863 | return query.next() && query.value("verified"_ls ).toBool(); |
1864 | } |
1865 | |
1866 | bool Connection::isVerifiedDevice(const QString& userId, const QString& deviceId) const |
1867 | { |
1868 | auto query = database()->prepareQuery("SELECT verified FROM tracked_devices WHERE deviceId=:deviceId AND matrixId=:matrixId;"_ls ); |
1869 | query.bindValue(":deviceId"_ls , deviceId); |
1870 | query.bindValue(":matrixId"_ls , userId); |
1871 | database()->execute(query); |
1872 | return query.next() && query.value("verified"_ls ).toBool(); |
1873 | } |
1874 | |
1875 | bool Connection::isKnownE2eeCapableDevice(const QString& userId, const QString& deviceId) const |
1876 | { |
1877 | auto query = database()->prepareQuery("SELECT verified FROM tracked_devices WHERE deviceId=:deviceId AND matrixId=:matrixId;"_ls ); |
1878 | query.bindValue(":deviceId"_ls , deviceId); |
1879 | query.bindValue(":matrixId"_ls , userId); |
1880 | database()->execute(query); |
1881 | return query.next(); |
1882 | } |
1883 | |
1884 | #endif |
1885 | |
1886 | Connection* Connection::makeMockConnection(const QString& mxId, |
1887 | bool enableEncryption) |
1888 | { |
1889 | auto* c = new Connection; |
1890 | c->enableEncryption(enable: enableEncryption); |
1891 | c->d->completeSetup(mxId, mock: true); |
1892 | return c; |
1893 | } |
1894 | |