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 Alexey Andreyev <aa13q@ya.ru> |
4 | // SPDX-License-Identifier: LGPL-2.1-or-later |
5 | |
6 | #pragma once |
7 | |
8 | #include "quotient_common.h" |
9 | #include "ssosession.h" |
10 | #include "util.h" |
11 | |
12 | #include "csapi/create_room.h" |
13 | #include "csapi/login.h" |
14 | |
15 | #include "events/accountdataevents.h" |
16 | |
17 | #include <QtCore/QDir> |
18 | #include <QtCore/QObject> |
19 | #include <QtCore/QSize> |
20 | #include <QtCore/QUrl> |
21 | |
22 | #include <functional> |
23 | |
24 | #ifdef Quotient_E2EE_ENABLED |
25 | #include "e2ee/e2ee_common.h" |
26 | #include "e2ee/qolmoutboundsession.h" |
27 | #include "keyverificationsession.h" |
28 | #include "events/keyverificationevent.h" |
29 | #endif |
30 | |
31 | Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) |
32 | |
33 | namespace Quotient { |
34 | |
35 | class Room; |
36 | class User; |
37 | class ConnectionData; |
38 | class RoomEvent; |
39 | |
40 | class SyncJob; |
41 | class SyncData; |
42 | class RoomMessagesJob; |
43 | class PostReceiptJob; |
44 | class ForgetRoomJob; |
45 | class MediaThumbnailJob; |
46 | class JoinRoomJob; |
47 | class UploadContentJob; |
48 | class GetContentJob; |
49 | class DownloadFileJob; |
50 | class SendToDeviceJob; |
51 | class SendMessageJob; |
52 | class LeaveRoomJob; |
53 | class Database; |
54 | struct EncryptedFileMetadata; |
55 | |
56 | class QOlmAccount; |
57 | class QOlmInboundGroupSession; |
58 | |
59 | using LoginFlow = GetLoginFlowsJob::LoginFlow; |
60 | |
61 | //! Predefined login flows |
62 | namespace LoginFlows { |
63 | inline const LoginFlow Password { .type: "m.login.password"_ls }; |
64 | inline const LoginFlow SSO { .type: "m.login.sso"_ls }; |
65 | inline const LoginFlow Token { .type: "m.login.token"_ls }; |
66 | } |
67 | |
68 | // To simplify comparisons of LoginFlows |
69 | |
70 | inline bool operator==(const LoginFlow& lhs, const LoginFlow& rhs) |
71 | { |
72 | return lhs.type == rhs.type; |
73 | } |
74 | |
75 | inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs) |
76 | { |
77 | return !(lhs == rhs); |
78 | } |
79 | |
80 | class Connection; |
81 | |
82 | using room_factory_t = |
83 | std::function<Room*(Connection*, const QString&, JoinState)>; |
84 | using user_factory_t = std::function<User*(Connection*, const QString&)>; |
85 | |
86 | //! \brief The default factory to create room objects |
87 | //! |
88 | //! Just a wrapper around operator new. |
89 | //! \sa Connection::setRoomFactory, Connection::setRoomType |
90 | template <typename T = Room> |
91 | auto defaultRoomFactory(Connection* c, const QString& id, JoinState js) |
92 | { |
93 | return new T(c, id, js); |
94 | } |
95 | |
96 | //! \brief The default factory to create user objects |
97 | //! |
98 | //! Just a wrapper around operator new. |
99 | //! \sa Connection::setUserFactory, Connection::setUserType |
100 | template <typename T = User> |
101 | auto defaultUserFactory(Connection* c, const QString& id) |
102 | { |
103 | return new T(id, c); |
104 | } |
105 | |
106 | // Room ids, rather than room pointers, are used in the direct chat |
107 | // map types because the library keeps Invite rooms separate from |
108 | // rooms in Join and Leave state; and direct chats in account data |
109 | // are stored with no regard to their state. |
110 | using DirectChatsMap = QMultiHash<const User*, QString>; |
111 | using DirectChatUsersMap = QMultiHash<QString, User*>; |
112 | using IgnoredUsersList = IgnoredUsersEvent::value_type; |
113 | |
114 | class QUOTIENT_API Connection : public QObject { |
115 | Q_OBJECT |
116 | |
117 | Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) |
118 | Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) |
119 | Q_PROPERTY(QString domain READ domain NOTIFY stateChanged STORED false) |
120 | Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) |
121 | Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) |
122 | Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY stateChanged STORED false) |
123 | Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) |
124 | Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) |
125 | Q_PROPERTY(QVector<GetLoginFlowsJob::LoginFlow> loginFlows READ loginFlows NOTIFY loginFlowsChanged) |
126 | Q_PROPERTY(bool isUsable READ isUsable NOTIFY loginFlowsChanged STORED false) |
127 | Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged STORED false) |
128 | Q_PROPERTY(bool supportsPasswordAuth READ supportsPasswordAuth NOTIFY loginFlowsChanged STORED false) |
129 | Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) |
130 | Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) |
131 | Q_PROPERTY(bool canChangePassword READ canChangePassword NOTIFY capabilitiesLoaded) |
132 | Q_PROPERTY(bool encryptionEnabled READ encryptionEnabled WRITE enableEncryption NOTIFY encryptionChanged) |
133 | |
134 | public: |
135 | using UsersToDevicesToContent = QHash<QString, QHash<QString, QJsonObject>>; |
136 | |
137 | enum RoomVisibility { |
138 | PublishRoom, |
139 | UnpublishRoom |
140 | }; // FIXME: Should go inside CreateRoomJob |
141 | |
142 | explicit Connection(QObject* parent = nullptr); |
143 | explicit Connection(const QUrl& server, QObject* parent = nullptr); |
144 | ~Connection() override; |
145 | |
146 | //! \brief Get all rooms known within this Connection |
147 | //! |
148 | //! This includes Invite, Join and Leave rooms, in no particular order. |
149 | //! \note Leave rooms will only show up in the list if they have been left |
150 | //! in the same running session. The library doesn't cache left rooms |
151 | //! between runs and it doesn't retrieve the full list of left rooms |
152 | //! from the server. |
153 | //! \sa rooms, room, roomsWithTag |
154 | Q_INVOKABLE QVector<Quotient::Room*> allRooms() const; |
155 | |
156 | //! \brief Get rooms that have either of the given join state(s) |
157 | //! |
158 | //! This method returns, in no particular order, rooms which join state |
159 | //! matches the mask passed in \p joinStates. |
160 | //! \note Similar to allRooms(), this won't retrieve the full list of |
161 | //! Leave rooms from the server. |
162 | //! \sa allRooms, room, roomsWithTag |
163 | Q_INVOKABLE QVector<Quotient::Room*> |
164 | rooms(Quotient::JoinStates joinStates) const; |
165 | |
166 | //! Get the total number of rooms in the given join state(s) |
167 | Q_INVOKABLE int roomsCount(Quotient::JoinStates joinStates) const; |
168 | |
169 | //! \brief Check whether the account has data of the given type |
170 | //! |
171 | //! Direct chats map is not supported by this method _yet_. |
172 | bool hasAccountData(const QString& type) const; |
173 | |
174 | //! \brief Get a generic account data event of the given type |
175 | //! |
176 | //! \return an account data event of the given type stored on the server, |
177 | //! or nullptr if there's none of that type. |
178 | //! \note Direct chats map cannot be retrieved using this method _yet_; |
179 | //! use directChats() instead. |
180 | const EventPtr& accountData(const QString& type) const; |
181 | |
182 | //! \brief Get an account data event of the given type |
183 | //! |
184 | //! \return the account data content for the given event type stored |
185 | //! on the server, or a default-constructed object if there's none |
186 | //! of that type. |
187 | //! \note Direct chats map cannot be retrieved using this method _yet_; |
188 | //! use directChats() instead. |
189 | template <EventClass EventT> |
190 | const EventT* accountData() const |
191 | { |
192 | return eventCast<EventT>(accountData(EventT::TypeId)); |
193 | } |
194 | |
195 | //! \brief Get account data as a JSON object |
196 | //! |
197 | //! This returns the content part of the account data event |
198 | //! of the given type. Direct chats map cannot be retrieved using |
199 | //! this method _yet_; use directChats() instead. |
200 | Q_INVOKABLE QJsonObject accountDataJson(const QString& type) const; |
201 | |
202 | //! Set a generic account data event of the given type |
203 | void setAccountData(EventPtr&& event); |
204 | |
205 | Q_INVOKABLE void setAccountData(const QString& type, |
206 | const QJsonObject& content); |
207 | |
208 | //! \brief Get all Invited and Joined rooms grouped by tag |
209 | //! \return a hashmap from tag name to a vector of room pointers, |
210 | //! sorted by their order in the tag - details are at |
211 | //! https://spec.matrix.org/v1.5/client-server-api/#room-tagging |
212 | QHash<QString, QVector<Room*>> tagsToRooms() const; |
213 | |
214 | //! Get all room tags known on this connection |
215 | QStringList tagNames() const; |
216 | |
217 | //! Get the list of rooms with the specified tag |
218 | QVector<Room*> roomsWithTag(const QString& tagName) const; |
219 | |
220 | //! \brief Mark the room as a direct chat with the user |
221 | //! |
222 | //! This function marks \p room as a direct chat with \p user. |
223 | //! Emits the signal synchronously, without waiting to complete |
224 | //! synchronisation with the server. |
225 | //! \sa directChatsListChanged |
226 | void addToDirectChats(const Room* room, User* user); |
227 | |
228 | //! \brief Unmark the room from direct chats |
229 | //! |
230 | //! This function removes the room id from direct chats either for |
231 | //! a specific \p user or for all users if \p user in nullptr. |
232 | //! The room id is used to allow removal of, e.g., ids of forgotten |
233 | //! rooms; a Room object need not exist. Emits the signal |
234 | //! immediately, without waiting to complete synchronisation with |
235 | //! the server. |
236 | //! \sa directChatsListChanged |
237 | void removeFromDirectChats(const QString& roomId, User* user = nullptr); |
238 | |
239 | //! Check whether the room id corresponds to a direct chat |
240 | bool isDirectChat(const QString& roomId) const; |
241 | |
242 | //! Get the whole map from users to direct chat rooms |
243 | DirectChatsMap directChats() const; |
244 | |
245 | //! \brief Retrieve the list of users the room is a direct chat with |
246 | //! \return The list of users for which this room is marked as |
247 | //! a direct chat; an empty list if the room is not a direct chat |
248 | QList<User*> directChatUsers(const Room* room) const; |
249 | |
250 | //! Check whether a particular user id is in the ignore list |
251 | Q_INVOKABLE bool isIgnored(const QString& userId) const; |
252 | |
253 | //! Check whether a particular user is in the ignore list |
254 | Q_INVOKABLE bool isIgnored(const Quotient::User* user) const; |
255 | |
256 | //! Get the whole list of ignored users |
257 | Q_INVOKABLE Quotient::IgnoredUsersList ignoredUsers() const; |
258 | |
259 | //! \brief Add the user to the ignore list |
260 | //! The change signal is emitted synchronously, without waiting |
261 | //! to complete synchronisation with the server. |
262 | //! |
263 | //! \sa ignoredUsersListChanged |
264 | Q_INVOKABLE void addToIgnoredUsers(const Quotient::User* user); |
265 | |
266 | //! \brief Remove the user from the ignore list |
267 | //! |
268 | //! Similar to adding, the change signal is emitted synchronously. |
269 | //! \sa ignoredUsersListChanged |
270 | Q_INVOKABLE void removeFromIgnoredUsers(const Quotient::User* user); |
271 | |
272 | //! Get the full list of users known to this account |
273 | QMap<QString, User*> users() const; |
274 | |
275 | //! Get the base URL of the homeserver to connect to |
276 | QUrl homeserver() const; |
277 | //! Get the domain name used for ids/aliases on the server |
278 | QString domain() const; |
279 | //! Check if the homeserver is known to be reachable and working |
280 | bool isUsable() const; |
281 | //! Get the list of supported login flows |
282 | QVector<GetLoginFlowsJob::LoginFlow> loginFlows() const; |
283 | //! Check whether the current homeserver supports password auth |
284 | bool supportsPasswordAuth() const; |
285 | //! Check whether the current homeserver supports SSO |
286 | bool supportsSso() const; |
287 | //! Find a room by its id and a mask of applicable states |
288 | Q_INVOKABLE Quotient::Room* room( |
289 | const QString& roomId, |
290 | Quotient::JoinStates states = JoinState::Invite | JoinState::Join) const; |
291 | //! Find a room by its alias and a mask of applicable states |
292 | Q_INVOKABLE Quotient::Room* roomByAlias( |
293 | const QString& roomAlias, |
294 | Quotient::JoinStates states = JoinState::Invite | JoinState::Join) const; |
295 | //! \brief Update the internal map of room aliases to IDs |
296 | //! |
297 | //! This is used to maintain the internal index of room aliases. |
298 | //! It does NOT change aliases on the server, |
299 | //! \sa Room::setLocalAliases |
300 | void updateRoomAliases(const QString& roomId, |
301 | const QStringList& previousRoomAliases, |
302 | const QStringList& roomAliases); |
303 | Q_INVOKABLE Quotient::Room* invitation(const QString& roomId) const; |
304 | Q_INVOKABLE Quotient::User* user(const QString& uId); |
305 | const User* user() const; |
306 | User* user(); |
307 | QString userId() const; |
308 | QString deviceId() const; |
309 | QByteArray accessToken() const; |
310 | bool isLoggedIn() const; |
311 | #ifdef Quotient_E2EE_ENABLED |
312 | QOlmAccount* olmAccount() const; |
313 | Database* database() const; |
314 | |
315 | UnorderedMap<QByteArray, QOlmInboundGroupSession> loadRoomMegolmSessions( |
316 | const Room* room) const; |
317 | void saveMegolmSession(const Room* room, |
318 | const QOlmInboundGroupSession& session) const; |
319 | [[deprecated("Use database()->loadCurrentOutboundMegolmSession()" )]] |
320 | Omittable<QOlmOutboundGroupSession> loadCurrentOutboundMegolmSession( |
321 | const QString& roomId) const; |
322 | [[deprecated("Use database()->saveCurrentOutboundMegolmSession()" )]] |
323 | void saveCurrentOutboundMegolmSession( |
324 | const QString& roomId, const QOlmOutboundGroupSession& session) const; |
325 | |
326 | QString edKeyForUserDevice(const QString& userId, |
327 | const QString& deviceId) const; |
328 | bool hasOlmSession(const QString& user, const QString& deviceId) const; |
329 | |
330 | // This assumes that an olm session already exists. If it doesn't, no message is sent. |
331 | void sendToDevice(const QString& targetUserId, const QString& targetDeviceId, |
332 | const Event& event, bool encrypted); |
333 | |
334 | //! Returns true if this megolm session comes from a verified device |
335 | bool isVerifiedSession(const QByteArray& megolmSessionId) const; |
336 | |
337 | //! Returns whether the device is verified |
338 | bool isVerifiedDevice(const QString& userId, const QString& deviceId) const; |
339 | |
340 | //! \brief Returns whether the device is known and supports end-to-end encryption. |
341 | //! |
342 | //! This might give unexpected results for users we're not tracking, |
343 | //! i.e., users that we don't share an encrypted room with |
344 | bool isKnownE2eeCapableDevice(const QString& userId, const QString& deviceId) const; |
345 | |
346 | |
347 | void sendSessionKeyToDevices(const QString& roomId, |
348 | const QOlmOutboundGroupSession& outboundSession, |
349 | const QMultiHash<QString, QString>& devices); |
350 | |
351 | QJsonObject decryptNotification(const QJsonObject ¬ification); |
352 | QStringList devicesForUser(const QString& userId) const; |
353 | Q_INVOKABLE bool isQueryingKeys() const; |
354 | #endif // Quotient_E2EE_ENABLED |
355 | Q_INVOKABLE Quotient::SyncJob* syncJob() const; |
356 | Q_INVOKABLE QString nextBatchToken() const; |
357 | Q_INVOKABLE int millisToReconnect() const; |
358 | |
359 | Q_INVOKABLE void getTurnServers(); |
360 | |
361 | struct SupportedRoomVersion { |
362 | QString id; |
363 | QString status; |
364 | |
365 | static const QString StableTag; // "stable", as of CS API 0.5 |
366 | bool isStable() const { return status == StableTag; } |
367 | |
368 | friend QDebug operator<<(QDebug dbg, const SupportedRoomVersion& v) |
369 | { |
370 | QDebugStateSaver _(dbg); |
371 | return dbg.nospace() << v.id << '/' << v.status; |
372 | } |
373 | }; |
374 | |
375 | //! Find out if capabilites are still loading from the server |
376 | Q_INVOKABLE bool loadingCapabilities() const; |
377 | |
378 | //! \brief Get the room version recommended by the server |
379 | //! |
380 | //! Only works after server capabilities have been loaded. |
381 | //! \sa loadingCapabilities |
382 | QString defaultRoomVersion() const; |
383 | //! \brief Get the room version considered stable by the server |
384 | //! |
385 | //! Only works after server capabilities have been loaded. |
386 | //! \sa loadingCapabilities |
387 | QStringList stableRoomVersions() const; |
388 | //! \brief Get all room versions supported by the server |
389 | //! Only works after server capabilities have been loaded. |
390 | //! \sa loadingCapabilities |
391 | QVector<SupportedRoomVersion> availableRoomVersions() const; |
392 | |
393 | //! Indicate if the user can change its password from the client. |
394 | //! This is often not the case when SSO is enabled. |
395 | //! \sa loadingCapabilities |
396 | bool canChangePassword() const; |
397 | |
398 | //! \brief Check whether encryption is enabled on this connection |
399 | //! |
400 | //! There are two conditions for encryption to be enabled: |
401 | //! 1) the library must be compiled with Quotient_E2EE_ENABLED; |
402 | //! 2) encryption should be switched on explicitly with enableEncryption() |
403 | //! |
404 | //! The reason for the latter is E2EE still being in beta state as of 0.8, |
405 | //! so clients are strongly advised to keep E2EE off by default and warn |
406 | //! users opting in that their E2EE things may fall apart, leak etc. |
407 | //! |
408 | //! \sa enableEncryption |
409 | bool encryptionEnabled() const; |
410 | |
411 | //! \brief Enable or disable encryption on this connection |
412 | //! |
413 | //! \note This has no effect if the library is compiled without E2EE support |
414 | //! |
415 | //! \sa encryptionEnabled |
416 | void enableEncryption(bool enable); |
417 | |
418 | //! \brief Load room state from a previously saved file |
419 | //! |
420 | //! Call this before first sync. |
421 | //! \sa saveState |
422 | Q_INVOKABLE void loadState(); |
423 | |
424 | //! \brief Save the current state for all rooms to a file |
425 | //! |
426 | //! This method saves the current state of rooms (but not messages |
427 | //! in them) to a local cache file, so that it could be loaded by |
428 | //! loadState() on a next run of the client. |
429 | //! \sa loadState |
430 | Q_INVOKABLE void saveState() const; |
431 | |
432 | //! This method saves the current state of a single room. |
433 | void saveRoomState(Room* r) const; |
434 | |
435 | //! \brief Get the default directory path to save the room state to |
436 | //! \sa stateCacheDir |
437 | Q_INVOKABLE QString stateCachePath() const; |
438 | |
439 | //! \brief Get the default directory to save the room state to |
440 | //! |
441 | //! This function returns the default directory to store the cached |
442 | //! room state, defined as follows: |
443 | //! \code |
444 | //! QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + |
445 | //! _safeUserId + "_state.json" \endcode where `_safeUserId` is userId() with |
446 | //! `:` (colon) replaced by |
447 | //! `_` (underscore), as colons are reserved characters on Windows. |
448 | //! \sa loadState, saveState, stateCachePath |
449 | QDir stateCacheDir() const; |
450 | |
451 | //! \brief Whether or not the rooms state should be cached locally |
452 | //! \sa loadState, saveState |
453 | bool cacheState() const; |
454 | void setCacheState(bool newValue); |
455 | |
456 | bool lazyLoading() const; |
457 | void setLazyLoading(bool newValue); |
458 | |
459 | //! Start a pre-created job object on this connection |
460 | Q_INVOKABLE BaseJob* run(BaseJob* job, |
461 | RunningPolicy runningPolicy = ForegroundRequest); |
462 | |
463 | //! \brief Start a job of a given type with specified arguments and policy |
464 | //! |
465 | //! This is a universal method to create and start a job of a type passed |
466 | //! as a template parameter. The policy allows to fine-tune the way |
467 | //! the job is executed - as of this writing it means a choice |
468 | //! between "foreground" and "background". |
469 | //! |
470 | //! \param runningPolicy controls how the job is executed |
471 | //! \param jobArgs arguments to the job constructor |
472 | //! |
473 | //! \sa BaseJob::isBackground. QNetworkRequest::BackgroundRequestAttribute |
474 | template <typename JobT, typename... JobArgTs> |
475 | JobT* callApi(RunningPolicy runningPolicy, JobArgTs&&... jobArgs) |
476 | { |
477 | auto job = new JobT(std::forward<JobArgTs>(jobArgs)...); |
478 | run(job, runningPolicy); |
479 | return job; |
480 | } |
481 | |
482 | //! \brief Start a job of a specified type with specified arguments |
483 | //! |
484 | //! This is an overload that runs the job with "foreground" policy. |
485 | template <typename JobT, typename... JobArgTs> |
486 | JobT* callApi(JobArgTs&&... jobArgs) |
487 | { |
488 | return callApi<JobT>(ForegroundRequest, |
489 | std::forward<JobArgTs>(jobArgs)...); |
490 | } |
491 | |
492 | //! \brief Get a request URL for a job with specified type and arguments |
493 | //! |
494 | //! This calls JobT::makeRequestUrl() prepending the connection's homeserver |
495 | //! to the list of arguments. |
496 | template <typename JobT, typename... JobArgTs> |
497 | QUrl getUrlForApi(JobArgTs&&... jobArgs) const |
498 | { |
499 | return JobT::makeRequestUrl(homeserver(), |
500 | std::forward<JobArgTs>(jobArgs)...); |
501 | } |
502 | |
503 | //! \brief Start a local HTTP server and generate a single sign-on URL |
504 | //! |
505 | //! This call does the preparatory steps to carry out single sign-on |
506 | //! sequence |
507 | //! \sa https://matrix.org/docs/guides/sso-for-client-developers |
508 | //! \return A proxy object holding two URLs: one for SSO on the chosen |
509 | //! homeserver and another for the local callback address. Normally |
510 | //! you won't need the callback URL unless you proxy the response |
511 | //! with a custom UI. You do not need to delete the SsoSession |
512 | //! object; the Connection that issued it will dispose of it once |
513 | //! the login sequence completes (with any outcome). |
514 | Q_INVOKABLE SsoSession* (const QString& initialDeviceName, |
515 | const QString& deviceId = {}); |
516 | |
517 | //! \brief Generate a new transaction id |
518 | //! |
519 | //! Transaction id's are unique within a single Connection object |
520 | Q_INVOKABLE QString generateTxnId() const; |
521 | |
522 | //! Convert an mxc: URL into a CS API URL |
523 | Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const; |
524 | |
525 | Q_INVOKABLE bool roomSucceeds(const QString& maybePredecessorId, |
526 | const QString& maybeSuccessorId) const; |
527 | |
528 | #ifdef Quotient_E2EE_ENABLED |
529 | //! Set the E2EE default state for any Connection created further |
530 | static void setEncryptionDefault(bool useByDefault); |
531 | #endif |
532 | |
533 | //! Set a room factory function |
534 | static void setRoomFactory(room_factory_t f); |
535 | |
536 | //! Set a user factory function |
537 | static void setUserFactory(user_factory_t f); |
538 | |
539 | //! Get a room factory function |
540 | static room_factory_t roomFactory(); |
541 | |
542 | //! Get a user factory function |
543 | static user_factory_t userFactory(); |
544 | |
545 | //! Set the room factory to default with the overriden room type |
546 | template <typename T> |
547 | static void setRoomType() |
548 | { |
549 | setRoomFactory(defaultRoomFactory<T>); |
550 | } |
551 | |
552 | //! Set the user factory to default with the overriden user type |
553 | template <typename T> |
554 | static void setUserType() |
555 | { |
556 | setUserFactory(defaultUserFactory<T>); |
557 | } |
558 | |
559 | public Q_SLOTS: |
560 | //! \brief Set the homeserver base URL and retrieve its login flows |
561 | //! |
562 | //! \sa LoginFlowsJob, loginFlows, loginFlowsChanged, homeserverChanged |
563 | void setHomeserver(const QUrl& baseUrl); |
564 | |
565 | //! \brief Determine and set the homeserver from MXID |
566 | //! |
567 | //! This attempts to resolve the homeserver by requesting |
568 | //! .well-known/matrix/client record from the server taken from the MXID |
569 | //! serverpart. If there is no record found, the serverpart itself is |
570 | //! attempted as the homeserver base URL; if the record is there but |
571 | //! is malformed (e.g., the homeserver base URL cannot be found in it) |
572 | //! resolveError() is emitted and further processing stops. Otherwise, |
573 | //! setHomeserver is called, preparing the Connection object for the login |
574 | //! attempt. |
575 | //! \param mxid user Matrix ID, such as @someone:example.org |
576 | //! \sa setHomeserver, homeserverChanged, loginFlowsChanged, resolveError |
577 | void resolveServer(const QString& mxid); |
578 | |
579 | //! \brief Log in using a username and password pair |
580 | //! |
581 | //! Before logging in, this method checks if the homeserver is valid and |
582 | //! supports the password login flow. If the homeserver is invalid but |
583 | //! a full user MXID is provided, this method calls resolveServer() using |
584 | //! this MXID. |
585 | //! \sa resolveServer, resolveError, loginError |
586 | void loginWithPassword(const QString& userId, const QString& password, |
587 | const QString& initialDeviceName, |
588 | const QString& deviceId = {}); |
589 | |
590 | //! \brief Log in using a login token |
591 | //! |
592 | //! One usual case for this method is the final stage of logging in via SSO. |
593 | //! Unlike loginWithPassword() and assumeIdentity(), this method cannot |
594 | //! resolve the server from the user name because the full user MXID is |
595 | //! encoded in the login token. Callers should ensure the homeserver |
596 | //! sanity in advance. |
597 | void loginWithToken(const QString& loginToken, |
598 | const QString& initialDeviceName, |
599 | const QString& deviceId = {}); |
600 | |
601 | //! \brief Use an existing access token to connect to the homeserver |
602 | //! |
603 | //! This is the old signature that will be deprecated in 0.8; since 0.7.2, |
604 | //! device id is resolved from the access token. Use 2-arg assumeIdentity() |
605 | [[deprecated("Device id is now resolved from accessToken; use " |
606 | "assumeIdentity(QString, QString) instead of this overload" )]] // |
607 | void assumeIdentity(const QString& mxId, const QString& accessToken, |
608 | [[maybe_unused]] const QString& deviceId); |
609 | |
610 | //! \brief Use an existing access token to connect to the homeserver |
611 | //! |
612 | //! Similar to loginWithPassword(), this method checks that the homeserver |
613 | //! URL is valid and tries to resolve it from the MXID in case it is not. |
614 | //! \since 0.7.2 |
615 | void assumeIdentity(const QString& mxId, const QString& accessToken); |
616 | |
617 | //! Explicitly request capabilities from the server |
618 | void reloadCapabilities(); |
619 | |
620 | void logout(); |
621 | |
622 | void sync(int timeout = -1); |
623 | void syncLoop(int timeout = 30000); |
624 | |
625 | void stopSync(); |
626 | |
627 | virtual MediaThumbnailJob* |
628 | getThumbnail(const QString& mediaId, QSize requestedSize, |
629 | RunningPolicy policy = BackgroundRequest); |
630 | MediaThumbnailJob* getThumbnail(const QUrl& url, QSize requestedSize, |
631 | RunningPolicy policy = BackgroundRequest); |
632 | MediaThumbnailJob* getThumbnail(const QUrl& url, int requestedWidth, |
633 | int requestedHeight, |
634 | RunningPolicy policy = BackgroundRequest); |
635 | |
636 | // QIODevice* should already be open |
637 | UploadContentJob* uploadContent(QIODevice* contentSource, |
638 | const QString& filename = {}, |
639 | const QString& overrideContentType = {}); |
640 | UploadContentJob* uploadFile(const QString& fileName, |
641 | const QString& overrideContentType = {}); |
642 | GetContentJob* getContent(const QString& mediaId); |
643 | GetContentJob* getContent(const QUrl& url); |
644 | // If localFilename is empty, a temporary file will be created |
645 | DownloadFileJob* downloadFile(const QUrl& url, |
646 | const QString& localFilename = {}); |
647 | |
648 | #ifdef Quotient_E2EE_ENABLED |
649 | DownloadFileJob* downloadFile(const QUrl& url, |
650 | const EncryptedFileMetadata& fileMetadata, |
651 | const QString& localFilename = {}); |
652 | #endif |
653 | |
654 | //! \brief Create a room (generic method) |
655 | //! |
656 | //! This method allows to customize room entirely to your liking, |
657 | //! providing all the attributes the original CS API provides. |
658 | CreateRoomJob* |
659 | createRoom(RoomVisibility visibility, const QString& alias, |
660 | const QString& name, const QString& topic, QStringList invites, |
661 | const QString& presetName = {}, const QString& roomVersion = {}, |
662 | bool isDirect = false, |
663 | const QVector<CreateRoomJob::StateEvent>& initialState = {}, |
664 | const QVector<CreateRoomJob::Invite3pid>& invite3pids = {}, |
665 | const QJsonObject& creationContent = {}); |
666 | |
667 | //! \brief Get a direct chat with a single user |
668 | //! |
669 | //! This method may return synchronously or asynchoronously depending |
670 | //! on whether a direct chat room with the respective person exists |
671 | //! already. |
672 | //! \sa directChatAvailable |
673 | void requestDirectChat(const QString& userId); |
674 | |
675 | //! \brief Get a direct chat with a single user |
676 | //! |
677 | //! This method may return synchronously or asynchoronously depending |
678 | //! on whether a direct chat room with the respective person exists |
679 | //! already. |
680 | //! \sa directChatAvailable |
681 | void requestDirectChat(User* u); |
682 | |
683 | //! \brief Run an operation in a direct chat with the user |
684 | //! |
685 | //! This method may return synchronously or asynchoronously depending |
686 | //! on whether a direct chat room with the respective person exists |
687 | //! already. Instead of emitting a signal it executes the passed |
688 | //! function object with the direct chat room as its parameter. |
689 | void doInDirectChat(const QString& userId, |
690 | const std::function<void(Room*)>& operation); |
691 | |
692 | //! \brief Run an operation in a direct chat with the user |
693 | //! |
694 | //! This method may return synchronously or asynchoronously depending |
695 | //! on whether a direct chat room with the respective person exists |
696 | //! already. Instead of emitting a signal it executes the passed |
697 | //! function object with the direct chat room as its parameter. |
698 | void doInDirectChat(User* u, const std::function<void(Room*)>& operation); |
699 | |
700 | //! Create a direct chat with a single user, optional name and topic |
701 | //! |
702 | //! A room will always be created, unlike in requestDirectChat. |
703 | //! It is advised to use requestDirectChat as a default way of getting |
704 | //! one-on-one with a person, and only use createDirectChat when |
705 | //! a new creation is explicitly desired. |
706 | CreateRoomJob* createDirectChat(const QString& userId, |
707 | const QString& topic = {}, |
708 | const QString& name = {}); |
709 | |
710 | virtual JoinRoomJob* joinRoom(const QString& roomAlias, |
711 | const QStringList& serverNames = {}); |
712 | |
713 | //! \brief Send /forget to the server and delete room locally |
714 | //! |
715 | //! This method is in Connection, not in Room, since it's a |
716 | //! room lifecycle operation, and Connection is an acting room manager. |
717 | //! It ensures that the local user is not a member of a room (running /leave, |
718 | //! if necessary) then issues a /forget request and if that one doesn't fail |
719 | //! deletion of the local Room object is ensured. |
720 | //! \param id the room id to forget |
721 | //! \return the ongoing /forget request to the server; note that the |
722 | //! success() signal of this request is connected to deleteLater() |
723 | //! of a respective room so by the moment this finishes, there |
724 | //! might be no Room object anymore. |
725 | ForgetRoomJob* forgetRoom(const QString& id); |
726 | |
727 | SendToDeviceJob* sendToDevices(const QString& eventType, |
728 | const UsersToDevicesToContent& contents); |
729 | |
730 | [[deprecated("This method is experimental and may be removed any time" )]] // |
731 | SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event); |
732 | |
733 | //! \deprecated Do not use this directly, use Room::leaveRoom() instead |
734 | virtual LeaveRoomJob* leaveRoom(Room* room); |
735 | |
736 | #ifdef Quotient_E2EE_ENABLED |
737 | KeyVerificationSession* startKeyVerificationSession(const QString& userId, |
738 | const QString& deviceId); |
739 | |
740 | void encryptionUpdate(const Room* room, const QList<User*>& invited = {}); |
741 | #endif |
742 | |
743 | static Connection* makeMockConnection(const QString& mxId, |
744 | bool enableEncryption = E2EE_Enabled); |
745 | |
746 | Q_SIGNALS: |
747 | //! \brief Initial server resolution has failed |
748 | //! |
749 | //! This signal is emitted when resolveServer() did not manage to resolve |
750 | //! the homeserver using its .well-known/client record or otherwise. |
751 | //! \sa resolveServer |
752 | void resolveError(QString error); |
753 | |
754 | void homeserverChanged(QUrl baseUrl); |
755 | void loginFlowsChanged(); |
756 | void capabilitiesLoaded(); |
757 | |
758 | void connected(); |
759 | void loggedOut(); |
760 | |
761 | //! \brief Login data or state have changed |
762 | //! |
763 | //! This is a common change signal for userId, deviceId and |
764 | //! accessToken - these properties normally only change at |
765 | //! a successful login and logout and are constant at other times. |
766 | void stateChanged(); |
767 | void loginError(QString message, QString details); |
768 | |
769 | //! \brief A network request (job) started by callApi() has failed |
770 | //! \param request the pointer to the failed job |
771 | //! \sa callApi |
772 | void requestFailed(Quotient::BaseJob* request); |
773 | |
774 | //! \brief A network request (job) failed due to network problems |
775 | //! |
776 | //! This is _only_ emitted when the job will retry on its own; |
777 | //! once it gives up, requestFailed() will be emitted. |
778 | //! |
779 | //! \param message message about the network problem |
780 | //! \param details raw error details, if any available |
781 | //! \param retriesTaken how many retries have already been taken |
782 | //! \param nextRetryInMilliseconds when the job will retry again |
783 | //! (-1 if no next retry is scheduled) |
784 | void networkError(QString message, QString details, int retriesTaken, |
785 | int nextRetryInMilliseconds); |
786 | |
787 | void syncDone(); |
788 | void syncError(QString message, QString details); |
789 | |
790 | void newUser(Quotient::User* user); |
791 | |
792 | //! \group Signals emitted on room transitions |
793 | //! |
794 | //! Note: Rooms in Invite state are always stored separately from |
795 | //! rooms in Join/Leave state, because of special treatment of |
796 | //! invite_state in Matrix CS API (see The Spec on /sync for details). |
797 | //! Therefore, objects below are: r - room in Join/Leave state; |
798 | //! i - room in Invite state |
799 | //! |
800 | //! 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr) |
801 | //! 2. none -> Join: newRoom(r), joinedRoom(r,nullptr) |
802 | //! 3. none -> Leave: newRoom(r), leftRoom(r,nullptr) |
803 | //! 4. Invite -> Join: |
804 | //! newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i) |
805 | //! 4a. Leave and Invite -> Join: |
806 | //! joinedRoom(r,i), aboutToDeleteRoom(i) |
807 | //! 5. Invite -> Leave: |
808 | //! newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i) |
809 | //! 5a. Leave and Invite -> Leave: |
810 | //! leftRoom(r,i), aboutToDeleteRoom(i) |
811 | //! 6. Join -> Leave: leftRoom(r) |
812 | //! 7. Leave -> Invite: newRoom(i), invitedRoom(i,r) |
813 | //! 8. Leave -> Join: joinedRoom(r) |
814 | //! The following transitions are only possible via forgetRoom() |
815 | //! so far; if a room gets forgotten externally, sync won't tell |
816 | //! about it: |
817 | //! 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r) |
818 | |
819 | //! A new room object has been created |
820 | void newRoom(Quotient::Room* room); |
821 | |
822 | //! \brief A room invitation is seen for the first time |
823 | //! |
824 | //! If the same room is in Left state, it's passed in prev. Beware |
825 | //! that initial sync will trigger this signal for all rooms in |
826 | //! Invite state. |
827 | void invitedRoom(Quotient::Room* room, Quotient::Room* prev); |
828 | |
829 | //! \brief A joined room is seen for the first time |
830 | //! |
831 | //! It's not the same as receiving a room in "join" section of sync |
832 | //! response (rooms will be there even after joining); it's also |
833 | //! not (exactly) the same as actual joining action of a user (all |
834 | //! rooms coming in initial sync will trigger this signal too). If |
835 | //! this room was in Invite state before, the respective object is |
836 | //! passed in prev (and it will be deleted shortly afterwards). |
837 | void joinedRoom(Quotient::Room* room, Quotient::Room* prev); |
838 | |
839 | //! \brief A room has just been left |
840 | //! |
841 | //! If this room has been in Invite state (as in case of rejecting |
842 | //! an invitation), the respective object will be passed in prev |
843 | //! (and will be deleted shortly afterwards). Note that, similar |
844 | //! to invitedRoom and joinedRoom, this signal is triggered for all |
845 | //! Left rooms upon initial sync (not only those that were left |
846 | //! right before the sync). |
847 | void leftRoom(Quotient::Room* room, Quotient::Room* prev); |
848 | |
849 | //! The room object is about to be deleted |
850 | void aboutToDeleteRoom(Quotient::Room* room); |
851 | |
852 | //! \brief The room has just been created by createRoom or requestDirectChat |
853 | //! |
854 | //! This signal is not emitted in usual room state transitions, |
855 | //! only as an outcome of room creation operations invoked by |
856 | //! the client. |
857 | //! \note requestDirectChat doesn't necessarily create a new chat; |
858 | //! directChatAvailable() is more appropriate if you need to obtain |
859 | //! a direct chat room after requestDirectChat(). |
860 | void createdRoom(Quotient::Room* room); |
861 | |
862 | //! \brief The first sync for the room has been completed |
863 | //! |
864 | //! This signal is emitted after the room has been synced the first |
865 | //! time. This is the right signal to connect to if you need to |
866 | //! access the room state (name, aliases, members); state transition |
867 | //! signals (newRoom, joinedRoom etc.) come earlier, when the room |
868 | //! has just been created. |
869 | void loadedRoomState(Quotient::Room* room); |
870 | |
871 | //! Account data (except direct chats) have changed |
872 | void accountDataChanged(QString type); |
873 | |
874 | //! \brief The direct chat room is ready for using |
875 | //! |
876 | //! This signal is emitted upon any successful outcome from |
877 | //! requestDirectChat. |
878 | void directChatAvailable(Quotient::Room* directChat); |
879 | |
880 | //! \brief The list of direct chats has changed |
881 | //! |
882 | //! This signal is emitted every time when the mapping of users |
883 | //! to direct chat rooms is changed (because of either local updates |
884 | //! or a different list arrived from the server). |
885 | void directChatsListChanged(Quotient::DirectChatsMap additions, |
886 | Quotient::DirectChatsMap removals); |
887 | |
888 | void ignoredUsersListChanged(Quotient::IgnoredUsersList additions, |
889 | Quotient::IgnoredUsersList removals); |
890 | |
891 | void cacheStateChanged(); |
892 | void lazyLoadingChanged(); |
893 | void turnServersChanged(const QJsonObject& servers); |
894 | void devicesListLoaded(); |
895 | |
896 | //! Encryption has been enabled or disabled |
897 | void encryptionChanged(bool enabled); |
898 | |
899 | #ifdef Quotient_E2EE_ENABLED |
900 | void newKeyVerificationSession(Quotient::KeyVerificationSession* session); |
901 | void keyVerificationStateChanged( |
902 | const Quotient::KeyVerificationSession* session, |
903 | Quotient::KeyVerificationSession::State state); |
904 | void sessionVerified(const QString& userId, const QString& deviceId); |
905 | bool finishedQueryingKeys(); |
906 | #endif |
907 | |
908 | protected: |
909 | //! Access the underlying ConnectionData class |
910 | const ConnectionData* connectionData() const; |
911 | |
912 | //! \brief Get a Room object for the given id in the given state |
913 | //! |
914 | //! Use this method when you need a Room object in the local list |
915 | //! of rooms, with the given state. Note that this does not interact |
916 | //! with the server; in particular, does not automatically create |
917 | //! rooms on the server. This call performs necessary join state |
918 | //! transitions; e.g., if it finds a room in Invite but |
919 | //! `joinState == JoinState::Join` then the Invite room object |
920 | //! will be deleted and a new room object with Join state created. |
921 | //! In contrast, switching between Join and Leave happens within |
922 | //! the same object. |
923 | //! \param roomId room id (not alias!) |
924 | //! \param joinState desired (target) join state of the room; if |
925 | //! omitted, any state will be found and return unchanged, or a |
926 | //! new Join room created. |
927 | //! \return a pointer to a Room object with the specified id and the |
928 | //! specified state; nullptr if roomId is empty or if roomFactory() |
929 | //! failed to create a Room object. |
930 | Room* provideRoom(const QString& roomId, |
931 | Omittable<JoinState> joinState = none); |
932 | |
933 | //! Process sync data from a successful sync request |
934 | void onSyncSuccess(SyncData&& data, bool fromCache = false); |
935 | |
936 | protected Q_SLOTS: |
937 | void syncLoopIteration(); |
938 | |
939 | private: |
940 | class Private; |
941 | ImplPtr<Private> d; |
942 | |
943 | static room_factory_t _roomFactory; |
944 | static user_factory_t _userFactory; |
945 | }; |
946 | } // namespace Quotient |
947 | Q_DECLARE_METATYPE(Quotient::DirectChatsMap) |
948 | Q_DECLARE_METATYPE(Quotient::IgnoredUsersList) |
949 | |