| 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 | |