diff options
author | Hubert Chathi <uhoreg@debian.org> | 2019-06-25 16:33:24 -0400 |
---|---|---|
committer | Hubert Chathi <uhoreg@debian.org> | 2019-06-25 16:33:24 -0400 |
commit | 72d5660efd0755bb53a8699cd39865155400d288 (patch) | |
tree | ed7e7537e6a3eb7e8b92226c4015f9bfc8e11c5a | |
parent | 52407a933bfe1fcc5f3aa1dccaa0b9a8279aa634 (diff) | |
parent | 681203f951d13e9e8eaf772435cac28c6d74cd42 (diff) | |
download | libquotient-72d5660efd0755bb53a8699cd39865155400d288.tar.gz libquotient-72d5660efd0755bb53a8699cd39865155400d288.zip |
Merge branch 'upstream' (v0.5.2)
153 files changed, 4804 insertions, 2520 deletions
diff --git a/.travis.yml b/.travis.yml index 0b2967cf..3aaa4039 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,10 @@ addons: - ubuntu-toolchain-r-test - sourceline: 'ppa:beineri/opt-qt571-trusty' packages: + - ninja-build - g++-5 - qt57base + - qt57multimedia - valgrind matrix: @@ -18,23 +20,28 @@ matrix: - os: linux compiler: clang - os: osx - env: [ 'ENV_EVAL="brew update && brew install qt5 && PATH=/usr/local/opt/qt/bin:$PATH"' ] + osx_image: xcode10.1 + env: [ 'PATH=/usr/local/opt/qt/bin:$PATH' ] + addons: + homebrew: + packages: + - qt5 before_install: - eval "${ENV_EVAL}" -- if [ "$TRAVIS_OS_NAME" = "linux" ]; then VALGRIND="valgrind $VALGRIND_OPTIONS"; . /opt/qt57/bin/qt57-env.sh; fi +- if [ "$TRAVIS_OS_NAME" = "linux" ]; then USE_NINJA="-GNinja"; VALGRIND="valgrind $VALGRIND_OPTIONS"; . /opt/qt57/bin/qt57-env.sh; fi install: - git clone https://github.com/QMatrixClient/matrix-doc.git - git clone --recursive https://github.com/KitsuneRal/gtad.git - pushd gtad -- cmake -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} . +- cmake $USE_NINJA -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} . - cmake --build . - popd before_script: - mkdir build && pushd build -- cmake -DMATRIX_DOC_PATH="matrix-doc" -DGTAD_PATH="gtad/gtad" -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_PREFIX=../install .. +- cmake $USE_NINJA -DMATRIX_DOC_PATH="matrix-doc" -DGTAD_PATH="gtad/gtad" -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_PREFIX=../install .. - cmake --build . --target update-api - popd diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a9b7d11..b2536f1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.1) -project(qmatrixclient CXX) +set(API_VERSION "0.5.1") # Normally it should just include major.minor +project(qmatrixclient VERSION "0.5.2" LANGUAGES CXX) + +option(QMATRIXCLIENT_INSTALL_EXAMPLE "install qmc-example application" ON) include(CheckCXXCompilerFlag) if (NOT WIN32) @@ -40,7 +43,9 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu endif () endforeach () -find_package(Qt5 5.4.1 REQUIRED Network Gui) +# Qt 5.6+ is the formal requirement but for the sake of supporting UBPorts +# upstream Qt 5.4 is required. +find_package(Qt5 5.4.1 REQUIRED Network Gui Multimedia) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) if (GTAD_PATH) @@ -54,6 +59,7 @@ message( STATUS ) message( STATUS "=============================================================================" ) message( STATUS " libqmatrixclient Build Information" ) message( STATUS "=============================================================================" ) +message( STATUS "Version: ${PROJECT_VERSION}, API version: ${API_VERSION}") if (CMAKE_BUILD_TYPE) message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") endif(CMAKE_BUILD_TYPE) @@ -76,6 +82,7 @@ set(libqmatrixclient_SRCS lib/room.cpp lib/user.cpp lib/avatar.cpp + lib/syncdata.cpp lib/settings.cpp lib/networksettings.cpp lib/converters.cpp @@ -85,6 +92,8 @@ set(libqmatrixclient_SRCS lib/events/roomevent.cpp lib/events/stateevent.cpp lib/events/eventcontent.cpp + lib/events/roomcreateevent.cpp + lib/events/roomtombstoneevent.cpp lib/events/roommessageevent.cpp lib/events/roommemberevent.cpp lib/events/typingevent.cpp @@ -115,7 +124,6 @@ if (MATRIX_DOC_PATH AND GTAD_PATH) add_custom_target(update-api ${ABS_GTAD_PATH} --config ${CSAPI_DIR}/gtad.yaml --out ${CSAPI_DIR} ${FULL_CSAPI_SRC_DIR} - cas_login_redirect.yaml- cas_login_ticket.yaml- old_sync.yaml- room_initial_sync.yaml- # deprecated sync.yaml- # we have a better handcrafted implementation WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/lib @@ -139,8 +147,7 @@ add_library(QMatrixClient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS} ${libqmatrixclient_csdef_SRCS} ${libqmatrixclient_cswellknown_SRCS} ${libqmatrixclient_asdef_SRCS} ${libqmatrixclient_isdef_SRCS}) -set(API_VERSION "0.4") -set_property(TARGET QMatrixClient PROPERTY VERSION "${API_VERSION}.2.1") +set_property(TARGET QMatrixClient PROPERTY VERSION "${PROJECT_VERSION}") set_property(TARGET QMatrixClient PROPERTY SOVERSION ${API_VERSION} ) set_property(TARGET QMatrixClient PROPERTY INTERFACE_QMatrixClient_MAJOR_VERSION ${API_VERSION}) @@ -151,7 +158,7 @@ target_include_directories(QMatrixClient PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib> $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> ) -target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui) +target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) add_executable(qmc-example ${example_SRCS}) target_link_libraries(qmc-example Qt5::Core QMatrixClient) @@ -168,10 +175,11 @@ install(DIRECTORY lib/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h") include(CMakePackageConfigHelpers) +# NB: SameMajorVersion doesn't really work yet, as we're within 0.x trail. +# Maybe consider jumping the gun and releasing 1.0, as semver advises? write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfigVersion.cmake" - VERSION ${API_VERSION} - COMPATIBILITY AnyNewerVersion + COMPATIBILITY SameMajorVersion ) export(PACKAGE QMatrixClient) @@ -197,7 +205,9 @@ if (WIN32) install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) endif (WIN32) -install(TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +if (QMATRIXCLIENT_INSTALL_EXAMPLE) + install(TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif (QMATRIXCLIENT_INSTALL_EXAMPLE) if (UNIX AND NOT APPLE) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ee39eec..56bc9d91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ For general discussion, feel free to use our Matrix room: [#quaternion:matrix.org](https://matrix.to/#/#quaternion:matrix.org). If you're new to the project (or FLOSS in general), -[issues tagged as simple](https://github.com/QMatrixClient/libQMatrixClient/labels/simple) +[issues tagged as easy](https://github.com/QMatrixClient/libQMatrixClient/labels/easy) are smaller tasks that may typically take 1-3 days. You are welcome aboard! @@ -86,17 +86,18 @@ a commit without a DCO is an accident and the DCO still applies. --> ### License -Unless a contributor explicitly specifies otherwise, we assume that all -contributed code is released under [the same license as libQMatrixClient itself](./COPYING), -which is LGPL v2.1 as of the time of this writing. +Unless a contributor explicitly specifies otherwise, we assume contributors +to agree that all contributed code is released either under *LGPL v2.1 or later*. +This is more than just [LGPL v2.1 libQMatrixClient now uses](./COPYING) +because the project plans to switch to LGPL v3 for library code in the near future. <!-- The below is invalid yet! All new contributed material that is not executable, including all text when not executed, is also released under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/) or later. --> Any components proposed for reuse should have a license that permits releasing -a derivative work under LGPL v2.1. Moreover, the license of a proposed component -should be approved by OSI, no exceptions. +a derivative work under *LGPL v2.1 or later* or LGPL v3. Moreover, the license of +a proposed component should be approved by OSI, no exceptions. ## Vulnerability reporting (security issues) @@ -108,11 +109,12 @@ use either of the following contacts: In any of these two options, _indicate that you have such information_ (do not share the information yet), and we'll tell you the next steps. -By default, we will give credit to anyone who reports a vulnerability so that -we can fix it. If you want to remain anonymous or pseudonymous instead, please -let us know that; we will gladly respect your wishes. If you provide a fix as -a PR, you have no way to remain anonymous but we still accept contributors with -pseudonyms. +By default, we will give credit to anyone who reports a vulnerability in +a responsible way so that we can fix it before public disclosure. If you want +to remain anonymous or pseudonymous instead, please let us know; we will +gladly respect your wishes. If you provide a fix as a PR, you have no way +to remain anonymous (and you also disclose the vulnerability thereby) so this +is not the right way, unless the vulnerability is already made public. ## Documentation changes @@ -154,22 +156,40 @@ If you don't plan/have substantial contributions, you can end reading here. Furt The code should strive to be DRY (don't repeat yourself), clear, and obviously correct. Some technical debt is inevitable, just don't bankrupt us with it. Refactoring is welcome. ### Generated C++ code for CS API -The code in lib/csapi, although it resides in Git, is actually generated from the official Matrix Swagger/OpenAPI definition files. If you're unhappy with something in that directory and want to improve the code, you have to understand the way these files are produced and setup some additional tooling. The shortest possible procedure resembling the below text can be found in .travis.yml (our Travis CI configuration actually regenerates those files upon every build). The generating sequence only works with CMake atm; patches to enable it with qmake are (you guessed it) very welcome. +The code in lib/csapi, lib/identity and lib/application-service, although +it resides in Git, is actually generated from (a soft fork of) the official +Matrix Swagger/OpenAPI definition files. If you're unhappy with something in +these directories and want to improve the code, you have to understand the way +these files are produced and setup some additional tooling. The shortest +possible procedure resembling the below text can be found in .travis.yml +(our Travis CI configuration actually regenerates those files upon every build). +The generating sequence only works with CMake atm; +patches to enable it with qmake are (you guessed it) very welcome. #### Why generate the code at all? -Because before both original authors of libQMatrixClient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). +Because before both original authors of libQMatrixClient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep, coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). #### Prerequisites for CS API code generation 1. Get the source code of GTAD and its dependencies, e.g. using the command: `git clone --recursive https://github.com/KitsuneRal/gtad.git` 2. Build GTAD: in the source code directory, do `cmake . && cmake --build .` (you might need to pass `-DCMAKE_PREFIX_PATH=<path to Qt>`, similar to libQMatrixClient itself). 3. Get the Matrix CS API definitions that are included in the matrix-doc repo: `git clone https://github.com/QMatrixClient/matrix-doc.git` (QMatrixClient/matrix-doc is a fork that's known to produce working code; you may want to use your own fork if you wish to alter something in the API). -#### Generating lib/csapi contents -1. Pass additional configuration to CMake when configuring libQMatrixClient, namely: `-DMATRIX_DOC_PATH=<path you your matrix-doc repo> -DGTAD_PATH=<path to gtad binary (not the repo!)>`. If everything's right, these two CMake variables will be mentioned in CMake output and will trigger configuration of an additional build target, see the next step. -2. Generate the code: `cmake --build <your build dir> --target update-api`; if you use CMake with GNU Make, you can just do `make update-api` instead. Building this target will create (overwriting without warning) .h and .cpp files in lib/csapi directory for all YAML files it can find in `matrix-doc/api/client-server`. -3. Once you've done that, you can build the library as usual; rerunning CMake is recommended if the list of generated files has changed. - -#### Changing things in lib/csapi +#### Generating CS API contents +1. Pass additional configuration to CMake when configuring libQMatrixClient: + `-DMATRIX_DOC_PATH=<path you your matrix-doc repo> -DGTAD_PATH=<path to gtad binary (not the repo!)>`. + If everything's right, these two CMake variables will be mentioned in + CMake output and will trigger configuration of an additional build target, + see the next step. +2. Generate the code: `cmake --build <your build dir> --target update-api`; + if you use CMake with GNU Make, you can just do `make update-api` instead. + Building this target will create (overwriting without warning) `.h` and `.cpp` + files in `lib/csapi`, `lib/identity`, `lib/application-service` for all + YAML files it can find in `matrix-doc/api/client-server` and other files + in `matrix-doc/api` these depend on. +3. Once you've done that, you can build the library as usual; rerunning CMake + is recommended if the list of generated files has changed. + +#### Changing generated code See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQMatrixClient are described here. GTAD uses the following three kinds of sources: @@ -179,43 +199,81 @@ GTAD uses the following three kinds of sources: The mustache files have a templated (not in C++ sense) definition of a network job, deriving from BaseJob; each job class is prepended, if necessary, with data structure definitions used by this job. The look of those files is hideous for a newcomer; the fact that there's no highlighter for the combination of Mustache (originally a web templating language) and C++ doesn't help things, either. To slightly simplify things some more or less generic constructs are defined in gtad.yaml (see its "mustache:" section). Adventurous souls that would like to figure what's going on in these files should speak up in the libQMatrixClient room - I (Kitsune) will be very glad to help you out. -The types map in gtad.yaml is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": +The types map in `gtad.yaml` is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": * `avoidCopy` - this attribute defines whether a const ref should be used instead of a value. For basic types like int this is obviously unnecessary; but compound types like `QVector` should rather be taken by reference when possible. * `moveOnly` - some types are not copyable at all and must be moved instead (an obvious example is anything "tainted" with a member of type `std::unique_ptr<>`). The template will use `T&&` instead of `T` or `const T&` to pass such types around. * `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h` - a substitute for `std::optional` from C++17 (we're still at C++14 yet). * `omittedValue` - an alternative for `useOmittable`, just provide a value used for an omitted parameter. This is used for bool parameters which normally are considered false if omitted (or they have an explicit default value, passed in the "official" GTAD's `defaultValue` variable). * `initializer` - this is a _partial_ (see GTAD and Mustache documentation for explanations but basically it's a variable that is a Mustache template itself) that specifies how exactly a default value should be passed to the parameter. E.g., the default value for a `QString` parameter is enclosed into `QStringLiteral`. -Instead of relying on the event structure definition in the OpenAPI files, gtad.yaml uses pointers to libQMatrixClient's event structures: `EventPtr`, `RoomEventPtr` and `StateEventPtr`. Respectively, arrays of events, when encountered in OpenAPI definitions, are converted to `Events`, `RoomEvents` and `StateEvents` containers. When there's no way to figure the type from the definition, an opaque `QJsonObject` is used, leaving the conversion to the library and/or client code. +Instead of relying on the event structure definition in the OpenAPI files, `gtad.yaml` uses pointers to libQMatrixClient's event structures: `EventPtr`, `RoomEventPtr` and `StateEventPtr`. Respectively, arrays of events, when encountered in OpenAPI definitions, are converted to `Events`, `RoomEvents` and `StateEvents` containers. When there's no way to figure the type from the definition, an opaque `QJsonObject` is used, leaving the conversion to the library and/or client code. ### Library API and doc-comments -Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. Doxygen (with backslashes) style is preferred. You can find that some parts of the code still use JavaDoc (with @'s) style; feel free to replace it with Doxygen backslashes if that bothers you. Some parts are not even documented; add doc-comments to them is highly encouraged. +Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. Doxygen (with backslashes) style is preferred. You can find that some parts of the code still use JavaDoc (with @'s) style; feel free to replace it with Doxygen backslashes if that bothers you. Some parts are not even documented; adding doc-comments to them is highly encouraged. -Calls, data structures and other symbols not intended for use by clients should _not_ be exposed in (public) .h files, unless they are necessary to declare other public symbols. In particular, this involves private members (functions, typedefs, or variables) in public classes; use pimpl idiom to hide implementation details as much as possible. +Calls, data structures and other symbols not intended for use by clients +should _not_ be exposed in (public) .h files, unless they are necessary +to declare other public symbols. In particular, this involves private members +(functions, typedefs, or variables) in public classes; use pimpl idiom to hide +implementation details as much as possible. `_impl` namespace is reserved for +definitions that should not be used by clients and are not covered by +API guarantees. Note: As of now, all header files of libQMatrixClient are considered public; this may change eventually. ### Qt-flavoured C++ -This is our primary language. We don't have a particular code style _as of yet_ but some rules-of-thumb are below: -* 4-space indents, no tabs, no trailing spaces, no last empty lines. If you spot the code abusing these - we'll thank you for fixing it. +This is our primary language. A particular code style is not enforced _yet_ but +[the PR imposing the common code style](https://github.com/QMatrixClient/libqmatrixclient/pull/295) +is planned to arrive in version 0.6. +* 4-space indents, no tabs, no trailing spaces, no last empty lines. If you + spot the code abusing these - we'll thank you for fixing it. * Prefer keeping lines within 80 characters. -* Braces after if's, while's, do's, function signatures etc. take a separate line. Keeping the opening brace on the same line is still ok. -* A historical deviation from the usual Qt code format conventions is an extra indent inside _classes_ (access specifiers go at +4 spaces to the base, members at +8 spaces) but not _structs_ (members at +4 spaces). This may change in the future for something more conventional. -* Please don't make "hypocritical structs" with protected or private members. Just make them classes instead. -* For newly created classes, keep to [the rule of 3/5/0](http://en.cppreference.com/w/cpp/language/rule_of_three) - make sure to read about the rule of zero if you haven't before, it's not what you might think it is. -* Qt containers are generally preferred to STL containers; however, there are notable exceptions, and libQMatrixClient already uses them: +* Braces after if's, while's, do's, function signatures etc. take + a separate line. Keeping the opening brace on the same line is still ok. +* A historical deviation from the usual Qt code format conventions is an extra + indent inside _classes_ (access specifiers go at +4 spaces to the base, + members at +8 spaces) but not _structs_ (members at +4 spaces). This may + change in the future for something more conventional. +* Please don't make "hypocritical structs" with protected or private members. + In general, `struct` is used to denote a plain-old-data structure, rather + than data+behaviour. If you need access control or are adding yet another + non-trivial (construction, assignment) member function to a `struct`, + just make it a `class` instead. +* For newly created classes, keep to + [the rule of 3/5/0](http://en.cppreference.com/w/cpp/language/rule_of_three) - + make sure to read about the rule of zero if you haven't before, it's not + what you might think it is. +* Qt containers are generally preferred to STL containers; however, there are + notable exceptions, and libQMatrixClient already uses them: * `std::array` and `std::deque` have no direct counterparts in Qt. - * Because of COW semantics, Qt containers cannot hold uncopyable classes. Classes without a default constructor are a problem too. Examples of that are `SyncRoomData` and `EventsArray<>`. Use STL containers for those but see the next point and also consider if you can supply a reasonable copy/default constructor. - * STL containers can be freely used in code internal to a translation unit (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up code where you don't need COW, or when dealing with uncopyable data structures (see the previous point). However, exposing STL containers in the API is not encouraged (except where absolutely necessary, e.g. we use `std::deque` for a timeline). Exposing STL containers or iterators in API intended for usage by QML code (e.g. in `Q_PROPERTY`) is unlikely to work and therefore unlikely to be accepted into `master`. -* Use `QVector` instead of `QList` where possible - see a [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) for details. + * Because of COW semantics, Qt containers cannot hold uncopyable classes. + Classes without a default constructor are a problem too. Examples of that + are `SyncRoomData` and `EventsArray<>`. Use STL containers for those but + see the next point and also consider if you can supply a reasonable + copy/default constructor. + * STL containers can be freely used in code internal to a translation unit + (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. + It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up + code where you don't need COW, or when dealing with uncopyable + data structures (see the previous point). However, exposing STL containers + in the API is not encouraged (except where absolutely necessary, e.g. we use + `std::deque` for a timeline). Exposing STL containers or iterators in API + intended for usage by QML code (e.g. in `Q_PROPERTY`) is unlikely to work + and therefore unlikely to be accepted into `master`. +* Use `QVector` instead of `QList` where possible - see the + [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) + for details. ### Automated tests -There's no testing framework as of now; either Catch or Qt Test or both will be used eventually. However, as a stopgap measure, qmc-example is used for automated end-to-end testing. +There's no testing framework as of now; either Catch or Qt Test or both will +be used eventually. -Any significant addition to the library API should be accompanied by a respective test in qmc-example. To add a test you should: +As a stopgap measure, qmc-example is used for automated functional testing. +Therefore, any significant addition to the library API should be accompanied +by a respective test in qmc-example. To add a test you should: - Add a new private slot to the `QMCTest` class. - Add to the beginning of the slot the line `running.push_back("Test name");`. - Add test logic to the slot, using `QMC_CHECK` macro to assert the test outcome. ALL (even failing) branches should conclude with a QMC_CHECK invocation, unless you intend to have a "DID NOT FINISH" message in the logs under certain conditions. @@ -253,15 +311,30 @@ your commit into it (with an explanation what it is about and why). ### Standard checks -The following warnings configuration is applied with GCC and Clang when using CMake: `-W -Wall -Wextra -pedantic -Werror=return-type -Wno-unused-parameter -Wno-gnu-zero-variadic-macro-arguments` (the last one is to mute a warning triggered by Qt code for debug logging). We don't turn most of the warnings to errors but please treat them as such. In Qt Creator, the following line can be used with the Clang code model (before Qt Creator 4.7 you should explicitly enable the Clang code model plugin): `-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables` +The following warnings configuration is applied with GCC and Clang when using CMake: +`-W -Wall -Wextra -pedantic -Werror=return-type -Wno-unused-parameter -Wno-gnu-zero-variadic-macro-arguments` +(the last one is to mute a warning triggered by Qt code for debug logging). +We don't turn most of the warnings to errors but please treat them as such. +In Qt Creator, the following line can be used with the Clang code model +(before Qt Creator 4.7 you should explicitly enable the Clang code model plugin): +`-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables -Wno-unknown-attributes -Wno-comma`. ### Continuous Integration -We use Travis CI to check buildability and smoke-testing on Linux (GCC, Clang) and MacOS (Clang), and AppVeyor CI to build on Windows (MSVC). Every PR will go through these, and you'll see the traffic lights from them on the PR page. Failure on any platform will most likely entail a request to you for a fix before merging a PR. +We use Travis CI to check buildability and smoke-testing on Linux (GCC, Clang) and MacOS (Clang), and AppVeyor CI to build on Windows (MSVC). Every PR will go through these, and you'll see the traffic lights from them on the PR page. If your PR fails on any platform double-check that it's not your code causing it - and fix it if it is. ### Other tools -If you know how to use clang-tidy, here's a list of checks we do and do not use (a leading hyphen means a disabled check, an asterisk is a wildcard): `*,cert-env33-c,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-cppcoreguidelines-pro-type-const-cast,-cppcoreguidelines-pro-type-union-access,-cppcoreguidelines-special-member-functions,-google-build-using-namespace,-google-readability-braces-around-statements,-hicpp-*,-llvm-*,-misc-unused-parameters,-misc-noexcept-moveconstructor,-modernize-use-using,-readability-braces-around-statements,readability-identifier-naming,-readability-implicit-bool-cast,-clang-diagnostic-*,-clang-analyzer-*`. If you're on CLion, you can simply copy-paste the above list into the Clang-Tidy inspection configuration. In Qt Creator 4.6 and newer, one can enable clang-tidy and clazy (clazy level 1 eats away CPU but produces some very relevant and unobvious notices, such as possible unintended copying of a Qt container, or unguarded null pointers). +Recent versions of Qt Creator and CLion can automatically run your code through +clang-tidy. The following list of clang-tidy checks slows Qt Creator analysis +quite considerably but gives a good insight without too many false positives: +`-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-move-forwarding-reference,bugprone-string-constructor,bugprone-undefined-memory-manipulation,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl03-c,cert-dcl21-cpp,cert-dcl50-cpp,cert-dcl54-cpp,cert-dcl58-cpp,cert-env33-c,cert-err09-cpp,cert-err34-c,cert-err52-cpp,cert-err60-cpp,cert-err61-cpp,cert-fio38-c,cert-flp30-c,cert-msc30-c,cert-msc50-cpp,cert-oop11-cpp,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-slicing,hicpp-deprecated-headers,hicpp-invalid-access-moved,hicpp-member-init,hicpp-move-const-arg,hicpp-named-parameter,hicpp-new-delete-operators,hicpp-static-assert,hicpp-undelegated-constructor,hicpp-use-*,misc-misplaced-const,misc-new-delete-overloads,misc-non-copyable-objects,misc-redundant-expression,misc-static-assert,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,misc-uniqueptr-reset-release,misc-unused-*,modernize-loop-convert,modernize-pass-by-value,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-*,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-*,performance-move-*,performance-type-promotion-in-math-fn,performance-unnecessary-*,readability-delete-null-pointer,readability-else-after-return,readability-inconsistent-declaration-parameter-name,readability-misleading-indentation,readability-redundant-*,readability-simplify-boolean-expr,readability-static-definition-in-anonymous-namespace,readability-uniqueptr-delete-release`. + +Qt Creator, in addition, knows about clazy, an even deeper Qt-aware static +analysis tool. Even level 1 clazy eats away CPU but produces some very relevant +and unobvious notices, such as possible unintended copying of a Qt container, +or unguarded null pointers. You can use this time to time (see Analyze menu in +Qt Creator) instead of hogging your machine with deep analysis as you type. ## Git commit messages @@ -281,13 +354,28 @@ When writing git commit messages, try to follow the guidelines in C++ is unfortunately not very coherent about SDK/package management, and we try to keep building the library as easy as possible. Because of that we are very conservative about adding dependencies to libQMatrixClient. That relates to additional Qt components and even more to other libraries. Fortunately, even the Qt components now in use (Qt Core and Network) are very feature-rich and provide plenty of ready-made stuff. -Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for automated testing, so PRs onboarding a test framework will be considered with much gratitude. +Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for futures and automated testing, so PRs onboarding those will be considered with much gratitude. Some cases need additional explanation: -* Before rolling out your own super-optimised container or algorithm written from scratch, take a good long look through documentation on Qt and C++ standard library. Please try to reuse the existing facilities as much as possible. -* You should have a good reason (or better several ones) to add a component from KDE Frameworks. We don't rule this out and there's no prejudice against KDE; it just so happened that KDE Frameworks is one of most obvious reuse candidates but so far none of these components survived as libQMatrixClient deps. So we are cautious. -* Never forget that libQMatrixClient is aimed to be a non-visual library; QtGui in dependencies is only driven by (entirely offscreen) dealing with QPixmaps. While there's a bunch of visual code (in C++ and QML) shared between libQMatrixClient-enabled _applications_, this is likely to end up in a separate (libQMatrixClient-enabled) library, rather than libQMatrixClient itself. +* Before rolling out your own super-optimised container or algorithm written + from scratch, take a good long look through documentation on Qt and + C++ standard library. Please try to reuse the existing facilities + as much as possible. +* You should have a good reason (or better several ones) to add a component + from KDE Frameworks. We don't rule this out and there's no prejudice against + KDE; it just so happened that KDE Frameworks is one of most obvious + reuse candidates but so far none of these components survived + as libQMatrixClient deps. So we are cautious. Extra notice to KDE folks: + I'll be happy if an addon library on top of libQMatrixClient is made using + KDE facilities, and I'm willing to take part in its evolution; but please + also respect LXDE people who normally don't have KDE frameworks installed. +* Never forget that libQMatrixClient is aimed to be a non-visual library; + QtGui in dependencies is only driven by (entirely offscreen) dealing with + QImages. While there's a bunch of visual code (in C++ and QML) shared + between libQMatrixClient-enabled _applications_, this is likely to end up + in a separate (libQMatrixClient-enabled) library, rather than + libQMatrixClient itself. ## Attribution -This text is largely based on CONTRIBUTING.md from CII Best Practices Badge project, which is a collective work of its contributors (many thanks!). The text itself is licensed under CC-BY-4.0. +This text is based on CONTRIBUTING.md from CII Best Practices Badge project, which is a collective work of its contributors (many thanks!). The text itself is licensed under CC-BY-4.0. @@ -1,12 +1,15 @@ # libQMatrixClient +<a href='https://matrix.org'><img src='https://matrix.org/docs/projects/images/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a> + [![license](https://img.shields.io/github/license/QMatrixClient/libqmatrixclient.svg)](https://github.com/QMatrixClient/libqmatrixclient/blob/master/COPYING) ![status](https://img.shields.io/badge/status-beta-yellow.svg) [![release](https://img.shields.io/github/release/QMatrixClient/libqmatrixclient/all.svg)](https://github.com/QMatrixClient/libqmatrixclient/releases/latest) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1023/badge)](https://bestpractices.coreinfrastructure.org/projects/1023) +[![](https://img.shields.io/cii/percentage/1023.svg?label=CII%20best%20practices)](https://bestpractices.coreinfrastructure.org/projects/1023/badge) +![](https://img.shields.io/github/commit-activity/y/QMatrixClient/libQMatrixClient.svg) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) -libQMatrixClient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Tensor](https://matrix.org/docs/projects/client/tensor.html) and some other projects. +libQMatrixClient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Spectral](https://matrix.org/docs/projects/client/spectral.html) and some other projects. ## Contacts You can find authors of libQMatrixClient in the Matrix room: [#qmatrixclient:matrix.org](https://matrix.to/#/#qmatrixclient:matrix.org). @@ -14,33 +17,44 @@ You can find authors of libQMatrixClient in the Matrix room: [#qmatrixclient:mat You can also file issues at [the project's issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues). If you have what looks like a security issue, please see respective instructions in CONTRIBUTING.md. ## Building and usage -So far the library is typically used as a git submodule of another project (such as Quaternion); however it can be built separately (either as a static or as a dynamic library). As of version 0.2, the library can be installed and CMake package config files are provided; projects can use `find_package(QMatrixClient)` to setup their code with the installed library files. PRs to enable the same for qmake are most welcome. - -The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrixclient - checking out a certain commit or tag from GitHub (rather than downloading the archive) is the recommended way for one-off building. If you want to hack on the library as a part of another project (e.g. you are working on Quaternion but need to do some changes to the library code), you're advised to make a recursive check out of that project (in this case, Quaternion) and update the library submodule to its master branch. - -Tags starting with `v` represent released versions; `rc` mark release candidates. +So far the library is typically used as a git submodule of another project +(such as Quaternion); however it can be built separately (either as a static or +as a dynamic library). After installing the library the CMake package becomes +available for `find_package(QMatrixClient)` to setup the client code with +the installed library files. PRs to enable the same for qmake are most welcome. + +[The source code is hosted at GitHub](https://github.com/QMatrixClient/libqmatrixclient) - +checking out a certain commit or tag (rather than downloading the archive) is +the recommended way for one-off building. If you want to hack on the library +as a part of another project (e.g. you are working on Quaternion but need +to do some changes to the library code), you're advised to make a recursive +check out of that project (in this case, Quaternion) and update +the library submodule to its master branch. + +Tags consisting of digits and periods represent released versions; tags ending with `-betaN` or `-rcN` mark pre-releases. If/when packaging pre-releases, it is advised to replace a dash with a tilde. ### Pre-requisites -- a Linux, OSX or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) +- a Linux, macOS or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) - For Ubuntu flavours - zesty or later (or a derivative) is good enough out of the box; older ones will need PPAs at least for a newer Qt; in particular, if you have xenial you're advised to add Kubuntu Backports PPA for it - a Git client to check out this repo - Qt 5 (either Open Source or Commercial), version 5.6 or higher + (5.9 or higher is strongly recommended) - a build configuration tool: - CMake (from your package management system or [the official website](https://cmake.org/download/)) - or qmake (comes with Qt) - a C++ toolchain supported by your version of Qt (see a link for your platform at [the Qt's platform requirements page](http://doc.qt.io/qt-5/gettingstarted.html#platform-requirements)) - - GCC 5 (Windows, Linux, OSX), Clang 5 (Linux), Apple Clang 8.1 (OSX) and Visual C++ 2015 (Windows) are the oldest officially supported; Clang 3.8 and GCC 4.9.2 are known to still work, maintenance patches for them are accepted + - GCC 5 (Windows, Linux, macOS), Clang 5 (Linux), Apple Clang 8.1 (macOS) and Visual C++ 2015 (Windows) are the oldest officially supported; Clang 3.8 and GCC 4.9.2 are known to still work, maintenance patches for them are accepted - any build system that works with CMake and/or qmake should be fine: GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. #### Linux Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to run cmake/qmake and look at error messages. The library is entirely offscreen (QtCore and QtNetwork are essential) but it also depends on QtGui in order to handle avatar thumbnails. -#### OS X -`brew install qt5` should get you a recent Qt5. If you plan to use CMake, you may need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=<where-Qt-installed>` +#### macOS +`brew install qt5` should get you a recent Qt5. If you plan to use CMake, you will need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)` #### Windows 1. Install Qt5, using their official installer. -1. If you plan to build with CMake, install CMake; if you're ok with qmake, you don't need to install anything on top of Qt. The commands in further sections imply that cmake/qmake is in your PATH - otherwise you have to prepend those commands with actual paths. As an option, it's a good idea to run a `qtenv2.bat` script that can be found in `C:\Qt\<Qt version>\<toolchain>\bin` (assuming you installed Qt to `C:\Qt`); the only thing it does is adding necessary paths to PATH. You might not want to run that script on system startup but it's very handy to setup the environment before building. For CMake, setting `CMAKE_PREFIX_PATH` in the same way as for OS X (see above), also helps. +1. If you plan to build with CMake, install CMake; if you're ok with qmake, you don't need to install anything on top of Qt. The commands in further sections imply that cmake/qmake is in your PATH - otherwise you have to prepend those commands with actual paths. As an option, it's a good idea to run a `qtenv2.bat` script that can be found in `C:\Qt\<Qt version>\<toolchain>\bin` (assuming you installed Qt to `C:\Qt`); the only thing it does is adding necessary paths to PATH. You might not want to run that script on system startup but it's very handy to setup the environment before building. For CMake, setting `CMAKE_PREFIX_PATH` in the same way as for macOS (see above), also helps. There are no official MinGW-based 64-bit packages for Qt. If you're determined to build a 64-bit library, either use a Visual Studio toolchain or build Qt5 yourself as described in Qt documentation. @@ -53,13 +67,14 @@ cd build_dir cmake .. # Pass -DCMAKE_PREFIX_PATH and -DCMAKE_INSTALL_PREFIX here if needed cmake --build . --target all ``` -This will get you the compiled library in `build_dir` inside your project sources. Static builds are tested on all supported platforms. Dynamic builds of libqmatrixclient are only tested on Linux at the moment; experiments with dynamic builds on Windows/OSX are welcome. Taking a look at [qmc-example](https://github.com/QMatrixClient/libqmatrixclient/tree/master/examples) (used to test the library) should give you a basic idea of using libQMatrixClient; for more extensive usage check out the source code of [Quaternion](https://github.com/QMatrixClient/Quaternion) (the reference client built on QMatrixClient). +This will get you the compiled library in `build_dir` inside your project sources. Static builds are tested on all supported platforms. Dynamic builds of libqmatrixclient are only tested on Linux at the moment; experiments with dynamic builds on Windows/macOS are welcome. Taking a look at [qmc-example](https://github.com/QMatrixClient/libqmatrixclient/tree/master/examples) (used to test the library) should give you a basic idea of using libQMatrixClient; for more extensive usage check out the source code of [Quaternion](https://github.com/QMatrixClient/Quaternion) (the reference client built on QMatrixClient). You can install the library with CMake: ``` cmake --build . --target install ``` This will also install cmake package config files; once this is done, you can use `examples/CMakeLists.txt` to compile the example with the _installed_ library. This file is a good starting point for your own CMake-based project using libQMatrixClient. +Installation of `qmc-example` application can be skipped by setting `QMATRIXCLIENT_INSTALL_EXAMPLE` to `OFF`. #### qmake-based The library provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `qmc-example.pro` that will build a minimal example of library usage for you. In the root directory of the project sources: diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 49e0089a..cd5e15ed 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -45,7 +45,7 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu endif () endforeach () -find_package(Qt5 5.6 REQUIRED Network Gui) +find_package(Qt5 5.6 REQUIRED Network Gui Multimedia) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) find_package(QMatrixClient REQUIRED) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 206501a5..bd9190b9 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -10,6 +10,8 @@ #include <QtCore/QCoreApplication> #include <QtCore/QStringBuilder> #include <QtCore/QTimer> +#include <QtCore/QTemporaryFile> +#include <QtCore/QFileInfo> #include <iostream> #include <functional> @@ -21,22 +23,26 @@ using namespace std::placeholders; class QMCTest : public QObject { public: - QMCTest(Connection* conn, const QString& testRoomName, QString source); + QMCTest(Connection* conn, QString testRoomName, QString source); private slots: - void setup(const QString& testRoomName); + void setupAndRun(); void onNewRoom(Room* r); - void startTests(); + void run(); + void doTests(); + void loadMembers(); void sendMessage(); + void sendFile(); + void checkFileSendingOutcome(const QString& txnId, + const QString& fileName); void setTopic(); void addAndRemoveTag(); void sendAndRedact(); - void checkRedactionOutcome(const QString& evtIdToRedact, - RoomEventsRange events); + bool checkRedactionOutcome(const QString& evtIdToRedact); void markDirectChat(); void checkDirectChatOutcome( const Connection::DirectChatsMap& added); - void leave(); + void conclude(); void finalize(); private: @@ -45,80 +51,79 @@ class QMCTest : public QObject QStringList succeeded; QStringList failed; QString origin; + QString targetRoomName; Room* targetRoom = nullptr; + + bool validatePendingEvent(const QString& txnId); }; #define QMC_CHECK(description, condition) \ { \ - const bool result = !!(condition); \ Q_ASSERT(running.removeOne(description)); \ - (result ? succeeded : failed).push_back(description); \ - cout << (description) << (result ? " successul" : " FAILED") << endl; \ - if (targetRoom) \ - targetRoom->postMessage(origin % ": " % QStringLiteral(description) % \ - (result ? QStringLiteral(" successful") : QStringLiteral(" FAILED")), \ - result ? MessageEventType::Notice : MessageEventType::Text); \ + if (!!(condition)) \ + { \ + succeeded.push_back(description); \ + cout << (description) << " successful" << endl; \ + if (targetRoom) \ + targetRoom->postMessage( \ + origin % ": " % (description) % " successful", \ + MessageEventType::Notice); \ + } else { \ + failed.push_back(description); \ + cout << (description) << " FAILED" << endl; \ + if (targetRoom) \ + targetRoom->postPlainText( \ + origin % ": " % (description) % " FAILED"); \ + } \ +} + +bool QMCTest::validatePendingEvent(const QString& txnId) +{ + auto it = targetRoom->findPendingEvent(txnId); + return it != targetRoom->pendingEvents().end() && + it->deliveryStatus() == EventStatus::Submitted && + (*it)->transactionId() == txnId; } -QMCTest::QMCTest(Connection* conn, const QString& testRoomName, QString source) - : c(conn), origin(std::move(source)) +QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source) + : c(conn), origin(std::move(source)), targetRoomName(std::move(testRoomName)) { if (!origin.isEmpty()) cout << "Origin for the test message: " << origin.toStdString() << endl; - if (!testRoomName.isEmpty()) - cout << "Test room name: " << testRoomName.toStdString() << endl; + if (!targetRoomName.isEmpty()) + cout << "Test room name: " << targetRoomName.toStdString() << endl; - connect(c.data(), &Connection::connected, - this, std::bind(&QMCTest::setup, this, testRoomName)); + connect(c.data(), &Connection::connected, this, &QMCTest::setupAndRun); connect(c.data(), &Connection::loadedRoomState, this, &QMCTest::onNewRoom); // Big countdown watchdog - QTimer::singleShot(180000, this, &QMCTest::leave); + QTimer::singleShot(180000, this, &QMCTest::conclude); } -void QMCTest::setup(const QString& testRoomName) +void QMCTest::setupAndRun() { + Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid()); + Q_ASSERT(c->domain() == c->userId().section(':', 1)); cout << "Connected, server: " << c->homeserver().toDisplayString().toStdString() << endl; cout << "Access token: " << c->accessToken().toStdString() << endl; - // Setting up sync loop - c->sync(); - connect(c.data(), &Connection::syncDone, c.data(), [this,testRoomName] { - cout << "Sync complete, " - << running.size() << " tests in the air" << endl; - if (!running.isEmpty()) - c->sync(10000); - else if (targetRoom) - { - targetRoom->postPlainText(origin % ": All tests finished"); - connect(targetRoom, &Room::pendingEventMerged, this, &QMCTest::leave); - } - else - finalize(); - }); - - // Join a testroom, if provided - if (!targetRoom && !testRoomName.isEmpty()) + if (!targetRoomName.isEmpty()) { - cout << "Joining " << testRoomName.toStdString() << endl; + cout << "Joining " << targetRoomName.toStdString() << endl; running.push_back("Join room"); - auto joinJob = c->joinRoom(testRoomName); + auto joinJob = c->joinRoom(targetRoomName); connect(joinJob, &BaseJob::failure, this, - [this] { QMC_CHECK("Join room", false); finalize(); }); - // As of BaseJob::success, a Room object is not guaranteed to even - // exist; it's a mere confirmation that the server processed - // the request. - connect(c.data(), &Connection::loadedRoomState, this, - [this,testRoomName] (Room* room) { - Q_ASSERT(room); // It's a grave failure if room is nullptr here - if (room->canonicalAlias() != testRoomName) - return; // Not our room - - targetRoom = room; - QMC_CHECK("Join room", true); - startTests(); - }); - } + [this] { QMC_CHECK("Join room", false); conclude(); }); + // Connection::joinRoom() creates a Room object upon JoinRoomJob::success + // but this object is empty until the first sync is done. + connect(joinJob, &BaseJob::success, this, [this,joinJob] { + targetRoom = c->room(joinJob->roomId(), JoinState::Join); + QMC_CHECK("Join room", targetRoom != nullptr); + + run(); + }); + } else + run(); } void QMCTest::onNewRoom(Room* r) @@ -141,14 +146,64 @@ void QMCTest::onNewRoom(Room* r) }); } -void QMCTest::startTests() +void QMCTest::run() +{ + c->setLazyLoading(true); + c->syncLoop(); + connectSingleShot(c.data(), &Connection::syncDone, this, &QMCTest::doTests); + connect(c.data(), &Connection::syncDone, c.data(), [this] { + cout << "Sync complete, " << running.size() << " test(s) in the air: " + << running.join(", ").toStdString() << endl; + if (running.isEmpty()) + conclude(); + }); +} + +void QMCTest::doTests() { cout << "Starting tests" << endl; + + loadMembers(); + // Add here tests not requiring the test room + if (targetRoomName.isEmpty()) + return; + sendMessage(); + sendFile(); setTopic(); addAndRemoveTag(); sendAndRedact(); markDirectChat(); + // Add here tests with the test room +} + +void QMCTest::loadMembers() +{ + running.push_back("Loading members"); + // The dedicated qmc-test room is too small to test + // lazy-loading-then-full-loading; use #qmatrixclient:matrix.org instead. + // TODO: #264 + auto* r = c->room(QStringLiteral("!PCzUtxtOjUySxSelof:matrix.org")); + if (!r) + { + cout << "#test:matrix.org is not found in the test user's rooms" << endl; + QMC_CHECK("Loading members", false); + return; + } + // It's not exactly correct because an arbitrary server might not support + // lazy loading; but in the absence of capabilities framework we assume + // it does. + if (r->memberNames().size() >= r->joinedCount()) + { + cout << "Lazy loading doesn't seem to be enabled" << endl; + QMC_CHECK("Loading members", false); + return; + } + r->setDisplayed(); + connect(r, &Room::allMembersLoaded, [this,r] { + QMC_CHECK("Loading members", + r->memberNames().size() >= r->joinedCount()); + }); } void QMCTest::sendMessage() @@ -156,25 +211,134 @@ void QMCTest::sendMessage() running.push_back("Message sending"); cout << "Sending a message" << endl; auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); - auto& pending = targetRoom->pendingEvents(); - if (pending.empty()) + if (!validatePendingEvent(txnId)) { + cout << "Invalid pending event right after submitting" << endl; QMC_CHECK("Message sending", false); return; } - auto it = std::find_if(pending.begin(), pending.end(), - [&txnId] (const auto& e) { - return e->transactionId() == txnId; + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,txnId] (const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + QMC_CHECK("Message sending", + is<RoomMessageEvent>(*evt) && !evt->id().isEmpty() && + pendingEvents[size_t(pendingIdx)]->transactionId() + == evt->transactionId()); + return true; + }); +} + +void QMCTest::sendFile() +{ + running.push_back("File sending"); + cout << "Sending a file" << endl; + auto* tf = new QTemporaryFile; + if (!tf->open()) + { + cout << "Failed to create a temporary file" << endl; + QMC_CHECK("File sending", false); + return; + } + tf->write("Test"); + tf->close(); + // QFileInfo::fileName brings only the file name; QFile::fileName brings + // the full path + const auto tfName = QFileInfo(*tf).fileName(); + cout << "Sending file" << tfName.toStdString() << endl; + const auto txnId = targetRoom->postFile("Test file", + QUrl::fromLocalFile(tf->fileName())); + if (!validatePendingEvent(txnId)) + { + cout << "Invalid pending event right after submitting" << endl; + QMC_CHECK("File sending", false); + delete tf; + return; + } + + // FIXME: Clean away connections (connectUntil doesn't help here). + connect(targetRoom, &Room::fileTransferCompleted, this, + [this,txnId,tf,tfName] (const QString& id) { + auto fti = targetRoom->fileTransferInfo(id); + Q_ASSERT(fti.status == FileTransferInfo::Completed); + + if (id != txnId) + return; + + delete tf; + + checkFileSendingOutcome(txnId, tfName); + }); + connect(targetRoom, &Room::fileTransferFailed, this, + [this,txnId,tf] + (const QString& id, const QString& error) { + if (id != txnId) + return; + + targetRoom->postPlainText(origin % ": File upload failed: " % error); + delete tf; + + QMC_CHECK("File sending", false); + }); +} + +void QMCTest::checkFileSendingOutcome(const QString& txnId, + const QString& fileName) +{ + auto it = targetRoom->findPendingEvent(txnId); + if (it == targetRoom->pendingEvents().end()) + { + cout << "Pending file event dropped before upload completion" + << endl; + QMC_CHECK("File sending", false); + return; + } + if (it->deliveryStatus() != EventStatus::FileUploaded) + { + cout << "Pending file event status upon upload completion is " + << it->deliveryStatus() << " != FileUploaded(" + << EventStatus::FileUploaded << ')' << endl; + QMC_CHECK("File sending", false); + return; + } + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,txnId,fileName] (const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + cout << "File event " << txnId.toStdString() + << " arrived in the timeline" << endl; + visit(*evt, + [&] (const RoomMessageEvent& e) { + QMC_CHECK("File sending", + !e.id().isEmpty() && + pendingEvents[size_t(pendingIdx)] + ->transactionId() == txnId && + e.hasFileContent() && + e.content()->fileInfo()->originalName == fileName); + }, + [this] (const RoomEvent&) { + QMC_CHECK("File sending", false); }); - QMC_CHECK("Message sending", it != pending.end()); - // TODO: Wait when it actually gets sent; check that it obtained an id - // Independently, check when it shows up in the timeline. + return true; + }); } void QMCTest::setTopic() { - running.push_back("State setting test"); - running.push_back("Fake state event immunity test"); + static const char* const stateTestName = "State setting test"; + static const char* const fakeStateTestName = "Fake state event immunity test"; + running.push_back(stateTestName); + running.push_back(fakeStateTestName); auto initialTopic = targetRoom->topic(); const auto newTopic = c->generateTxnId(); @@ -183,35 +347,30 @@ void QMCTest::setTopic() targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event RoomTopicEvent(fakeTopic).contentJson()); - { - auto* context = new QObject; - connect(targetRoom, &Room::topicChanged, context, - [this,newTopic,fakeTopic,initialTopic,context] { - if (targetRoom->topic() == newTopic) - { - QMC_CHECK("State setting test", true); - // Don't reset the topic yet if the negative test still runs - if (!running.contains("Fake state event immunity test")) - targetRoom->setTopic(initialTopic); - - context->deleteLater(); - } - }); - } - - { - auto* context = new QObject; - connect(targetRoom, &Room::pendingEventAboutToMerge, context, - [this,fakeTopic,initialTopic,context] (const RoomEvent* e, int) { - if (e->contentJson().value("topic").toString() != fakeTopic) - return; // Wait on for the right event - - QMC_CHECK("Fake state event immunity test", !e->isStateEvent()); - if (!running.contains("State setting test")) + connectUntil(targetRoom, &Room::topicChanged, this, + [this,newTopic,fakeTopic,initialTopic] { + if (targetRoom->topic() == newTopic) + { + QMC_CHECK(stateTestName, true); + // Don't reset the topic yet if the negative test still runs + if (!running.contains(fakeStateTestName)) targetRoom->setTopic(initialTopic); - context->deleteLater(); - }); - } + + return true; + } + return false; + }); + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,fakeTopic,initialTopic] (const RoomEvent* e, int) { + if (e->contentJson().value("topic").toString() != fakeTopic) + return false; // Wait on for the right event + + QMC_CHECK(fakeStateTestName, !e->isStateEvent()); + if (!running.contains(fakeStateTestName)) + targetRoom->setTopic(initialTopic); + return true; + }); } void QMCTest::addAndRemoveTag() @@ -232,7 +391,7 @@ void QMCTest::addAndRemoveTag() cout << "Test tag set, removing it now" << endl; targetRoom->removeTag(TestTag); QMC_CHECK("Tagging test", !targetRoom->tags().contains(TestTag)); - QObject::disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); + disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); } }); cout << "Adding a tag" << endl; @@ -243,70 +402,53 @@ void QMCTest::sendAndRedact() { running.push_back("Redaction"); cout << "Sending a message to redact" << endl; - if (auto* job = targetRoom->connection()->sendMessage(targetRoom->id(), - RoomMessageEvent(origin % ": message to redact"))) + auto txnId = targetRoom->postPlainText(origin % ": message to redact"); + if (txnId.isEmpty()) { - connect(job, &BaseJob::success, targetRoom, [job,this] { + QMC_CHECK("Redaction", false); + return; + } + connect(targetRoom, &Room::messageSent, this, + [this,txnId] (const QString& tId, const QString& evtId) { + if (tId != txnId) + return; + cout << "Redacting the message" << endl; - targetRoom->redactEvent(job->eventId(), origin); - // Make sure to save the event id because the job is about to end. - connect(targetRoom, &Room::aboutToAddNewMessages, this, - std::bind(&QMCTest::checkRedactionOutcome, - this, job->eventId(), _1)); + targetRoom->redactEvent(evtId, origin); + + connectUntil(targetRoom, &Room::addedMessages, this, + [this,evtId] { return checkRedactionOutcome(evtId); }); }); - } else - QMC_CHECK("Redaction", false); } -void QMCTest::checkRedactionOutcome(const QString& evtIdToRedact, - RoomEventsRange events) +bool QMCTest::checkRedactionOutcome(const QString& evtIdToRedact) { - static bool checkSucceeded = false; // There are two possible (correct) outcomes: either the event comes already // redacted at the next sync, or the nearest sync completes with // the unredacted event but the next one brings redaction. - auto it = std::find_if(events.begin(), events.end(), - [=] (const RoomEventPtr& e) { - return e->id() == evtIdToRedact; - }); - if (it == events.end()) - return; // Waiting for the next sync + auto it = targetRoom->findInTimeline(evtIdToRedact); + if (it == targetRoom->timelineEdge()) + return false; // Waiting for the next sync if ((*it)->isRedacted()) { - if (checkSucceeded) - { - const auto msg = - "The redacted event came in with the sync again, ignoring"; - cout << msg << endl; - targetRoom->postPlainText(msg); - return; - } cout << "The sync brought already redacted message" << endl; QMC_CHECK("Redaction", true); - // Not disconnecting because there are other connections from this class - // to aboutToAddNewMessages - checkSucceeded = true; - return; - } - // The event is not redacted - if (checkSucceeded) - { - const auto msg = - "Warning: the redacted event came non-redacted with the sync!"; - cout << msg << endl; - targetRoom->postPlainText(msg); + } else { + cout << "Message came non-redacted with the sync, waiting for redaction" + << endl; + connectUntil(targetRoom, &Room::replacedEvent, this, + [this,evtIdToRedact] + (const RoomEvent* newEvent, const RoomEvent* oldEvent) { + if (oldEvent->id() != evtIdToRedact) + return false; + + QMC_CHECK("Redaction", newEvent->isRedacted() && + newEvent->redactionReason() == origin); + return true; + }); } - cout << "Message came non-redacted with the sync, waiting for redaction" << endl; - connect(targetRoom, &Room::replacedEvent, targetRoom, - [=] (const RoomEvent* newEvent, const RoomEvent* oldEvent) { - QMC_CHECK("Redaction", oldEvent->id() == evtIdToRedact && - newEvent->isRedacted() && - newEvent->redactionReason() == origin); - checkSucceeded = true; - disconnect(targetRoom, &Room::replacedEvent, nullptr, nullptr); - }); - + return true; } void QMCTest::markDirectChat() @@ -347,13 +489,48 @@ void QMCTest::checkDirectChatOutcome(const Connection::DirectChatsMap& added) QMC_CHECK("Direct chat test", !c->isDirectChat(targetRoom->id())); } -void QMCTest::leave() +void QMCTest::conclude() { + c->stopSync(); + auto succeededRec = QString::number(succeeded.size()) + " tests succeeded"; + if (!failed.isEmpty() || !running.isEmpty()) + succeededRec += " of " % + QString::number(succeeded.size() + failed.size() + running.size()) % + " total"; + QString plainReport = origin % ": Testing complete, " % succeededRec; + QString color = failed.isEmpty() && running.isEmpty() ? "00AA00" : "AA0000"; + QString htmlReport = origin % ": <strong><font data-mx-color='#" % color % + "' color='#" % color % "'>Testing complete</font></strong>, " % + succeededRec; + if (!failed.isEmpty()) + { + plainReport += "\nFAILED: " % failed.join(", "); + htmlReport += "<br><strong>Failed:</strong> " % failed.join(", "); + } + if (!running.isEmpty()) + { + plainReport += "\nDID NOT FINISH: " % running.join(", "); + htmlReport += + "<br><strong>Did not finish:</strong> " % running.join(", "); + } + cout << plainReport.toStdString() << endl; + if (targetRoom) { - cout << "Leaving the room" << endl; - connect(targetRoom->leaveRoom(), &BaseJob::finished, - this, &QMCTest::finalize); + // TODO: Waiting for proper futures to come so that it could be: +// targetRoom->postHtmlText(...) +// .then(this, &QMCTest::finalize); // Qt-style or +// .then([this] { finalize(); }); // STL-style + auto txnId = targetRoom->postHtmlText(plainReport, htmlReport); + connect(targetRoom, &Room::messageSent, this, + [this,txnId] (QString serverTxnId) { + if (txnId != serverTxnId) + return; + + cout << "Leaving the room" << endl; + connect(targetRoom->leaveRoom(), &BaseJob::finished, + this, &QMCTest::finalize); + }); } else finalize(); @@ -365,11 +542,6 @@ void QMCTest::finalize() c->logout(); connect(c.data(), &Connection::loggedOut, qApp, [this] { - if (!failed.isEmpty()) - cout << "FAILED: " << failed.join(", ").toStdString() << endl; - if (!running.isEmpty()) - cout << "DID NOT FINISH: " - << running.join(", ").toStdString() << endl; QCoreApplication::processEvents(); QCoreApplication::exit(failed.size() + running.size()); }); diff --git a/lib/application-service/definitions/location.cpp b/lib/application-service/definitions/location.cpp index 958a55bf..a53db8d7 100644 --- a/lib/application-service/definitions/location.cpp +++ b/lib/application-service/definitions/location.cpp @@ -6,25 +6,19 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const ThirdPartyLocation& pod) +void JsonObjectConverter<ThirdPartyLocation>::dumpTo( + QJsonObject& jo, const ThirdPartyLocation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("alias"), pod.alias); addParam<>(jo, QStringLiteral("protocol"), pod.protocol); addParam<>(jo, QStringLiteral("fields"), pod.fields); - return jo; } -ThirdPartyLocation FromJsonObject<ThirdPartyLocation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<ThirdPartyLocation>::fillFrom( + const QJsonObject& jo, ThirdPartyLocation& result) { - ThirdPartyLocation result; - result.alias = - fromJson<QString>(jo.value("alias"_ls)); - result.protocol = - fromJson<QString>(jo.value("protocol"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - - return result; + fromJson(jo.value("alias"_ls), result.alias); + fromJson(jo.value("protocol"_ls), result.protocol); + fromJson(jo.value("fields"_ls), result.fields); } diff --git a/lib/application-service/definitions/location.h b/lib/application-service/definitions/location.h index 89b48a43..5586cfc6 100644 --- a/lib/application-service/definitions/location.h +++ b/lib/application-service/definitions/location.h @@ -21,12 +21,10 @@ namespace QMatrixClient /// Information used to identify this third party location. QJsonObject fields; }; - - QJsonObject toJson(const ThirdPartyLocation& pod); - - template <> struct FromJsonObject<ThirdPartyLocation> + template <> struct JsonObjectConverter<ThirdPartyLocation> { - ThirdPartyLocation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyLocation& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyLocation& pod); }; } // namespace QMatrixClient diff --git a/lib/application-service/definitions/protocol.cpp b/lib/application-service/definitions/protocol.cpp index 04bb7dfc..2a62b15d 100644 --- a/lib/application-service/definitions/protocol.cpp +++ b/lib/application-service/definitions/protocol.cpp @@ -6,75 +6,55 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const FieldType& pod) +void JsonObjectConverter<FieldType>::dumpTo( + QJsonObject& jo, const FieldType& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("regexp"), pod.regexp); addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder); - return jo; } -FieldType FromJsonObject<FieldType>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<FieldType>::fillFrom( + const QJsonObject& jo, FieldType& result) { - FieldType result; - result.regexp = - fromJson<QString>(jo.value("regexp"_ls)); - result.placeholder = - fromJson<QString>(jo.value("placeholder"_ls)); - - return result; + fromJson(jo.value("regexp"_ls), result.regexp); + fromJson(jo.value("placeholder"_ls), result.placeholder); } -QJsonObject QMatrixClient::toJson(const ProtocolInstance& pod) +void JsonObjectConverter<ProtocolInstance>::dumpTo( + QJsonObject& jo, const ProtocolInstance& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("desc"), pod.desc); addParam<IfNotEmpty>(jo, QStringLiteral("icon"), pod.icon); addParam<>(jo, QStringLiteral("fields"), pod.fields); addParam<>(jo, QStringLiteral("network_id"), pod.networkId); - return jo; } -ProtocolInstance FromJsonObject<ProtocolInstance>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<ProtocolInstance>::fillFrom( + const QJsonObject& jo, ProtocolInstance& result) { - ProtocolInstance result; - result.desc = - fromJson<QString>(jo.value("desc"_ls)); - result.icon = - fromJson<QString>(jo.value("icon"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - result.networkId = - fromJson<QString>(jo.value("network_id"_ls)); - - return result; + fromJson(jo.value("desc"_ls), result.desc); + fromJson(jo.value("icon"_ls), result.icon); + fromJson(jo.value("fields"_ls), result.fields); + fromJson(jo.value("network_id"_ls), result.networkId); } -QJsonObject QMatrixClient::toJson(const ThirdPartyProtocol& pod) +void JsonObjectConverter<ThirdPartyProtocol>::dumpTo( + QJsonObject& jo, const ThirdPartyProtocol& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("user_fields"), pod.userFields); addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields); addParam<>(jo, QStringLiteral("icon"), pod.icon); addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes); addParam<>(jo, QStringLiteral("instances"), pod.instances); - return jo; } -ThirdPartyProtocol FromJsonObject<ThirdPartyProtocol>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<ThirdPartyProtocol>::fillFrom( + const QJsonObject& jo, ThirdPartyProtocol& result) { - ThirdPartyProtocol result; - result.userFields = - fromJson<QStringList>(jo.value("user_fields"_ls)); - result.locationFields = - fromJson<QStringList>(jo.value("location_fields"_ls)); - result.icon = - fromJson<QString>(jo.value("icon"_ls)); - result.fieldTypes = - fromJson<QHash<QString, FieldType>>(jo.value("field_types"_ls)); - result.instances = - fromJson<QVector<ProtocolInstance>>(jo.value("instances"_ls)); - - return result; + fromJson(jo.value("user_fields"_ls), result.userFields); + fromJson(jo.value("location_fields"_ls), result.locationFields); + fromJson(jo.value("icon"_ls), result.icon); + fromJson(jo.value("field_types"_ls), result.fieldTypes); + fromJson(jo.value("instances"_ls), result.instances); } diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h index 2aca7d66..0a1f9a21 100644 --- a/lib/application-service/definitions/protocol.h +++ b/lib/application-service/definitions/protocol.h @@ -25,12 +25,10 @@ namespace QMatrixClient /// An placeholder serving as a valid example of the field value. QString placeholder; }; - - QJsonObject toJson(const FieldType& pod); - - template <> struct FromJsonObject<FieldType> + template <> struct JsonObjectConverter<FieldType> { - FieldType operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const FieldType& pod); + static void fillFrom(const QJsonObject& jo, FieldType& pod); }; struct ProtocolInstance @@ -45,12 +43,10 @@ namespace QMatrixClient /// A unique identifier across all instances. QString networkId; }; - - QJsonObject toJson(const ProtocolInstance& pod); - - template <> struct FromJsonObject<ProtocolInstance> + template <> struct JsonObjectConverter<ProtocolInstance> { - ProtocolInstance operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ProtocolInstance& pod); + static void fillFrom(const QJsonObject& jo, ProtocolInstance& pod); }; struct ThirdPartyProtocol @@ -78,12 +74,10 @@ namespace QMatrixClient /// same application service. QVector<ProtocolInstance> instances; }; - - QJsonObject toJson(const ThirdPartyProtocol& pod); - - template <> struct FromJsonObject<ThirdPartyProtocol> + template <> struct JsonObjectConverter<ThirdPartyProtocol> { - ThirdPartyProtocol operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyProtocol& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyProtocol& pod); }; } // namespace QMatrixClient diff --git a/lib/application-service/definitions/user.cpp b/lib/application-service/definitions/user.cpp index ca334236..8ba92321 100644 --- a/lib/application-service/definitions/user.cpp +++ b/lib/application-service/definitions/user.cpp @@ -6,25 +6,19 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const ThirdPartyUser& pod) +void JsonObjectConverter<ThirdPartyUser>::dumpTo( + QJsonObject& jo, const ThirdPartyUser& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("userid"), pod.userid); addParam<>(jo, QStringLiteral("protocol"), pod.protocol); addParam<>(jo, QStringLiteral("fields"), pod.fields); - return jo; } -ThirdPartyUser FromJsonObject<ThirdPartyUser>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<ThirdPartyUser>::fillFrom( + const QJsonObject& jo, ThirdPartyUser& result) { - ThirdPartyUser result; - result.userid = - fromJson<QString>(jo.value("userid"_ls)); - result.protocol = - fromJson<QString>(jo.value("protocol"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - - return result; + fromJson(jo.value("userid"_ls), result.userid); + fromJson(jo.value("protocol"_ls), result.protocol); + fromJson(jo.value("fields"_ls), result.fields); } diff --git a/lib/application-service/definitions/user.h b/lib/application-service/definitions/user.h index 79ca7789..062d2cac 100644 --- a/lib/application-service/definitions/user.h +++ b/lib/application-service/definitions/user.h @@ -21,12 +21,10 @@ namespace QMatrixClient /// Information used to identify this third party location. QJsonObject fields; }; - - QJsonObject toJson(const ThirdPartyUser& pod); - - template <> struct FromJsonObject<ThirdPartyUser> + template <> struct JsonObjectConverter<ThirdPartyUser> { - ThirdPartyUser operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyUser& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyUser& pod); }; } // namespace QMatrixClient diff --git a/lib/avatar.cpp b/lib/avatar.cpp index b8e1096d..9279ef9d 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -190,18 +190,8 @@ bool Avatar::Private::checkUrl(const QUrl& url) const return _imageSource != Banned; } -QString cacheLocation() { - const auto cachePath = - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - + "/avatar/"; - QDir dir; - if (!dir.exists(cachePath)) - dir.mkpath(cachePath); - return cachePath; -} - QString Avatar::Private::localFile() const { - static const auto cachePath = cacheLocation(); + static const auto cachePath = cacheLocation(QStringLiteral("avatars")); return cachePath % _url.authority() % '_' % _url.fileName() % ".png"; } diff --git a/lib/connection.cpp b/lib/connection.cpp index 6bdedf26..ac69228b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -24,6 +24,7 @@ #include "room.h" #include "settings.h" #include "csapi/login.h" +#include "csapi/capabilities.h" #include "csapi/logout.h" #include "csapi/receipts.h" #include "csapi/leaving.h" @@ -39,11 +40,11 @@ #include <QtNetwork/QDnsLookup> #include <QtCore/QFile> #include <QtCore/QDir> -#include <QtCore/QFileInfo> #include <QtCore/QStandardPaths> #include <QtCore/QStringBuilder> #include <QtCore/QElapsedTimer> #include <QtCore/QRegularExpression> +#include <QtCore/QMimeDatabase> #include <QtCore/QCoreApplication> using namespace QMatrixClient; @@ -65,10 +66,6 @@ HashT erase_if(HashT& hashMap, Pred pred) return removals; } -#ifndef TRIM_RAW_DATA -#define TRIM_RAW_DATA 65535 -#endif - class Connection::Private { public: @@ -76,8 +73,7 @@ class Connection::Private : data(move(connection)) { } Q_DISABLE_COPY(Private) - Private(Private&&) = delete; - Private operator=(Private&&) = delete; + DISABLE_MOVE(Private) Connection* q = nullptr; std::unique_ptr<ConnectionData> data; @@ -86,24 +82,34 @@ class Connection::Private // separately so we should, e.g., keep objects for Invite and // Leave state of the same room. QHash<QPair<QString, bool>, Room*> roomMap; + // Mapping from aliases to room ids, as per the last sync + QHash<QString, QString> roomAliasMap; QVector<QString> roomIdsToForget; QVector<Room*> firstTimeRooms; + QVector<QString> pendingStateRoomIds; QMap<QString, User*> userMap; DirectChatsMap directChats; DirectChatUsersMap directChatUsers; + // The below two variables track local changes between sync completions. + // See also: https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events + DirectChatsMap dcLocalAdditions; + DirectChatsMap dcLocalRemovals; std::unordered_map<QString, EventPtr> accountData; QString userId; + int syncLoopTimeout = -1; + + GetCapabilitiesJob* capabilitiesJob = nullptr; + GetCapabilitiesJob::Capabilities capabilities; SyncJob* syncJob = nullptr; bool cacheState = true; bool cacheToBinary = SettingsGroup("libqmatrixclient") .value("cache_type").toString() != "json"; + bool lazyLoading = false; void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); - void broadcastDirectChatUpdates(const DirectChatsMap& additions, - const DirectChatsMap& removals); template <typename EventT> EventT* unpackAccountData() const @@ -228,11 +234,15 @@ void Connection::doConnectToServer(const QString& user, const QString& password, }); connect(loginJob, &BaseJob::failure, this, [this, loginJob] { - emit loginError(loginJob->errorString(), - loginJob->rawData(TRIM_RAW_DATA)); + emit loginError(loginJob->errorString(), loginJob->rawDataSample()); }); } +void Connection::syncLoopIteration() +{ + sync(d->syncLoopTimeout); +} + void Connection::connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId) @@ -241,6 +251,38 @@ void Connection::connectWithToken(const QString& userId, [=] { d->connectWithToken(userId, accessToken, deviceId); }); } +void Connection::reloadCapabilities() +{ + d->capabilitiesJob = callApi<GetCapabilitiesJob>(BackgroundRequest); + connect(d->capabilitiesJob, &BaseJob::finished, this, [this] { + if (d->capabilitiesJob->error() == BaseJob::Success) + d->capabilities = d->capabilitiesJob->capabilities(); + else if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError) + qCDebug(MAIN) << "Server doesn't support /capabilities"; + + if (d->capabilities.roomVersions.omitted()) + { + qCWarning(MAIN) << "Pinning supported room version to 1"; + d->capabilities.roomVersions = { "1", {{ "1", "stable" }} }; + } else { + qCDebug(MAIN) << "Room versions:" + << defaultRoomVersion() << "is default, full list:" + << availableRoomVersions(); + } + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + emit capabilitiesLoaded(); + for (auto* r: d->roomMap) + r->checkVersion(); + }); +} + +bool Connection::loadingCapabilities() const +{ + // (Ab)use the fact that room versions cannot be omitted after + // the capabilities have been loaded (see reloadCapabilities() above). + return d->capabilities.roomVersions.omitted(); +} + void Connection::Private::connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId) @@ -253,7 +295,7 @@ void Connection::Private::connectWithToken(const QString& user, << "by user" << userId << "from device" << deviceId; emit q->stateChanged(); emit q->connected(); - + q->reloadCapabilities(); } void Connection::checkAndConnect(const QString& userId, @@ -280,11 +322,14 @@ void Connection::checkAndConnect(const QString& userId, void Connection::logout() { auto job = callApi<LogoutJob>(); - connect( job, &LogoutJob::success, this, [this] { - stopSync(); - d->data->setToken({}); - emit stateChanged(); - emit loggedOut(); + connect( job, &LogoutJob::finished, this, [job,this] { + if (job->status().good() || job->error() == BaseJob::ContentAccessError) + { + stopSync(); + d->data->setToken({}); + emit stateChanged(); + emit loggedOut(); + } }); } @@ -293,11 +338,11 @@ void Connection::sync(int timeout) if (d->syncJob) return; - // Raw string: http://en.cppreference.com/w/cpp/language/string_literal - const auto filter = - QStringLiteral(R"({"room": { "timeline": { "limit": 100 } } })"); + Filter filter; + filter.room->timeline->limit = 100; + filter.room->state->lazyLoadMembers = d->lazyLoading; auto job = d->syncJob = callApi<SyncJob>(BackgroundRequest, - d->data->lastEvent(), filter, timeout); + d->data->lastEvent(), filter, timeout); connect( job, &SyncJob::success, this, [this, job] { onSyncSuccess(job->takeData()); d->syncJob = nullptr; @@ -306,7 +351,7 @@ void Connection::sync(int timeout) connect( job, &SyncJob::retryScheduled, this, [this,job] (int retriesTaken, int nextInMilliseconds) { - emit networkError(job->errorString(), job->rawData(TRIM_RAW_DATA), + emit networkError(job->errorString(), job->rawDataSample(), retriesTaken, nextInMilliseconds); }); connect( job, &SyncJob::failure, this, [this, job] { @@ -315,14 +360,35 @@ void Connection::sync(int timeout) { qCWarning(SYNCJOB) << "Sync job failed with ContentAccessError - login expired?"; - emit loginError(job->errorString(), job->rawData(TRIM_RAW_DATA)); + emit loginError(job->errorString(), job->rawDataSample()); } else - emit syncError(job->errorString(), job->rawData(TRIM_RAW_DATA)); + emit syncError(job->errorString(), job->rawDataSample()); }); } -void Connection::onSyncSuccess(SyncData &&data) { +void Connection::syncLoop(int timeout) +{ + d->syncLoopTimeout = timeout; + connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration); + syncLoopIteration(); // initial sync to start the loop +} + +QJsonObject toJson(const Connection::DirectChatsMap& directChats) +{ + QJsonObject json; + for (auto it = directChats.begin(); it != directChats.end();) + { + QJsonArray roomIds; + const auto* user = it.key(); + for (; it != directChats.end() && it.key() == user; ++it) + roomIds.append(*it); + json.insert(user->id(), roomIds); + } + return json; +} + +void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { d->data->setLastEvent(data.nextBatch()); for (auto&& roomData: data.takeRoomData()) { @@ -343,80 +409,123 @@ void Connection::onSyncSuccess(SyncData &&data) { } if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) { - r->updateData(std::move(roomData)); + d->pendingStateRoomIds.removeOne(roomData.roomId); + r->updateData(std::move(roomData), fromCache); if (d->firstTimeRooms.removeOne(r)) + { emit loadedRoomState(r); + if (!d->capabilities.roomVersions.omitted()) + r->checkVersion(); + // Otherwise, the version will be checked in reloadCapabilities() + } } + // Let UI update itself after updating each room QCoreApplication::processEvents(); } - for (auto&& accountEvent: data.takeAccountData()) + // After running this loop, the account data events not saved in + // d->accountData (see the end of the loop body) are auto-cleaned away + for (auto& eventPtr : data.takeAccountData()) { - if (is<DirectChatEvent>(*accountEvent)) - { - const auto usersToDCs = ptrCast<DirectChatEvent>(move(accountEvent)) - ->usersToDirectChats(); - DirectChatsMap removals = - erase_if(d->directChats, [&usersToDCs] (auto it) { - return !usersToDCs.contains(it.key()->id(), it.value()); - }); - erase_if(d->directChatUsers, [&usersToDCs] (auto it) { - return !usersToDCs.contains(it.value()->id(), it.key()); - }); - if (MAIN().isDebugEnabled()) - for (auto it = removals.begin(); it != removals.end(); ++it) - qCDebug(MAIN) << it.value() - << "is no more a direct chat with" << it.key()->id(); - - DirectChatsMap additions; - for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) - { - if (auto* u = user(it.key())) - { - if (!d->directChats.contains(u, it.value())) - { - Q_ASSERT(!d->directChatUsers.contains(it.value(), u)); - additions.insert(u, it.value()); - d->directChats.insert(u, it.value()); - d->directChatUsers.insert(it.value(), u); - qCDebug(MAIN) << "Marked room" << it.value() + visit(*eventPtr, + [this](const DirectChatEvent& dce) { + // See https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events + const auto& usersToDCs = dce.usersToDirectChats(); + DirectChatsMap remoteRemovals = + erase_if(d->directChats, [&usersToDCs, this](auto it) { + return !(usersToDCs.contains(it.key()->id(), it.value()) + || d->dcLocalAdditions.contains(it.key(), + it.value())); + }); + erase_if(d->directChatUsers, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.value(), it.key()); + }); + // Remove from dcLocalRemovals what the server already has. + erase_if(d->dcLocalRemovals, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.key(), it.value()); + }); + if (MAIN().isDebugEnabled()) + for (auto it = remoteRemovals.begin(); + it != remoteRemovals.end(); ++it) { + qCDebug(MAIN) + << it.value() << "is no more a direct chat with" + << it.key()->id(); + } + + DirectChatsMap remoteAdditions; + for (auto it = usersToDCs.begin(); it != usersToDCs.end(); + ++it) { + if (auto* u = user(it.key())) { + if (!d->directChats.contains(u, it.value()) + && !d->dcLocalRemovals.contains(u, it.value())) + { + Q_ASSERT( + !d->directChatUsers.contains(it.value(), u)); + remoteAdditions.insert(u, it.value()); + d->directChats.insert(u, it.value()); + d->directChatUsers.insert(it.value(), u); + qCDebug(MAIN) + << "Marked room" << it.value() << "as a direct chat with" << u->id(); - } - } else - qCWarning(MAIN) - << "Couldn't get a user object for" << it.key(); - } - if (!additions.isEmpty() || !removals.isEmpty()) - emit directChatsListChanged(additions, removals); - - continue; - } - if (is<IgnoredUsersEvent>(*accountEvent)) - qCDebug(MAIN) << "Users ignored by" << d->userId << "updated:" - << QStringList::fromSet(ignoredUsers()).join(','); - - auto& currentData = d->accountData[accountEvent->matrixType()]; - // A polymorphic event-specific comparison might be a bit more - // efficient; maaybe do it another day - if (!currentData || - currentData->contentJson() != accountEvent->contentJson()) - { - currentData = std::move(accountEvent); - qCDebug(MAIN) << "Updated account data of type" - << currentData->matrixType(); - emit accountDataChanged(currentData->matrixType()); - } + } + } else + qCWarning(MAIN) << "Couldn't get a user object for" + << it.key(); + } + // Remove from dcLocalAdditions what the server already has. + erase_if(d->dcLocalAdditions, [&remoteAdditions](auto it) { + return remoteAdditions.contains(it.key(), it.value()); + }); + if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) + emit directChatsListChanged(remoteAdditions, remoteRemovals); + }, + // catch-all, passing eventPtr for a possible take-over + [this, &eventPtr](const Event& accountEvent) { + if (is<IgnoredUsersEvent>(accountEvent)) + qCDebug(MAIN) + << "Users ignored by" << d->userId << "updated:" + << QStringList::fromSet(ignoredUsers()).join(','); + + auto& currentData = d->accountData[accountEvent.matrixType()]; + // A polymorphic event-specific comparison might be a bit more + // efficient; maaybe do it another day + if (!currentData + || currentData->contentJson() + != accountEvent.contentJson()) { + currentData = std::move(eventPtr); + qCDebug(MAIN) << "Updated account data of type" + << currentData->matrixType(); + emit accountDataChanged(currentData->matrixType()); + } + }); + } + if (!d->dcLocalAdditions.isEmpty() || !d->dcLocalRemovals.isEmpty()) { + qDebug(MAIN) << "Sending updated direct chats to the server:" + << d->dcLocalRemovals.size() << "removal(s)," + << d->dcLocalAdditions.size() << "addition(s)"; + callApi<SetAccountDataJob>(d->userId, QStringLiteral("m.direct"), + toJson(d->directChats)); + d->dcLocalAdditions.clear(); + d->dcLocalRemovals.clear(); } } void Connection::stopSync() { - if (d->syncJob) + // If there's a sync loop, break it + disconnect(this, &Connection::syncDone, + this, &Connection::syncLoopIteration); + if (d->syncJob) // If there's an ongoing sync job, stop it too { d->syncJob->abandon(); d->syncJob = nullptr; } } +QString Connection::nextBatchToken() const +{ + return d->data->lastEvent(); +} + PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const { return callApi<PostReceiptJob>(room->id(), "m.read", event->id()); @@ -426,14 +535,32 @@ JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { auto job = callApi<JoinRoomJob>(roomAlias, serverNames); + // Upon completion, ensure a room object in Join state is created but only + // if it's not already there due to a sync completing earlier. connect(job, &JoinRoomJob::success, - this, [this, job] { provideRoom(job->roomId(), JoinState::Join); }); + this, [this, job] { provideRoom(job->roomId()); }); return job; } -void Connection::leaveRoom(Room* room) +LeaveRoomJob* Connection::leaveRoom(Room* room) { - callApi<LeaveRoomJob>(room->id()); + const auto& roomId = room->id(); + const auto job = callApi<LeaveRoomJob>(roomId); + if (room->joinState() == JoinState::Invite) + { + // Workaround matrix-org/synapse#2181 - if the room is in invite state + // the invite may have been cancelled but Synapse didn't send it in + // `/sync`. See also #273 for the discussion in the library context. + d->pendingStateRoomIds.push_back(roomId); + connect(job, &LeaveRoomJob::success, this, [this,roomId] { + if (d->pendingStateRoomIds.removeOne(roomId)) + { + qCDebug(MAIN) << "Forcing the room to Leave status"; + provideRoom(roomId, JoinState::Leave); + } + }); + } + return job; } inline auto splitMediaId(const QString& mediaId) @@ -466,13 +593,21 @@ MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, } UploadContentJob* Connection::uploadContent(QIODevice* contentSource, - const QString& filename, const QString& contentType) const + const QString& filename, const QString& overrideContentType) const { + auto contentType = overrideContentType; + if (contentType.isEmpty()) + { + contentType = + QMimeDatabase().mimeTypeForFileNameAndData(filename, contentSource) + .name(); + contentSource->open(QIODevice::ReadOnly); + } return callApi<UploadContentJob>(contentSource, filename, contentType); } UploadContentJob* Connection::uploadFile(const QString& fileName, - const QString& contentType) + const QString& overrideContentType) { auto sourceFile = new QFile(fileName); if (!sourceFile->open(QIODevice::ReadOnly)) @@ -482,7 +617,7 @@ UploadContentJob* Connection::uploadFile(const QString& fileName, return nullptr; } return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(), - contentType); + overrideContentType); } GetContentJob* Connection::getContent(const QString& mediaId) const @@ -508,7 +643,8 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, CreateRoomJob* Connection::createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, - QStringList invites, const QString& presetName, bool isDirect, + QStringList invites, const QString& presetName, + const QString& roomVersion, bool isDirect, const QVector<CreateRoomJob::StateEvent>& initialState, const QVector<CreateRoomJob::Invite3pid>& invite3pids, const QJsonObject& creationContent) @@ -517,10 +653,19 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility, auto job = callApi<CreateRoomJob>( visibility == PublishRoom ? QStringLiteral("public") : QStringLiteral("private"), - alias, name, topic, invites, invite3pids, QString(/*TODO: #233*/), + alias, name, topic, invites, invite3pids, roomVersion, creationContent, initialState, presetName, isDirect); - connect(job, &BaseJob::success, this, [this,job] { - emit createdRoom(provideRoom(job->roomId(), JoinState::Join)); + connect(job, &BaseJob::success, this, [this,job,invites,isDirect] { + auto* room = provideRoom(job->roomId(), JoinState::Join); + if (!room) + { + Q_ASSERT_X(room, "Connection::createRoom", "Failed to create a room"); + return; + } + emit createdRoom(room); + if (isDirect) + for (const auto& i: invites) + addToDirectChats(room, user(i)); }); return job; } @@ -556,8 +701,8 @@ void Connection::doInDirectChat(User* u, { Q_ASSERT(u); const auto& userId = u->id(); - // There can be more than one DC; find the first valid, and delete invalid - // (left/forgotten) ones along the way. + // There can be more than one DC; find the first valid (existing and + // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; for (auto it = d->directChats.find(u); it != d->directChats.end() && it.key() == u; ++it) @@ -567,7 +712,7 @@ void Connection::doInDirectChat(User* u, { Q_ASSERT(r->id() == roomId); // A direct chat with yourself should only involve yourself :) - if (userId == d->userId && r->memberCount() > 1) + if (userId == d->userId && r->totalMemberCount() > 1) continue; qCDebug(MAIN) << "Requested direct chat with" << userId << "is already available as" << r->id(); @@ -594,6 +739,8 @@ void Connection::doInDirectChat(User* u, << roomId << "is not valid and will be discarded"; // Postpone actual deletion until we finish iterating d->directChats. removals.insert(it.key(), it.value()); + // Add to the list of updates to send to the server upon the next sync. + d->dcLocalRemovals.insert(it.key(), it.value()); } if (!removals.isEmpty()) { @@ -603,7 +750,7 @@ void Connection::doInDirectChat(User* u, d->directChatUsers.remove(it.value(), const_cast<User*>(it.key())); // FIXME } - d->broadcastDirectChatUpdates({}, removals); + emit directChatsListChanged({}, removals); } auto j = createDirectChat(userId); @@ -618,8 +765,8 @@ void Connection::doInDirectChat(User* u, CreateRoomJob* Connection::createDirectChat(const QString& userId, const QString& topic, const QString& name) { - return createRoom(UnpublishRoom, "", name, topic, {userId}, - "trusted_private_chat", true); + return createRoom(UnpublishRoom, {}, name, topic, {userId}, + QStringLiteral("trusted_private_chat"), {}, true); } ForgetRoomJob* Connection::forgetRoom(const QString& id) @@ -698,6 +845,11 @@ QUrl Connection::homeserver() const return d->data->baseUrl(); } +QString Connection::domain() const +{ + return d->userId.section(':', 1); +} + Room* Connection::room(const QString& roomId, JoinStates states) const { Room* room = d->roomMap.value({roomId, false}, nullptr); @@ -716,6 +868,41 @@ Room* Connection::room(const QString& roomId, JoinStates states) const return nullptr; } +Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const +{ + const auto id = d->roomAliasMap.value(roomAlias); + if (!id.isEmpty()) + return room(id, states); + qCWarning(MAIN) << "Room for alias" << roomAlias + << "is not found under account" << userId(); + return nullptr; +} + +void Connection::updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases) +{ + for (const auto& a: previousRoomAliases) + if (d->roomAliasMap.remove(a) == 0) + qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; + + for (const auto& a: roomAliases) + { + auto& mappedId = d->roomAliasMap[a]; + if (!mappedId.isEmpty()) + { + if (mappedId == roomId) + qCDebug(MAIN) << "Alias" << a << "is already mapped to room" + << roomId; + else + qCWarning(MAIN) << "Alias" << a + << "will be force-remapped from room" + << mappedId << "to" << roomId; + } + mappedId = roomId; + } +} + Room* Connection::invitation(const QString& roomId) const { return d->roomMap.value({roomId, true}, nullptr); @@ -825,7 +1012,8 @@ QHash<QString, QVector<Room*>> Connection::tagsToRooms() const QHash<QString, QVector<Room*>> result; for (auto* r: qAsConst(d->roomMap)) { - for (const auto& tagName: r->tagNames()) + const auto& tagNames = r->tagNames(); + for (const auto& tagName: tagNames) result[tagName].push_back(r); } for (auto it = result.begin(); it != result.end(); ++it) @@ -840,9 +1028,12 @@ QStringList Connection::tagNames() const { QStringList tags ({FavouriteTag}); for (auto* r: qAsConst(d->roomMap)) - for (const auto& tag: r->tagNames()) + { + const auto& tagNames = r->tagNames(); + for (const auto& tag: tagNames) if (tag != LowPriorityTag && !tags.contains(tag)) tags.push_back(tag); + } tags.push_back(LowPriorityTag); return tags; } @@ -860,28 +1051,6 @@ Connection::DirectChatsMap Connection::directChats() const return d->directChats; } -QJsonObject toJson(const Connection::DirectChatsMap& directChats) -{ - QJsonObject json; - for (auto it = directChats.begin(); it != directChats.end();) - { - QJsonArray roomIds; - const auto* user = it.key(); - for (; it != directChats.end() && it.key() == user; ++it) - roomIds.append(*it); - json.insert(user->id(), roomIds); - } - return json; -} - -void Connection::Private::broadcastDirectChatUpdates(const DirectChatsMap& additions, - const DirectChatsMap& removals) -{ - q->callApi<SetAccountDataJob>(userId, QStringLiteral("m.direct"), - toJson(directChats)); - emit q->directChatsListChanged(additions, removals); -} - void Connection::addToDirectChats(const Room* room, User* user) { Q_ASSERT(room != nullptr && user != nullptr); @@ -890,8 +1059,8 @@ void Connection::addToDirectChats(const Room* room, User* user) Q_ASSERT(!d->directChatUsers.contains(room->id(), user)); d->directChats.insert(user, room->id()); d->directChatUsers.insert(room->id(), user); - DirectChatsMap additions { { user, room->id() } }; - d->broadcastDirectChatUpdates(additions, {}); + d->dcLocalAdditions.insert(user, room->id()); + emit directChatsListChanged({ { user, room->id() } }, {}); } void Connection::removeFromDirectChats(const QString& roomId, User* user) @@ -904,15 +1073,17 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user) DirectChatsMap removals; if (user != nullptr) { - removals.insert(user, roomId); d->directChats.remove(user, roomId); d->directChatUsers.remove(roomId, user); + removals.insert(user, roomId); + d->dcLocalRemovals.insert(user, roomId); } else { removals = erase_if(d->directChats, [&roomId] (auto it) { return it.value() == roomId; }); d->directChatUsers.remove(roomId); + d->dcLocalRemovals += removals; } - d->broadcastDirectChatUpdates({}, removals); + emit directChatsListChanged({}, removals); } bool Connection::isDirectChat(const QString& roomId) const @@ -972,11 +1143,12 @@ const ConnectionData* Connection::connectionData() const return d->data.get(); } -Room* Connection::provideRoom(const QString& id, JoinState joinState) +Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState) { // TODO: This whole function is a strong case for a RoomManager class. Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); + // If joinState.omitted(), all joinState == comparisons below are false. const auto roomKey = qMakePair(id, joinState == JoinState::Invite); auto* room = d->roomMap.value(roomKey, nullptr); if (room) @@ -986,10 +1158,19 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) // and emit a signal. For Invite and Join, there's no such problem. if (room->joinState() == joinState && joinState != JoinState::Leave) return room; + } else if (joinState.omitted()) + { + // No Join and Leave, maybe Invite? + room = d->roomMap.value({id, true}, nullptr); + if (room) + return room; + // No Invite either, setup a new room object below } - else + + if (!room) { - room = roomFactory()(this, id, joinState); + room = roomFactory()(this, id, + joinState.omitted() ? JoinState::Join : joinState.value()); if (!room) { qCCritical(MAIN) << "Failed to create a room" << id; @@ -1001,6 +1182,9 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) this, &Connection::aboutToDeleteRoom); emit newRoom(room); } + if (joinState.omitted()) + return room; + if (joinState == JoinState::Invite) { // prev is either Leave or nullptr @@ -1009,7 +1193,7 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) } else { - room->setJoinState(joinState); + room->setJoinState(joinState.value()); // Preempt the Invite room (if any) with a room in Join/Leave state. auto* prevInvite = d->roomMap.take({id, true}); if (joinState == JoinState::Join) @@ -1018,6 +1202,9 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) emit leftRoom(room, prevInvite); if (prevInvite) { + const auto dcUsers = prevInvite->directChatUsers(); + for (auto* u: dcUsers) + addToDirectChats(room, u); qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); emit prevInvite->beforeDestruction(prevInvite); prevInvite->deleteLater(); @@ -1064,56 +1251,64 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 8; -static constexpr int CACHE_VERSION_MINOR = 0; +void Connection::saveRoomState(Room* r) const +{ + Q_ASSERT(r); + if (!d->cacheState) + return; -void Connection::saveState(const QUrl &toFile) const + QFile outRoomFile { stateCachePath() % SyncData::fileNameForRoom(r->id()) }; + if (outRoomFile.open(QFile::WriteOnly)) + { + QJsonDocument json { r->toJson() }; + auto data = d->cacheToBinary ? json.toBinaryData() + : json.toJson(QJsonDocument::Compact); + outRoomFile.write(data.data(), data.size()); + qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); + } else { + qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() + << ":" << outRoomFile.errorString(); + } +} + +void Connection::saveState() const { if (!d->cacheState) return; QElapsedTimer et; et.start(); - QFileInfo stateFile { - toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile() - }; - if (!stateFile.dir().exists()) - stateFile.dir().mkpath("."); - - QFile outfile { stateFile.absoluteFilePath() }; - if (!outfile.open(QFile::WriteOnly)) + QFile outFile { stateCachePath() % "state.json" }; + if (!outFile.open(QFile::WriteOnly)) { - qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath() - << ":" << outfile.errorString(); + qCWarning(MAIN) << "Error opening" << outFile.fileName() + << ":" << outFile.errorString(); qCWarning(MAIN) << "Caching the rooms state disabled"; d->cacheState = false; return; } - QJsonObject rootObj; + QJsonObject rootObj { + { QStringLiteral("cache_version"), QJsonObject { + { QStringLiteral("major"), SyncData::cacheVersion().first }, + { QStringLiteral("minor"), SyncData::cacheVersion().second } + }}}; { QJsonObject rooms; QJsonObject inviteRooms; - for (const auto* i : roomMap()) // Pass on rooms in Leave state - { - if (i->joinState() == JoinState::Invite) - inviteRooms.insert(i->id(), i->toJson()); - else - rooms.insert(i->id(), i->toJson()); - QElapsedTimer et1; et1.start(); - QCoreApplication::processEvents(); - if (et1.elapsed() > 1) - qCDebug(PROFILER) << "processEvents() borrowed" << et1; - } + const auto& rs = roomMap(); // Pass on rooms in Leave state + for (const auto* i : rs) + (i->joinState() == JoinState::Invite ? inviteRooms : rooms) + .insert(i->id(), QJsonValue::Null); QJsonObject roomObj; if (!rooms.isEmpty()) - roomObj.insert("join", rooms); + roomObj.insert(QStringLiteral("join"), rooms); if (!inviteRooms.isEmpty()) - roomObj.insert("invite", inviteRooms); + roomObj.insert(QStringLiteral("invite"), inviteRooms); - rootObj.insert("next_batch", d->data->lastEvent()); - rootObj.insert("rooms", roomObj); + rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent()); + rootObj.insert(QStringLiteral("rooms"), roomObj); } { QJsonArray accountDataEvents { @@ -1123,67 +1318,39 @@ void Connection::saveState(const QUrl &toFile) const accountDataEvents.append( basicEventJson(e.first, e.second->contentJson())); - rootObj.insert("account_data", + rootObj.insert(QStringLiteral("account_data"), QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } - QJsonObject versionObj; - versionObj.insert("major", CACHE_VERSION_MAJOR); - versionObj.insert("minor", CACHE_VERSION_MINOR); - rootObj.insert("cache_version", versionObj); - QJsonDocument json { rootObj }; auto data = d->cacheToBinary ? json.toBinaryData() : json.toJson(QJsonDocument::Compact); qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; - outfile.write(data.data(), data.size()); - qCDebug(MAIN) << "State cache saved to" << outfile.fileName(); + outFile.write(data.data(), data.size()); + qCDebug(MAIN) << "State cache saved to" << outFile.fileName(); } -void Connection::loadState(const QUrl &fromFile) +void Connection::loadState() { if (!d->cacheState) return; QElapsedTimer et; et.start(); - QFile file { - fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() - }; - if (!file.exists()) - { - qCDebug(MAIN) << "No state cache file found"; - return; - } - if(!file.open(QFile::ReadOnly)) - { - qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read"; - return; - } - QByteArray data = file.readAll(); - auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : - QJsonDocument::fromJson(data); - if (jsonDoc.isNull()) - { - qCWarning(MAIN) << "Cache file broken, discarding"; + SyncData sync { stateCachePath() % "state.json" }; + if (sync.nextBatch().isEmpty()) // No token means no cache by definition return; - } - auto actualCacheVersionMajor = - jsonDoc.object() - .value("cache_version").toObject() - .value("major").toInt(); - if (actualCacheVersionMajor < CACHE_VERSION_MAJOR) + + if (!sync.unresolvedRooms().isEmpty()) { - qCWarning(MAIN) - << "Major version of the cache file is" << actualCacheVersionMajor - << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache"; + qCWarning(MAIN) << "State cache incomplete, discarding"; return; } - - SyncData sync; - sync.parseJson(jsonDoc); - onSyncSuccess(std::move(sync)); + // TODO: to handle load failures, instead of the above block: + // 1. Do initial sync on failed rooms without saving the nextBatch token + // 2. Do the sync across all rooms as normal + onSyncSuccess(std::move(sync), true); qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; } @@ -1191,8 +1358,7 @@ QString Connection::stateCachePath() const { auto safeUserId = userId(); safeUserId.replace(':', '_'); - return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - % '/' % safeUserId % "_state.json"; + return cacheLocation(safeUserId); } bool Connection::cacheState() const @@ -1209,11 +1375,70 @@ void Connection::setCacheState(bool newValue) } } +bool QMatrixClient::Connection::lazyLoading() const +{ + return d->lazyLoading; +} + +void QMatrixClient::Connection::setLazyLoading(bool newValue) +{ + if (d->lazyLoading != newValue) + { + d->lazyLoading = newValue; + emit lazyLoadingChanged(); + } +} + void Connection::getTurnServers() { - auto job = callApi<GetTurnServerJob>(); - connect( job, &GetTurnServerJob::success, [=] { - emit turnServersChanged(job->data()); - }); + auto job = callApi<GetTurnServerJob>(); + connect(job, &GetTurnServerJob::success, + this, [=] { emit turnServersChanged(job->data()); }); +} + +const QString Connection::SupportedRoomVersion::StableTag = + QStringLiteral("stable"); + +QString Connection::defaultRoomVersion() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + return d->capabilities.roomVersions->defaultVersion; +} +QStringList Connection::stableRoomVersions() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + QStringList l; + const auto& allVersions = d->capabilities.roomVersions->available; + for (auto it = allVersions.begin(); it != allVersions.end(); ++it) + if (it.value() == SupportedRoomVersion::StableTag) + l.push_back(it.key()); + return l; +} + +inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, + const Connection::SupportedRoomVersion& v2) +{ + bool ok1 = false, ok2 = false; + const auto vNum1 = v1.id.toFloat(&ok1); + const auto vNum2 = v2.id.toFloat(&ok2); + return ok1 && ok2 ? vNum1 < vNum2 : v1.id < v2.id; +} + +QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + QVector<SupportedRoomVersion> result; + result.reserve(d->capabilities.roomVersions->available.size()); + for (auto it = d->capabilities.roomVersions->available.begin(); + it != d->capabilities.roomVersions->available.end(); ++it) + result.push_back({ it.key(), it.value() }); + // Put stable versions over unstable; within each group, + // sort numeric versions as numbers, the rest as strings. + const auto mid = std::partition(result.begin(), result.end(), + std::mem_fn(&SupportedRoomVersion::isStable)); + std::sort(result.begin(), mid, roomVersionLess); + std::sort(mid, result.end(), roomVersionLess); + + return result; } diff --git a/lib/connection.h b/lib/connection.h index b06fb143..ea5be51a 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -21,6 +21,7 @@ #include "csapi/create_room.h" #include "joinstate.h" #include "events/accountdataevents.h" +#include "qt_connection_util.h" #include <QtCore/QObject> #include <QtCore/QUrl> @@ -48,26 +49,7 @@ namespace QMatrixClient class DownloadFileJob; class SendToDeviceJob; class SendMessageJob; - - /** Create a single-shot connection that triggers on the signal and - * then self-disconnects - * - * Only supports DirectConnection type. - */ - template <typename SenderT1, typename SignalT, - typename ReceiverT2, typename SlotT> - inline auto connectSingleShot(SenderT1* sender, SignalT signal, - ReceiverT2* receiver, SlotT slot) - { - QMetaObject::Connection connection; - connection = QObject::connect(sender, signal, receiver, slot, - Qt::DirectConnection); - Q_ASSERT(connection); - QObject::connect(sender, signal, receiver, - [connection] { QObject::disconnect(connection); }, - Qt::DirectConnection); - return connection; - } + class LeaveRoomJob; class Connection; @@ -120,8 +102,12 @@ namespace QMatrixClient Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) + Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(QString domain READ domain NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) + Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) + public: // Room ids, rather than room pointers, are used in the direct chat // map types because the library keeps Invite rooms separate from @@ -246,8 +232,8 @@ namespace QMatrixClient */ void addToIgnoredUsers(const User* user); - /** Remove the user from the ignore list - * Similar to adding, the change signal is emitted synchronously. + /** Remove the user from the ignore list */ + /** Similar to adding, the change signal is emitted synchronously. * * \sa ignoredUsersListChanged */ @@ -256,9 +242,22 @@ namespace QMatrixClient /** Get the full list of users known to this account */ QMap<QString, User*> users() const; + /** Get the base URL of the homeserver to connect to */ QUrl homeserver() const; + /** Get the domain name used for ids/aliases on the server */ + QString domain() const; + /** Find a room by its id and a mask of applicable states */ Q_INVOKABLE Room* room(const QString& roomId, - JoinStates states = JoinState::Invite|JoinState::Join) const; + JoinStates states = JoinState::Invite|JoinState::Join) const; + /** Find a room by its alias and a mask of applicable states */ + Q_INVOKABLE Room* roomByAlias(const QString& roomAlias, + JoinStates states = JoinState::Invite|JoinState::Join) const; + /** Update the internal map of room aliases to IDs */ + /// This is used for internal bookkeeping of rooms. Do NOT use + /// it to try change aliases, use Room::setAliases instead + void updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases); Q_INVOKABLE Room* invitation(const QString& roomId) const; Q_INVOKABLE User* user(const QString& userId); const User* user() const; @@ -273,6 +272,35 @@ namespace QMatrixClient Q_INVOKABLE QString token() const; Q_INVOKABLE void getTurnServers(); + struct SupportedRoomVersion + { + QString id; + QString status; + + static const QString StableTag; // "stable", as of CS API 0.5 + bool isStable() const { return status == StableTag; } + + friend QDebug operator<<(QDebug dbg, + const SupportedRoomVersion& v) + { + QDebugStateSaver _(dbg); + return dbg.nospace() << v.id << '/' << v.status; + } + }; + + /// Get the room version recommended by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QString defaultRoomVersion() const; + /// Get the room version considered stable by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QStringList stableRoomVersions() const; + /// Get all room versions supported by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QVector<SupportedRoomVersion> availableRoomVersions() const; + /** * Call this before first sync to load from previously saved file. * @@ -280,7 +308,7 @@ namespace QMatrixClient * to be QML-friendly. Empty parameter means using a path * defined by stateCachePath(). */ - Q_INVOKABLE void loadState(const QUrl &fromFile = {}); + Q_INVOKABLE void loadState(); /** * This method saves the current state of rooms (but not messages * in them) to a local cache file, so that it could be loaded by @@ -290,7 +318,10 @@ namespace QMatrixClient * QML-friendly. Empty parameter means using a path defined by * stateCachePath(). */ - Q_INVOKABLE void saveState(const QUrl &toFile = {}) const; + Q_INVOKABLE void saveState() const; + + /// This method saves the current state of a single room. + void saveRoomState(Room* r) const; /** * The default path to store the cached room state, defined as @@ -305,6 +336,9 @@ namespace QMatrixClient bool cacheState() const; void setCacheState(bool newValue); + bool lazyLoading() const; + void setLazyLoading(bool newValue); + /** Start a job of a specified type with specified arguments and policy * * This is a universal method to start a job of a type passed @@ -375,13 +409,21 @@ namespace QMatrixClient const QString& deviceId = {}); void connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId); + /// Explicitly request capabilities from the server + void reloadCapabilities(); + + /// Find out if capabilites are still loading from the server + bool loadingCapabilities() const; /** @deprecated Use stopSync() instead */ void disconnectFromServer() { stopSync(); } void logout(); void sync(int timeout = -1); + void syncLoop(int timeout = -1); + void stopSync(); + QString nextBatchToken() const; virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; @@ -393,10 +435,10 @@ namespace QMatrixClient // QIODevice* should already be open UploadContentJob* uploadContent(QIODevice* contentSource, - const QString& filename = {}, - const QString& contentType = {}) const; + const QString& filename = {}, + const QString& overrideContentType = {}) const; UploadContentJob* uploadFile(const QString& fileName, - const QString& contentType = {}); + const QString& overrideContentType = {}); GetContentJob* getContent(const QString& mediaId) const; GetContentJob* getContent(const QUrl& url) const; // If localFilename is empty, a temporary file will be created @@ -411,7 +453,7 @@ namespace QMatrixClient CreateRoomJob* createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, QStringList invites, const QString& presetName = {}, - bool isDirect = false, + const QString& roomVersion = {}, bool isDirect = false, const QVector<CreateRoomJob::StateEvent>& initialState = {}, const QVector<CreateRoomJob::Invite3pid>& invite3pids = {}, const QJsonObject& creationContent = {}); @@ -485,14 +527,14 @@ namespace QMatrixClient SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event) const; + /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ + virtual LeaveRoomJob* leaveRoom( Room* room ); + // Old API that will be abolished any time soon. DO NOT USE. /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */ virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event) const; - /** @deprecated Use callApi<LeaveRoomJob>() or Room::leaveRoom() instead */ - virtual void leaveRoom( Room* room ); - signals: /** * @deprecated @@ -508,6 +550,7 @@ namespace QMatrixClient void resolveError(QString error); void homeserverChanged(QUrl baseUrl); + void capabilitiesLoaded(); void connected(); void reconnected(); //< \deprecated Use connected() instead @@ -519,7 +562,7 @@ namespace QMatrixClient * a successful login and logout and are constant at other times. */ void stateChanged(); - void loginError(QString message, QByteArray details); + void loginError(QString message, QString details); /** A network request (job) failed * @@ -537,11 +580,11 @@ namespace QMatrixClient * @param retriesTaken - how many retries have already been taken * @param nextRetryInMilliseconds - when the job will retry again */ - void networkError(QString message, QByteArray details, + void networkError(QString message, QString details, int retriesTaken, int nextRetryInMilliseconds); void syncDone(); - void syncError(QString message, QByteArray details); + void syncError(QString message, QString details); void newUser(User* user); @@ -652,6 +695,7 @@ namespace QMatrixClient IgnoredUsersList removals); void cacheStateChanged(); + void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); protected: @@ -660,21 +704,35 @@ namespace QMatrixClient */ const ConnectionData* connectionData() const; - /** - * @brief Find a (possibly new) Room object for the specified id - * Use this method whenever you need to find a Room object in - * the local list of rooms. Note that this does not interact with - * the server; in particular, does not automatically create rooms - * on the server. - * @return a pointer to a Room object with the specified id; nullptr - * if roomId is empty or roomFactory() failed to create a Room object. - */ - Room* provideRoom(const QString& roomId, JoinState joinState); + /** Get a Room object for the given id in the given state + * + * Use this method when you need a Room object in the local list + * of rooms, with the given state. Note that this does not interact + * with the server; in particular, does not automatically create + * rooms on the server. This call performs necessary join state + * transitions; e.g., if it finds a room in Invite but + * `joinState == JoinState::Join` then the Invite room object + * will be deleted and a new room object with Join state created. + * In contrast, switching between Join and Leave happens within + * the same object. + * \param roomId room id (not alias!) + * \param joinState desired (target) join state of the room; if + * omitted, any state will be found and return unchanged, or a + * new Join room created. + * @return a pointer to a Room object with the specified id and the + * specified state; nullptr if roomId is empty or if roomFactory() + * failed to create a Room object. + */ + Room* provideRoom(const QString& roomId, + Omittable<JoinState> joinState = none); /** * Completes loading sync data. */ - void onSyncSuccess(SyncData &&data); + void onSyncSuccess(SyncData &&data, bool fromCache = false); + + protected slots: + void syncLoopIteration(); private: class Private; diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index eb516ef7..91cda09f 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -25,7 +25,7 @@ using namespace QMatrixClient; struct ConnectionData::Private { - explicit Private(const QUrl& url) : baseUrl(url) { } + explicit Private(QUrl url) : baseUrl(std::move(url)) { } QUrl baseUrl; QByteArray accessToken; @@ -37,7 +37,7 @@ struct ConnectionData::Private }; ConnectionData::ConnectionData(QUrl baseUrl) - : d(std::make_unique<Private>(baseUrl)) + : d(std::make_unique<Private>(std::move(baseUrl))) { } ConnectionData::~ConnectionData() = default; @@ -98,7 +98,7 @@ QString ConnectionData::lastEvent() const void ConnectionData::setLastEvent(QString identifier) { - d->lastEvent = identifier; + d->lastEvent = std::move(identifier); } QByteArray ConnectionData::generateTxnId() const diff --git a/lib/converters.cpp b/lib/converters.cpp index 41a9a65e..88f5267e 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -22,38 +22,34 @@ using namespace QMatrixClient; -QJsonValue QMatrixClient::variantToJson(const QVariant& v) +QJsonValue JsonConverter<QVariant>::dump(const QVariant& v) { return QJsonValue::fromVariant(v); } -QJsonObject QMatrixClient::toJson(const QVariantMap& map) +QVariant JsonConverter<QVariant>::load(const QJsonValue& jv) { - return QJsonObject::fromVariantMap(map); + return jv.toVariant(); } -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -QJsonObject QMatrixClient::toJson(const QVariantHash& hMap) +QJsonObject JsonConverter<variant_map_t>::dump(const variant_map_t& map) { - return QJsonObject::fromVariantHash(hMap); -} + return +#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) + QJsonObject::fromVariantHash +#else + QJsonObject::fromVariantMap #endif - -QVariant FromJson<QVariant>::operator()(const QJsonValue& jv) const -{ - return jv.toVariant(); + (map); } -QMap<QString, QVariant> -FromJson<QMap<QString, QVariant>>::operator()(const QJsonValue& jv) const +variant_map_t JsonConverter<QVariantHash>::load(const QJsonValue& jv) { - return jv.toObject().toVariantMap(); -} - + return jv.toObject(). #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -QHash<QString, QVariant> -FromJson<QHash<QString, QVariant>>::operator()(const QJsonValue& jv) const -{ - return jv.toObject().toVariantHash(); -} + toVariantHash +#else + toVariantMap #endif + (); +} diff --git a/lib/converters.h b/lib/converters.h index 53855a1f..af2be645 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -57,238 +57,226 @@ class QVariant; namespace QMatrixClient { - // This catches anything implicitly convertible to QJsonValue/Object/Array - inline auto toJson(const QJsonValue& val) { return val; } - inline auto toJson(const QJsonObject& o) { return o; } - inline auto toJson(const QJsonArray& arr) { return arr; } - // Special-case QString to avoid ambiguity between QJsonValue - // and QVariant (also, QString.isEmpty() is used in _impl::AddNode<> below) - inline auto toJson(const QString& s) { return s; } - - inline QJsonArray toJson(const QStringList& strings) - { - return QJsonArray::fromStringList(strings); - } - - inline QString toJson(const QByteArray& bytes) + template <typename T> + struct JsonObjectConverter { - return bytes.constData(); - } + static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod; } + static void fillFrom(const QJsonObject& jo, T& pod) { pod = jo; } + }; - // QVariant is outrageously omnivorous - it consumes whatever is not - // exactly matching the signature of other toJson overloads. The trick - // below disables implicit conversion to QVariant through its numerous - // non-explicit constructors. - QJsonValue variantToJson(const QVariant& v); template <typename T> - inline auto toJson(T&& /* const QVariant& or QVariant&& */ var) - -> std::enable_if_t<std::is_same<std::decay_t<T>, QVariant>::value, - QJsonValue> + struct JsonConverter { - return variantToJson(var); - } - QJsonObject toJson(const QMap<QString, QVariant>& map); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - QJsonObject toJson(const QHash<QString, QVariant>& hMap); -#endif + static QJsonObject dump(const T& pod) + { + QJsonObject jo; + JsonObjectConverter<T>::dumpTo(jo, pod); + return jo; + } + static T doLoad(const QJsonObject& jo) + { + T pod; + JsonObjectConverter<T>::fillFrom(jo, pod); + return pod; + } + static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } + static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } + }; template <typename T> - inline QJsonArray toJson(const std::vector<T>& vals) + inline auto toJson(const T& pod) { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; + return JsonConverter<T>::dump(pod); } template <typename T> - inline QJsonArray toJson(const QVector<T>& vals) + inline auto fillJson(QJsonObject& json, const T& data) { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; + JsonObjectConverter<T>::dumpTo(json, data); } template <typename T> - inline QJsonObject toJson(const QSet<T>& set) + inline auto fromJson(const QJsonValue& jv) { - QJsonObject json; - for (auto e: set) - json.insert(toJson(e), QJsonObject{}); - return json; + return JsonConverter<T>::load(jv); } template <typename T> - inline QJsonObject toJson(const QHash<QString, T>& hashMap) + inline T fromJson(const QJsonDocument& jd) { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; + return JsonConverter<T>::load(jd); } template <typename T> - inline QJsonObject toJson(const std::unordered_map<QString, T>& hashMap) + inline void fromJson(const QJsonValue& jv, T& pod) { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; + if (!jv.isUndefined()) + pod = fromJson<T>(jv); } template <typename T> - struct FromJsonObject + inline void fromJson(const QJsonDocument& jd, T& pod) { - T operator()(const QJsonObject& jo) const { return T(jo); } - }; + pod = fromJson<T>(jd); + } + // Unfolds Omittable<> template <typename T> - struct FromJson + inline void fromJson(const QJsonValue& jv, Omittable<T>& pod) { - T operator()(const QJsonValue& jv) const - { - return FromJsonObject<T>()(jv.toObject()); - } - T operator()(const QJsonDocument& jd) const - { - return FromJsonObject<T>()(jd.object()); - } - }; + if (jv.isUndefined()) + pod = none; + else + pod = fromJson<T>(jv); + } template <typename T> - inline auto fromJson(const QJsonValue& jv) + inline void fillFromJson(const QJsonValue& jv, T& pod) { - return FromJson<T>()(jv); + if (jv.isObject()) + JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); } + // JsonConverter<> specialisations + template <typename T> - inline auto fromJson(const QJsonDocument& jd) + struct TrivialJsonDumper { - return FromJson<T>()(jd); - } + // Works for: QJsonValue (and all things it can consume), + // QJsonObject, QJsonArray + static auto dump(const T& val) { return val; } + }; - template <> struct FromJson<bool> + template <> struct JsonConverter<bool> : public TrivialJsonDumper<bool> { - auto operator()(const QJsonValue& jv) const { return jv.toBool(); } + static auto load(const QJsonValue& jv) { return jv.toBool(); } }; - template <> struct FromJson<int> + template <> struct JsonConverter<int> : public TrivialJsonDumper<int> { - auto operator()(const QJsonValue& jv) const { return jv.toInt(); } + static auto load(const QJsonValue& jv) { return jv.toInt(); } }; - template <> struct FromJson<double> + template <> struct JsonConverter<double> + : public TrivialJsonDumper<double> { - auto operator()(const QJsonValue& jv) const { return jv.toDouble(); } + static auto load(const QJsonValue& jv) { return jv.toDouble(); } }; - template <> struct FromJson<float> + template <> struct JsonConverter<float> : public TrivialJsonDumper<float> { - auto operator()(const QJsonValue& jv) const { return float(jv.toDouble()); } + static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } }; - template <> struct FromJson<qint64> + template <> struct JsonConverter<qint64> + : public TrivialJsonDumper<qint64> { - auto operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); } + static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } }; - template <> struct FromJson<QString> + template <> struct JsonConverter<QString> + : public TrivialJsonDumper<QString> { - auto operator()(const QJsonValue& jv) const { return jv.toString(); } + static auto load(const QJsonValue& jv) { return jv.toString(); } }; - template <> struct FromJson<QDateTime> + template <> struct JsonConverter<QDateTime> { - auto operator()(const QJsonValue& jv) const + static auto dump(const QDateTime& val) = delete; // not provided yet + static auto load(const QJsonValue& jv) { - return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); + return QDateTime::fromMSecsSinceEpoch( + fromJson<qint64>(jv), Qt::UTC); } }; - template <> struct FromJson<QDate> + template <> struct JsonConverter<QDate> { - auto operator()(const QJsonValue& jv) const + static auto dump(const QDate& val) = delete; // not provided yet + static auto load(const QJsonValue& jv) { return fromJson<QDateTime>(jv).date(); } }; - template <> struct FromJson<QJsonArray> + template <> struct JsonConverter<QJsonArray> + : public TrivialJsonDumper<QJsonArray> { - auto operator()(const QJsonValue& jv) const - { - return jv.toArray(); - } + static auto load(const QJsonValue& jv) { return jv.toArray(); } }; - template <> struct FromJson<QByteArray> + template <> struct JsonConverter<QByteArray> { - auto operator()(const QJsonValue& jv) const + static QString dump(const QByteArray& ba) { return ba.constData(); } + static auto load(const QJsonValue& jv) { return fromJson<QString>(jv).toLatin1(); } }; - template <> struct FromJson<QVariant> + template <> struct JsonConverter<QVariant> { - QVariant operator()(const QJsonValue& jv) const; + static QJsonValue dump(const QVariant& v); + static QVariant load(const QJsonValue& jv); }; - template <typename VectorT> - struct ArrayFromJson + template <typename VectorT, + typename T = typename VectorT::value_type> + struct JsonArrayConverter { - auto operator()(const QJsonArray& ja) const + static void dumpTo(QJsonArray& ar, const VectorT& vals) { - using size_type = typename VectorT::size_type; - VectorT vect; vect.resize(size_type(ja.size())); - std::transform(ja.begin(), ja.end(), - vect.begin(), FromJson<typename VectorT::value_type>()); - return vect; + for (const auto& v: vals) + ar.push_back(toJson(v)); } - auto operator()(const QJsonValue& jv) const + static auto dump(const VectorT& vals) { - return operator()(jv.toArray()); + QJsonArray ja; + dumpTo(ja, vals); + return ja; } - auto operator()(const QJsonDocument& jd) const + static auto load(const QJsonArray& ja) { - return operator()(jd.array()); + VectorT vect; vect.reserve(typename VectorT::size_type(ja.size())); + for (const auto& i: ja) + vect.push_back(fromJson<T>(i)); + return vect; } + static auto load(const QJsonValue& jv) { return load(jv.toArray()); } + static auto load(const QJsonDocument& jd) { return load(jd.array()); } }; - template <typename T> - struct FromJson<std::vector<T>> : ArrayFromJson<std::vector<T>> + template <typename T> struct JsonConverter<std::vector<T>> + : public JsonArrayConverter<std::vector<T>> { }; - template <typename T> - struct FromJson<QVector<T>> : ArrayFromJson<QVector<T>> + template <typename T> struct JsonConverter<QVector<T>> + : public JsonArrayConverter<QVector<T>> + { }; + + template <typename T> struct JsonConverter<QList<T>> + : public JsonArrayConverter<QList<T>> { }; - template <typename T> struct FromJson<QList<T>> + template <> struct JsonConverter<QStringList> + : public JsonConverter<QList<QString>> { - auto operator()(const QJsonValue& jv) const + static auto dump(const QStringList& sl) { - const auto jsonArray = jv.toArray(); - QList<T> sl; sl.reserve(jsonArray.size()); - std::transform(jsonArray.begin(), jsonArray.end(), - std::back_inserter(sl), FromJson<T>()); - return sl; + return QJsonArray::fromStringList(sl); } }; - template <> struct FromJson<QStringList> : FromJson<QList<QString>> { }; - - template <> struct FromJson<QMap<QString, QVariant>> + template <> struct JsonObjectConverter<QSet<QString>> { - QMap<QString, QVariant> operator()(const QJsonValue& jv) const; - }; - - template <typename T> struct FromJson<QSet<T>> - { - auto operator()(const QJsonValue& jv) const + static void dumpTo(QJsonObject& json, const QSet<QString>& s) + { + for (const auto& e: s) + json.insert(toJson(e), QJsonObject{}); + } + static auto fillFrom(const QJsonObject& json, QSet<QString>& s) { - const auto json = jv.toObject(); - QSet<T> s; s.reserve(json.size()); + s.reserve(s.size() + json.size()); for (auto it = json.begin(); it != json.end(); ++it) s.insert(it.key()); return s; @@ -298,39 +286,44 @@ namespace QMatrixClient template <typename HashMapT> struct HashMapFromJson { - auto operator()(const QJsonObject& jo) const + static void dumpTo(QJsonObject& json, const HashMapT& hashMap) + { + for (auto it = hashMap.begin(); it != hashMap.end(); ++it) + json.insert(it.key(), toJson(it.value())); + } + static void fillFrom(const QJsonObject& jo, HashMapT& h) { - HashMapT h; h.reserve(jo.size()); + h.reserve(jo.size()); for (auto it = jo.begin(); it != jo.end(); ++it) h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value()); - return h; - } - auto operator()(const QJsonValue& jv) const - { - return operator()(jv.toObject()); - } - auto operator()(const QJsonDocument& jd) const - { - return operator()(jd.object()); } }; template <typename T> - struct FromJson<std::unordered_map<QString, T>> - : HashMapFromJson<std::unordered_map<QString, T>> + struct JsonObjectConverter<std::unordered_map<QString, T>> + : public HashMapFromJson<std::unordered_map<QString, T>> { }; template <typename T> - struct FromJson<QHash<QString, T>> : HashMapFromJson<QHash<QString, T>> + struct JsonObjectConverter<QHash<QString, T>> + : public HashMapFromJson<QHash<QString, T>> { }; + // We could use std::conditional<> below but QT_VERSION* macros in C++ code + // cause (kinda valid but useless and noisy) compiler warnings about + // bitwise operations on signed integers; so use the preprocessor for now. + using variant_map_t = #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - template <> struct FromJson<QHash<QString, QVariant>> + QVariantHash; +#else + QVariantMap; +#endif + template <> struct JsonConverter<variant_map_t> { - QHash<QString, QVariant> operator()(const QJsonValue& jv) const; + static QJsonObject dump(const variant_map_t& vh); + static QVariantHash load(const QJsonValue& jv); }; -#endif // Conditional insertion into a QJsonObject diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 5021c73a..96b32a92 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -21,6 +21,20 @@ SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, setRequestData(Data(toJson(content))); } +QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/user/" % userId % "/account_data/" % type); +} + +static const auto GetAccountDataJobName = QStringLiteral("GetAccountDataJob"); + +GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) + : BaseJob(HttpVerb::Get, GetAccountDataJobName, + basePath % "/user/" % userId % "/account_data/" % type) +{ +} + static const auto SetAccountDataPerRoomJobName = QStringLiteral("SetAccountDataPerRoomJob"); SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content) @@ -30,3 +44,17 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const setRequestData(Data(toJson(content))); } +QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type); +} + +static const auto GetAccountDataPerRoomJobName = QStringLiteral("GetAccountDataPerRoomJob"); + +GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type) + : BaseJob(HttpVerb::Get, GetAccountDataPerRoomJobName, + basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) +{ +} + diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index f3656a14..b067618f 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -22,8 +22,8 @@ namespace QMatrixClient public: /*! Set some account_data for the user. * \param userId - * The id of the user to set account_data for. The access token must be - * authorized to make requests for this user id. + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. * \param type * The event type of the account_data to set. Custom types should be * namespaced to avoid clashes. @@ -33,6 +33,33 @@ namespace QMatrixClient explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); }; + /// Get some account_data for the user. + /// + /// Get some account_data for the client. This config is only visible to the user + /// that set the account_data. + class GetAccountDataJob : public BaseJob + { + public: + /*! Get some account_data for the user. + * \param userId + * The ID of the user to get account_data for. The access token must be + * authorized to make requests for this user ID. + * \param type + * The event type of the account_data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataJob(const QString& userId, const QString& type); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetAccountDataJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type); + + }; + /// Set some account_data for the user. /// /// Set some account_data for the client on a given room. This config is only @@ -43,10 +70,10 @@ namespace QMatrixClient public: /*! Set some account_data for the user. * \param userId - * The id of the user to set account_data for. The access token must be - * authorized to make requests for this user id. + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. * \param roomId - * The id of the room to set account_data on. + * The ID of the room to set account_data on. * \param type * The event type of the account_data to set. Custom types should be * namespaced to avoid clashes. @@ -55,4 +82,33 @@ namespace QMatrixClient */ explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); }; + + /// Get some account_data for the user. + /// + /// Get some account_data for the client on a given room. This config is only + /// visible to the user that set the account_data. + class GetAccountDataPerRoomJob : public BaseJob + { + public: + /*! Get some account_data for the user. + * \param userId + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. + * \param roomId + * The ID of the room to get account_data for. + * \param type + * The event type of the account_data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetAccountDataPerRoomJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type); + + }; } // namespace QMatrixClient diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 6066d4d9..ce06a56d 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -16,43 +16,29 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetWhoIsJob::ConnectionInfo> + template <> struct JsonObjectConverter<GetWhoIsJob::ConnectionInfo> { - GetWhoIsJob::ConnectionInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::ConnectionInfo& result) { - GetWhoIsJob::ConnectionInfo result; - result.ip = - fromJson<QString>(jo.value("ip"_ls)); - result.lastSeen = - fromJson<qint64>(jo.value("last_seen"_ls)); - result.userAgent = - fromJson<QString>(jo.value("user_agent"_ls)); - - return result; + fromJson(jo.value("ip"_ls), result.ip); + fromJson(jo.value("last_seen"_ls), result.lastSeen); + fromJson(jo.value("user_agent"_ls), result.userAgent); } }; - template <> struct FromJsonObject<GetWhoIsJob::SessionInfo> + template <> struct JsonObjectConverter<GetWhoIsJob::SessionInfo> { - GetWhoIsJob::SessionInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::SessionInfo& result) { - GetWhoIsJob::SessionInfo result; - result.connections = - fromJson<QVector<GetWhoIsJob::ConnectionInfo>>(jo.value("connections"_ls)); - - return result; + fromJson(jo.value("connections"_ls), result.connections); } }; - template <> struct FromJsonObject<GetWhoIsJob::DeviceInfo> + template <> struct JsonObjectConverter<GetWhoIsJob::DeviceInfo> { - GetWhoIsJob::DeviceInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::DeviceInfo& result) { - GetWhoIsJob::DeviceInfo result; - result.sessions = - fromJson<QVector<GetWhoIsJob::SessionInfo>>(jo.value("sessions"_ls)); - - return result; + fromJson(jo.value("sessions"_ls), result.sessions); } }; } // namespace QMatrixClient @@ -94,8 +80,8 @@ const QHash<QString, GetWhoIsJob::DeviceInfo>& GetWhoIsJob::devices() const BaseJob::Status GetWhoIsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->devices = fromJson<QHash<QString, DeviceInfo>>(json.value("devices"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("devices"_ls), d->devices); return Success; } diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index f62002a6..11385dff 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -16,21 +16,14 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetAccount3PIDsJob::ThirdPartyIdentifier> + template <> struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> { - GetAccount3PIDsJob::ThirdPartyIdentifier operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetAccount3PIDsJob::ThirdPartyIdentifier& result) { - GetAccount3PIDsJob::ThirdPartyIdentifier result; - result.medium = - fromJson<QString>(jo.value("medium"_ls)); - result.address = - fromJson<QString>(jo.value("address"_ls)); - result.validatedAt = - fromJson<qint64>(jo.value("validated_at"_ls)); - result.addedAt = - fromJson<qint64>(jo.value("added_at"_ls)); - - return result; + fromJson(jo.value("medium"_ls), result.medium); + fromJson(jo.value("address"_ls), result.address); + fromJson(jo.value("validated_at"_ls), result.validatedAt); + fromJson(jo.value("added_at"_ls), result.addedAt); } }; } // namespace QMatrixClient @@ -66,7 +59,7 @@ const QVector<GetAccount3PIDsJob::ThirdPartyIdentifier>& GetAccount3PIDsJob::thr BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->threepids = fromJson<QVector<ThirdPartyIdentifier>>(json.value("threepids"_ls)); + fromJson(json.value("threepids"_ls), d->threepids); return Success; } @@ -74,19 +67,20 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const Post3PIDsJob::ThreePidCredentials& pod) + template <> struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("sid"), pod.sid); - return jo; - } + static void dumpTo(QJsonObject& jo, const Post3PIDsJob::ThreePidCredentials& pod) + { + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("sid"), pod.sid); + } + }; } // namespace QMatrixClient static const auto Post3PIDsJobName = QStringLiteral("Post3PIDsJob"); -Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind) +Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable<bool> bind) : BaseJob(HttpVerb::Post, Post3PIDsJobName, basePath % "/account/3pid") { @@ -139,7 +133,7 @@ const Sid& RequestTokenTo3PIDEmailJob::data() const BaseJob::Status RequestTokenTo3PIDEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -175,7 +169,7 @@ const Sid& RequestTokenTo3PIDMSISDNJob::data() const BaseJob::Status RequestTokenTo3PIDMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index 3fb3d44c..02aeee4d 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -113,7 +113,7 @@ namespace QMatrixClient * identifier to the account's Matrix ID with the passed identity * server. Default: ``false``. */ - explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind = false); + explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable<bool> bind = none); }; /// Deletes a third party identifier from the user's account diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp new file mode 100644 index 00000000..210423f5 --- /dev/null +++ b/lib/csapi/capabilities.cpp @@ -0,0 +1,84 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "capabilities.h" + +#include "converters.h" + +#include <QtCore/QStringBuilder> + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +namespace QMatrixClient +{ + // Converters + + template <> struct JsonObjectConverter<GetCapabilitiesJob::ChangePasswordCapability> + { + static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::ChangePasswordCapability& result) + { + fromJson(jo.value("enabled"_ls), result.enabled); + } + }; + + template <> struct JsonObjectConverter<GetCapabilitiesJob::RoomVersionsCapability> + { + static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::RoomVersionsCapability& result) + { + fromJson(jo.value("default"_ls), result.defaultVersion); + fromJson(jo.value("available"_ls), result.available); + } + }; + + template <> struct JsonObjectConverter<GetCapabilitiesJob::Capabilities> + { + static void fillFrom(QJsonObject jo, GetCapabilitiesJob::Capabilities& result) + { + fromJson(jo.take("m.change_password"_ls), result.changePassword); + fromJson(jo.take("m.room_versions"_ls), result.roomVersions); + fromJson(jo, result.additionalProperties); + } + }; +} // namespace QMatrixClient + +class GetCapabilitiesJob::Private +{ + public: + Capabilities capabilities; +}; + +QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/capabilities"); +} + +static const auto GetCapabilitiesJobName = QStringLiteral("GetCapabilitiesJob"); + +GetCapabilitiesJob::GetCapabilitiesJob() + : BaseJob(HttpVerb::Get, GetCapabilitiesJobName, + basePath % "/capabilities") + , d(new Private) +{ +} + +GetCapabilitiesJob::~GetCapabilitiesJob() = default; + +const GetCapabilitiesJob::Capabilities& GetCapabilitiesJob::capabilities() const +{ + return d->capabilities; +} + +BaseJob::Status GetCapabilitiesJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("capabilities"_ls)) + return { JsonParseError, + "The key 'capabilities' not found in the response" }; + fromJson(json.value("capabilities"_ls), d->capabilities); + return Success; +} + diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h new file mode 100644 index 00000000..06a8bf0d --- /dev/null +++ b/lib/csapi/capabilities.h @@ -0,0 +1,82 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +#include <QtCore/QJsonObject> +#include "converters.h" +#include <QtCore/QHash> + +namespace QMatrixClient +{ + // Operations + + /// Gets information about the server's capabilities. + /// + /// Gets information about the server's supported feature set + /// and other relevant capabilities. + class GetCapabilitiesJob : public BaseJob + { + public: + // Inner data structures + + /// Capability to indicate if the user can change their password. + struct ChangePasswordCapability + { + /// True if the user can change their password, false otherwise. + bool enabled; + }; + + /// The room versions the server supports. + struct RoomVersionsCapability + { + /// The default room version the server is using for new rooms. + QString defaultVersion; + /// A detailed description of the room versions the server supports. + QHash<QString, QString> available; + }; + + /// The custom capabilities the server supports, using the + /// Java package naming convention. + struct Capabilities + { + /// Capability to indicate if the user can change their password. + Omittable<ChangePasswordCapability> changePassword; + /// The room versions the server supports. + Omittable<RoomVersionsCapability> roomVersions; + /// The custom capabilities the server supports, using the + /// Java package naming convention. + QHash<QString, QJsonObject> additionalProperties; + }; + + // Construction/destruction + + explicit GetCapabilitiesJob(); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetCapabilitiesJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + ~GetCapabilitiesJob() override; + + // Result properties + + /// The custom capabilities the server supports, using the + /// Java package naming convention. + const Capabilities& capabilities() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer<Private> d; + }; +} // namespace QMatrixClient diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 9b590e42..22223985 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -52,7 +52,7 @@ BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data) if (!json.contains("content_uri"_ls)) return { JsonParseError, "The key 'content_uri' not found in the response" }; - d->contentUri = fromJson<QString>(json.value("content_uri"_ls)); + fromJson(json.value("content_uri"_ls), d->contentUri); return Success; } @@ -276,8 +276,8 @@ const QString& GetUrlPreviewJob::ogImage() const BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->matrixImageSize = fromJson<qint64>(json.value("matrix:image:size"_ls)); - d->ogImage = fromJson<QString>(json.value("og:image"_ls)); + fromJson(json.value("matrix:image:size"_ls), d->matrixImageSize); + fromJson(json.value("og:image"_ls), d->ogImage); return Success; } @@ -312,7 +312,7 @@ Omittable<qint64> GetConfigJob::uploadSize() const BaseJob::Status GetConfigJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->uploadSize = fromJson<qint64>(json.value("m.upload.size"_ls)); + fromJson(json.value("m.upload.size"_ls), d->uploadSize); return Success; } diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 36f83727..448547ae 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -16,23 +16,25 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const CreateRoomJob::Invite3pid& pod) + template <> struct JsonObjectConverter<CreateRoomJob::Invite3pid> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("medium"), pod.medium); - addParam<>(jo, QStringLiteral("address"), pod.address); - return jo; - } + static void dumpTo(QJsonObject& jo, const CreateRoomJob::Invite3pid& pod) + { + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("medium"), pod.medium); + addParam<>(jo, QStringLiteral("address"), pod.address); + } + }; - QJsonObject toJson(const CreateRoomJob::StateEvent& pod) + template <> struct JsonObjectConverter<CreateRoomJob::StateEvent> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("type"), pod.type); - addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey); - addParam<>(jo, QStringLiteral("content"), pod.content); - return jo; - } + static void dumpTo(QJsonObject& jo, const CreateRoomJob::StateEvent& pod) + { + addParam<>(jo, QStringLiteral("type"), pod.type); + addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey); + addParam<>(jo, QStringLiteral("content"), pod.content); + } + }; } // namespace QMatrixClient class CreateRoomJob::Private @@ -43,7 +45,7 @@ class CreateRoomJob::Private static const auto CreateRoomJobName = QStringLiteral("CreateRoomJob"); -CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector<Invite3pid>& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, bool isDirect, const QJsonObject& powerLevelContentOverride) +CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector<Invite3pid>& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, Omittable<bool> isDirect, const QJsonObject& powerLevelContentOverride) : BaseJob(HttpVerb::Post, CreateRoomJobName, basePath % "/createRoom") , d(new Private) @@ -77,7 +79,7 @@ BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index a0a64df0..d7c01d00 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -216,7 +216,7 @@ namespace QMatrixClient * event content prior to it being sent to the room. Defaults to * overriding nothing. */ - explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, bool isDirect = false, const QJsonObject& powerLevelContentOverride = {}); + explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, Omittable<bool> isDirect = none, const QJsonObject& powerLevelContentOverride = {}); ~CreateRoomJob() override; // Result properties diff --git a/lib/csapi/definitions/auth_data.cpp b/lib/csapi/definitions/auth_data.cpp index f8639432..006b8c7e 100644 --- a/lib/csapi/definitions/auth_data.cpp +++ b/lib/csapi/definitions/auth_data.cpp @@ -6,23 +6,20 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const AuthenticationData& pod) +void JsonObjectConverter<AuthenticationData>::dumpTo( + QJsonObject& jo, const AuthenticationData& pod) { - QJsonObject jo = toJson(pod.authInfo); + fillJson(jo, pod.authInfo); addParam<>(jo, QStringLiteral("type"), pod.type); addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session); - return jo; } -AuthenticationData FromJsonObject<AuthenticationData>::operator()(QJsonObject jo) const +void JsonObjectConverter<AuthenticationData>::fillFrom( + QJsonObject jo, AuthenticationData& result) { - AuthenticationData result; - result.type = - fromJson<QString>(jo.take("type"_ls)); - result.session = - fromJson<QString>(jo.take("session"_ls)); + fromJson(jo.take("type"_ls), result.type); + fromJson(jo.take("session"_ls), result.session); - result.authInfo = fromJson<QHash<QString, QJsonObject>>(jo); - return result; + fromJson(jo, result.authInfo); } diff --git a/lib/csapi/definitions/auth_data.h b/lib/csapi/definitions/auth_data.h index 661d3e5f..26eb205c 100644 --- a/lib/csapi/definitions/auth_data.h +++ b/lib/csapi/definitions/auth_data.h @@ -23,12 +23,10 @@ namespace QMatrixClient /// Keys dependent on the login type QHash<QString, QJsonObject> authInfo; }; - - QJsonObject toJson(const AuthenticationData& pod); - - template <> struct FromJsonObject<AuthenticationData> + template <> struct JsonObjectConverter<AuthenticationData> { - AuthenticationData operator()(QJsonObject jo) const; + static void dumpTo(QJsonObject& jo, const AuthenticationData& pod); + static void fillFrom(QJsonObject jo, AuthenticationData& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/client_device.cpp b/lib/csapi/definitions/client_device.cpp index 4a192f85..752b806a 100644 --- a/lib/csapi/definitions/client_device.cpp +++ b/lib/csapi/definitions/client_device.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const Device& pod) +void JsonObjectConverter<Device>::dumpTo( + QJsonObject& jo, const Device& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); addParam<IfNotEmpty>(jo, QStringLiteral("display_name"), pod.displayName); addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp); addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs); - return jo; } -Device FromJsonObject<Device>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<Device>::fillFrom( + const QJsonObject& jo, Device& result) { - Device result; - result.deviceId = - fromJson<QString>(jo.value("device_id"_ls)); - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.lastSeenIp = - fromJson<QString>(jo.value("last_seen_ip"_ls)); - result.lastSeenTs = - fromJson<qint64>(jo.value("last_seen_ts"_ls)); - - return result; + fromJson(jo.value("device_id"_ls), result.deviceId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("last_seen_ip"_ls), result.lastSeenIp); + fromJson(jo.value("last_seen_ts"_ls), result.lastSeenTs); } diff --git a/lib/csapi/definitions/client_device.h b/lib/csapi/definitions/client_device.h index 9f10888a..a6224f71 100644 --- a/lib/csapi/definitions/client_device.h +++ b/lib/csapi/definitions/client_device.h @@ -28,12 +28,10 @@ namespace QMatrixClient /// reasons). Omittable<qint64> lastSeenTs; }; - - QJsonObject toJson(const Device& pod); - - template <> struct FromJsonObject<Device> + template <> struct JsonObjectConverter<Device> { - Device operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Device& pod); + static void fillFrom(const QJsonObject& jo, Device& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/device_keys.cpp b/lib/csapi/definitions/device_keys.cpp index a0e0ca42..1e79499f 100644 --- a/lib/csapi/definitions/device_keys.cpp +++ b/lib/csapi/definitions/device_keys.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const DeviceKeys& pod) +void JsonObjectConverter<DeviceKeys>::dumpTo( + QJsonObject& jo, const DeviceKeys& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("user_id"), pod.userId); addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms); addParam<>(jo, QStringLiteral("keys"), pod.keys); addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; } -DeviceKeys FromJsonObject<DeviceKeys>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<DeviceKeys>::fillFrom( + const QJsonObject& jo, DeviceKeys& result) { - DeviceKeys result; - result.userId = - fromJson<QString>(jo.value("user_id"_ls)); - result.deviceId = - fromJson<QString>(jo.value("device_id"_ls)); - result.algorithms = - fromJson<QStringList>(jo.value("algorithms"_ls)); - result.keys = - fromJson<QHash<QString, QString>>(jo.value("keys"_ls)); - result.signatures = - fromJson<QHash<QString, QHash<QString, QString>>>(jo.value("signatures"_ls)); - - return result; + fromJson(jo.value("user_id"_ls), result.userId); + fromJson(jo.value("device_id"_ls), result.deviceId); + fromJson(jo.value("algorithms"_ls), result.algorithms); + fromJson(jo.value("keys"_ls), result.keys); + fromJson(jo.value("signatures"_ls), result.signatures); } diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h index 6023e7e8..8ebe1125 100644 --- a/lib/csapi/definitions/device_keys.h +++ b/lib/csapi/definitions/device_keys.h @@ -34,12 +34,10 @@ namespace QMatrixClient /// JSON`_. QHash<QString, QHash<QString, QString>> signatures; }; - - QJsonObject toJson(const DeviceKeys& pod); - - template <> struct FromJsonObject<DeviceKeys> + template <> struct JsonObjectConverter<DeviceKeys> { - DeviceKeys operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const DeviceKeys& pod); + static void fillFrom(const QJsonObject& jo, DeviceKeys& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/event_filter.cpp b/lib/csapi/definitions/event_filter.cpp index cc444db0..b20d7807 100644 --- a/lib/csapi/definitions/event_filter.cpp +++ b/lib/csapi/definitions/event_filter.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const EventFilter& pod) +void JsonObjectConverter<EventFilter>::dumpTo( + QJsonObject& jo, const EventFilter& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("limit"), pod.limit); addParam<IfNotEmpty>(jo, QStringLiteral("not_senders"), pod.notSenders); addParam<IfNotEmpty>(jo, QStringLiteral("not_types"), pod.notTypes); addParam<IfNotEmpty>(jo, QStringLiteral("senders"), pod.senders); addParam<IfNotEmpty>(jo, QStringLiteral("types"), pod.types); - return jo; } -EventFilter FromJsonObject<EventFilter>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<EventFilter>::fillFrom( + const QJsonObject& jo, EventFilter& result) { - EventFilter result; - result.limit = - fromJson<int>(jo.value("limit"_ls)); - result.notSenders = - fromJson<QStringList>(jo.value("not_senders"_ls)); - result.notTypes = - fromJson<QStringList>(jo.value("not_types"_ls)); - result.senders = - fromJson<QStringList>(jo.value("senders"_ls)); - result.types = - fromJson<QStringList>(jo.value("types"_ls)); - - return result; + fromJson(jo.value("limit"_ls), result.limit); + fromJson(jo.value("not_senders"_ls), result.notSenders); + fromJson(jo.value("not_types"_ls), result.notTypes); + fromJson(jo.value("senders"_ls), result.senders); + fromJson(jo.value("types"_ls), result.types); } diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h index 5c6a5b27..6de1fe79 100644 --- a/lib/csapi/definitions/event_filter.h +++ b/lib/csapi/definitions/event_filter.h @@ -25,12 +25,10 @@ namespace QMatrixClient /// A list of event types to include. If this list is absent then all event types are included. A ``'*'`` can be used as a wildcard to match any sequence of characters. QStringList types; }; - - QJsonObject toJson(const EventFilter& pod); - - template <> struct FromJsonObject<EventFilter> + template <> struct JsonObjectConverter<EventFilter> { - EventFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const EventFilter& pod); + static void fillFrom(const QJsonObject& jo, EventFilter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/public_rooms_response.cpp b/lib/csapi/definitions/public_rooms_response.cpp index 2f52501d..0d26662c 100644 --- a/lib/csapi/definitions/public_rooms_response.cpp +++ b/lib/csapi/definitions/public_rooms_response.cpp @@ -6,9 +6,9 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod) +void JsonObjectConverter<PublicRoomsChunk>::dumpTo( + QJsonObject& jo, const PublicRoomsChunk& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("aliases"), pod.aliases); addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"), pod.canonicalAlias); addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name); @@ -18,56 +18,37 @@ QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod) addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl); - return jo; } -PublicRoomsChunk FromJsonObject<PublicRoomsChunk>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PublicRoomsChunk>::fillFrom( + const QJsonObject& jo, PublicRoomsChunk& result) { - PublicRoomsChunk result; - result.aliases = - fromJson<QStringList>(jo.value("aliases"_ls)); - result.canonicalAlias = - fromJson<QString>(jo.value("canonical_alias"_ls)); - result.name = - fromJson<QString>(jo.value("name"_ls)); - result.numJoinedMembers = - fromJson<int>(jo.value("num_joined_members"_ls)); - result.roomId = - fromJson<QString>(jo.value("room_id"_ls)); - result.topic = - fromJson<QString>(jo.value("topic"_ls)); - result.worldReadable = - fromJson<bool>(jo.value("world_readable"_ls)); - result.guestCanJoin = - fromJson<bool>(jo.value("guest_can_join"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("aliases"_ls), result.aliases); + fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias); + fromJson(jo.value("name"_ls), result.name); + fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("topic"_ls), result.topic); + fromJson(jo.value("world_readable"_ls), result.worldReadable); + fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } -QJsonObject QMatrixClient::toJson(const PublicRoomsResponse& pod) +void JsonObjectConverter<PublicRoomsResponse>::dumpTo( + QJsonObject& jo, const PublicRoomsResponse& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("chunk"), pod.chunk); addParam<IfNotEmpty>(jo, QStringLiteral("next_batch"), pod.nextBatch); addParam<IfNotEmpty>(jo, QStringLiteral("prev_batch"), pod.prevBatch); addParam<IfNotEmpty>(jo, QStringLiteral("total_room_count_estimate"), pod.totalRoomCountEstimate); - return jo; } -PublicRoomsResponse FromJsonObject<PublicRoomsResponse>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PublicRoomsResponse>::fillFrom( + const QJsonObject& jo, PublicRoomsResponse& result) { - PublicRoomsResponse result; - result.chunk = - fromJson<QVector<PublicRoomsChunk>>(jo.value("chunk"_ls)); - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - result.prevBatch = - fromJson<QString>(jo.value("prev_batch"_ls)); - result.totalRoomCountEstimate = - fromJson<int>(jo.value("total_room_count_estimate"_ls)); - - return result; + fromJson(jo.value("chunk"_ls), result.chunk); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("prev_batch"_ls), result.prevBatch); + fromJson(jo.value("total_room_count_estimate"_ls), result.totalRoomCountEstimate); } diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 88c805ba..4c54ac25 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -36,12 +36,10 @@ namespace QMatrixClient /// The URL for the room's avatar, if one is set. QString avatarUrl; }; - - QJsonObject toJson(const PublicRoomsChunk& pod); - - template <> struct FromJsonObject<PublicRoomsChunk> + template <> struct JsonObjectConverter<PublicRoomsChunk> { - PublicRoomsChunk operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod); + static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod); }; /// A list of the rooms on the server. @@ -61,12 +59,10 @@ namespace QMatrixClient /// server has an estimate. Omittable<int> totalRoomCountEstimate; }; - - QJsonObject toJson(const PublicRoomsResponse& pod); - - template <> struct FromJsonObject<PublicRoomsResponse> + template <> struct JsonObjectConverter<PublicRoomsResponse> { - PublicRoomsResponse operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod); + static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_condition.cpp b/lib/csapi/definitions/push_condition.cpp index 045094bc..ace02755 100644 --- a/lib/csapi/definitions/push_condition.cpp +++ b/lib/csapi/definitions/push_condition.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushCondition& pod) +void JsonObjectConverter<PushCondition>::dumpTo( + QJsonObject& jo, const PushCondition& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("kind"), pod.kind); addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); addParam<IfNotEmpty>(jo, QStringLiteral("is"), pod.is); - return jo; } -PushCondition FromJsonObject<PushCondition>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PushCondition>::fillFrom( + const QJsonObject& jo, PushCondition& result) { - PushCondition result; - result.kind = - fromJson<QString>(jo.value("kind"_ls)); - result.key = - fromJson<QString>(jo.value("key"_ls)); - result.pattern = - fromJson<QString>(jo.value("pattern"_ls)); - result.is = - fromJson<QString>(jo.value("is"_ls)); - - return result; + fromJson(jo.value("kind"_ls), result.kind); + fromJson(jo.value("key"_ls), result.key); + fromJson(jo.value("pattern"_ls), result.pattern); + fromJson(jo.value("is"_ls), result.is); } diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h index defcebb3..e45526d2 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -28,12 +28,10 @@ namespace QMatrixClient /// so forth. If no prefix is present, this parameter defaults to ==. QString is; }; - - QJsonObject toJson(const PushCondition& pod); - - template <> struct FromJsonObject<PushCondition> + template <> struct JsonObjectConverter<PushCondition> { - PushCondition operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushCondition& pod); + static void fillFrom(const QJsonObject& jo, PushCondition& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_rule.cpp b/lib/csapi/definitions/push_rule.cpp index baddd187..abbb04b5 100644 --- a/lib/csapi/definitions/push_rule.cpp +++ b/lib/csapi/definitions/push_rule.cpp @@ -6,34 +6,25 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushRule& pod) +void JsonObjectConverter<PushRule>::dumpTo( + QJsonObject& jo, const PushRule& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("actions"), pod.actions); addParam<>(jo, QStringLiteral("default"), pod.isDefault); addParam<>(jo, QStringLiteral("enabled"), pod.enabled); addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId); addParam<IfNotEmpty>(jo, QStringLiteral("conditions"), pod.conditions); addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); - return jo; } -PushRule FromJsonObject<PushRule>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PushRule>::fillFrom( + const QJsonObject& jo, PushRule& result) { - PushRule result; - result.actions = - fromJson<QVector<QVariant>>(jo.value("actions"_ls)); - result.isDefault = - fromJson<bool>(jo.value("default"_ls)); - result.enabled = - fromJson<bool>(jo.value("enabled"_ls)); - result.ruleId = - fromJson<QString>(jo.value("rule_id"_ls)); - result.conditions = - fromJson<QVector<PushCondition>>(jo.value("conditions"_ls)); - result.pattern = - fromJson<QString>(jo.value("pattern"_ls)); - - return result; + fromJson(jo.value("actions"_ls), result.actions); + fromJson(jo.value("default"_ls), result.isDefault); + fromJson(jo.value("enabled"_ls), result.enabled); + fromJson(jo.value("rule_id"_ls), result.ruleId); + fromJson(jo.value("conditions"_ls), result.conditions); + fromJson(jo.value("pattern"_ls), result.pattern); } diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h index 5f52876d..bea13e96 100644 --- a/lib/csapi/definitions/push_rule.h +++ b/lib/csapi/definitions/push_rule.h @@ -7,10 +7,10 @@ #include "converters.h" #include "csapi/definitions/push_condition.h" -#include "converters.h" +#include <QtCore/QJsonObject> #include <QtCore/QVector> #include <QtCore/QVariant> -#include <QtCore/QJsonObject> +#include "converters.h" namespace QMatrixClient { @@ -34,12 +34,10 @@ namespace QMatrixClient /// rules. QString pattern; }; - - QJsonObject toJson(const PushRule& pod); - - template <> struct FromJsonObject<PushRule> + template <> struct JsonObjectConverter<PushRule> { - PushRule operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushRule& pod); + static void fillFrom(const QJsonObject& jo, PushRule& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_ruleset.cpp b/lib/csapi/definitions/push_ruleset.cpp index 14b7a4b6..f1bad882 100644 --- a/lib/csapi/definitions/push_ruleset.cpp +++ b/lib/csapi/definitions/push_ruleset.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushRuleset& pod) +void JsonObjectConverter<PushRuleset>::dumpTo( + QJsonObject& jo, const PushRuleset& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("content"), pod.content); addParam<IfNotEmpty>(jo, QStringLiteral("override"), pod.override); addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); addParam<IfNotEmpty>(jo, QStringLiteral("sender"), pod.sender); addParam<IfNotEmpty>(jo, QStringLiteral("underride"), pod.underride); - return jo; } -PushRuleset FromJsonObject<PushRuleset>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PushRuleset>::fillFrom( + const QJsonObject& jo, PushRuleset& result) { - PushRuleset result; - result.content = - fromJson<QVector<PushRule>>(jo.value("content"_ls)); - result.override = - fromJson<QVector<PushRule>>(jo.value("override"_ls)); - result.room = - fromJson<QVector<PushRule>>(jo.value("room"_ls)); - result.sender = - fromJson<QVector<PushRule>>(jo.value("sender"_ls)); - result.underride = - fromJson<QVector<PushRule>>(jo.value("underride"_ls)); - - return result; + fromJson(jo.value("content"_ls), result.content); + fromJson(jo.value("override"_ls), result.override); + fromJson(jo.value("room"_ls), result.room); + fromJson(jo.value("sender"_ls), result.sender); + fromJson(jo.value("underride"_ls), result.underride); } diff --git a/lib/csapi/definitions/push_ruleset.h b/lib/csapi/definitions/push_ruleset.h index a274b72a..f2d937c0 100644 --- a/lib/csapi/definitions/push_ruleset.h +++ b/lib/csapi/definitions/push_ruleset.h @@ -22,12 +22,10 @@ namespace QMatrixClient QVector<PushRule> sender; QVector<PushRule> underride; }; - - QJsonObject toJson(const PushRuleset& pod); - - template <> struct FromJsonObject<PushRuleset> + template <> struct JsonObjectConverter<PushRuleset> { - PushRuleset operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushRuleset& pod); + static void fillFrom(const QJsonObject& jo, PushRuleset& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/room_event_filter.cpp b/lib/csapi/definitions/room_event_filter.cpp index f6f1e5cb..df92e684 100644 --- a/lib/csapi/definitions/room_event_filter.cpp +++ b/lib/csapi/definitions/room_event_filter.cpp @@ -6,25 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RoomEventFilter& pod) +void JsonObjectConverter<RoomEventFilter>::dumpTo( + QJsonObject& jo, const RoomEventFilter& pod) { - QJsonObject jo; + fillJson<EventFilter>(jo, pod); addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); addParam<IfNotEmpty>(jo, QStringLiteral("contains_url"), pod.containsUrl); - return jo; } -RoomEventFilter FromJsonObject<RoomEventFilter>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<RoomEventFilter>::fillFrom( + const QJsonObject& jo, RoomEventFilter& result) { - RoomEventFilter result; - result.notRooms = - fromJson<QStringList>(jo.value("not_rooms"_ls)); - result.rooms = - fromJson<QStringList>(jo.value("rooms"_ls)); - result.containsUrl = - fromJson<bool>(jo.value("contains_url"_ls)); - - return result; + fillFromJson<EventFilter>(jo, result); + fromJson(jo.value("not_rooms"_ls), result.notRooms); + fromJson(jo.value("rooms"_ls), result.rooms); + fromJson(jo.value("contains_url"_ls), result.containsUrl); } diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 697fe661..6eb9a390 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -19,15 +19,13 @@ namespace QMatrixClient QStringList notRooms; /// A list of room IDs to include. If this list is absent then all rooms are included. QStringList rooms; - /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. Defaults to ``false``. - bool containsUrl; + /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. If omitted, ``url`` key is not considered for filtering. + Omittable<bool> containsUrl; }; - - QJsonObject toJson(const RoomEventFilter& pod); - - template <> struct FromJsonObject<RoomEventFilter> + template <> struct JsonObjectConverter<RoomEventFilter> { - RoomEventFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod); + static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/sync_filter.cpp b/lib/csapi/definitions/sync_filter.cpp index bd87804c..32752d1f 100644 --- a/lib/csapi/definitions/sync_filter.cpp +++ b/lib/csapi/definitions/sync_filter.cpp @@ -6,9 +6,25 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RoomFilter& pod) +void JsonObjectConverter<StateFilter>::dumpTo( + QJsonObject& jo, const StateFilter& pod) +{ + fillJson<RoomEventFilter>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); + addParam<IfNotEmpty>(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers); +} + +void JsonObjectConverter<StateFilter>::fillFrom( + const QJsonObject& jo, StateFilter& result) +{ + fillFromJson<RoomEventFilter>(jo, result); + fromJson(jo.value("lazy_load_members"_ls), result.lazyLoadMembers); + fromJson(jo.value("include_redundant_members"_ls), result.includeRedundantMembers); +} + +void JsonObjectConverter<RoomFilter>::dumpTo( + QJsonObject& jo, const RoomFilter& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); addParam<IfNotEmpty>(jo, QStringLiteral("ephemeral"), pod.ephemeral); @@ -16,55 +32,37 @@ QJsonObject QMatrixClient::toJson(const RoomFilter& pod) addParam<IfNotEmpty>(jo, QStringLiteral("state"), pod.state); addParam<IfNotEmpty>(jo, QStringLiteral("timeline"), pod.timeline); addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData); - return jo; } -RoomFilter FromJsonObject<RoomFilter>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<RoomFilter>::fillFrom( + const QJsonObject& jo, RoomFilter& result) { - RoomFilter result; - result.notRooms = - fromJson<QStringList>(jo.value("not_rooms"_ls)); - result.rooms = - fromJson<QStringList>(jo.value("rooms"_ls)); - result.ephemeral = - fromJson<RoomEventFilter>(jo.value("ephemeral"_ls)); - result.includeLeave = - fromJson<bool>(jo.value("include_leave"_ls)); - result.state = - fromJson<RoomEventFilter>(jo.value("state"_ls)); - result.timeline = - fromJson<RoomEventFilter>(jo.value("timeline"_ls)); - result.accountData = - fromJson<RoomEventFilter>(jo.value("account_data"_ls)); - - return result; + fromJson(jo.value("not_rooms"_ls), result.notRooms); + fromJson(jo.value("rooms"_ls), result.rooms); + fromJson(jo.value("ephemeral"_ls), result.ephemeral); + fromJson(jo.value("include_leave"_ls), result.includeLeave); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("timeline"_ls), result.timeline); + fromJson(jo.value("account_data"_ls), result.accountData); } -QJsonObject QMatrixClient::toJson(const Filter& pod) +void JsonObjectConverter<Filter>::dumpTo( + QJsonObject& jo, const Filter& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("event_fields"), pod.eventFields); addParam<IfNotEmpty>(jo, QStringLiteral("event_format"), pod.eventFormat); addParam<IfNotEmpty>(jo, QStringLiteral("presence"), pod.presence); addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData); addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); - return jo; } -Filter FromJsonObject<Filter>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<Filter>::fillFrom( + const QJsonObject& jo, Filter& result) { - Filter result; - result.eventFields = - fromJson<QStringList>(jo.value("event_fields"_ls)); - result.eventFormat = - fromJson<QString>(jo.value("event_format"_ls)); - result.presence = - fromJson<EventFilter>(jo.value("presence"_ls)); - result.accountData = - fromJson<EventFilter>(jo.value("account_data"_ls)); - result.room = - fromJson<RoomFilter>(jo.value("room"_ls)); - - return result; + fromJson(jo.value("event_fields"_ls), result.eventFields); + fromJson(jo.value("event_format"_ls), result.eventFormat); + fromJson(jo.value("presence"_ls), result.presence); + fromJson(jo.value("account_data"_ls), result.accountData); + fromJson(jo.value("room"_ls), result.room); } diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h index ca275a9a..d94c74d7 100644 --- a/lib/csapi/definitions/sync_filter.h +++ b/lib/csapi/definitions/sync_filter.h @@ -14,6 +14,37 @@ namespace QMatrixClient { // Data structures + /// The state events to include for rooms. + struct StateFilter : RoomEventFilter + { + /// If ``true``, the only ``m.room.member`` events returned in + /// the ``state`` section of the ``/sync`` response are those + /// which are definitely necessary for a client to display + /// the ``sender`` of the timeline events in that response. + /// If ``false``, ``m.room.member`` events are not filtered. + /// By default, servers should suppress duplicate redundant + /// lazy-loaded ``m.room.member`` events from being sent to a given + /// client across multiple calls to ``/sync``, given that most clients + /// cache membership events (see ``include_redundant_members`` + /// to change this behaviour). + Omittable<bool> lazyLoadMembers; + /// If ``true``, the ``state`` section of the ``/sync`` response will + /// always contain the ``m.room.member`` events required to display + /// the ``sender`` of the timeline events in that response, assuming + /// ``lazy_load_members`` is enabled. This means that redundant + /// duplicate member events may be returned across multiple calls to + /// ``/sync``. This is useful for naive clients who never track + /// membership data. If ``false``, duplicate ``m.room.member`` events + /// may be suppressed by the server across multiple calls to ``/sync``. + /// If ``lazy_load_members`` is ``false`` this field is ignored. + Omittable<bool> includeRedundantMembers; + }; + template <> struct JsonObjectConverter<StateFilter> + { + static void dumpTo(QJsonObject& jo, const StateFilter& pod); + static void fillFrom(const QJsonObject& jo, StateFilter& pod); + }; + /// Filters to be applied to room data. struct RoomFilter { @@ -24,20 +55,18 @@ namespace QMatrixClient /// The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. Omittable<RoomEventFilter> ephemeral; /// Include rooms that the user has left in the sync, default false - bool includeLeave; + Omittable<bool> includeLeave; /// The state events to include for rooms. - Omittable<RoomEventFilter> state; + Omittable<StateFilter> state; /// The message and state update events to include for rooms. Omittable<RoomEventFilter> timeline; /// The per user account data to include for rooms. Omittable<RoomEventFilter> accountData; }; - - QJsonObject toJson(const RoomFilter& pod); - - template <> struct FromJsonObject<RoomFilter> + template <> struct JsonObjectConverter<RoomFilter> { - RoomFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RoomFilter& pod); + static void fillFrom(const QJsonObject& jo, RoomFilter& pod); }; struct Filter @@ -53,12 +82,10 @@ namespace QMatrixClient /// Filters to be applied to room data. Omittable<RoomFilter> room; }; - - QJsonObject toJson(const Filter& pod); - - template <> struct FromJsonObject<Filter> + template <> struct JsonObjectConverter<Filter> { - Filter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Filter& pod); + static void fillFrom(const QJsonObject& jo, Filter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/user_identifier.cpp b/lib/csapi/definitions/user_identifier.cpp index 80a6d450..05a27c1c 100644 --- a/lib/csapi/definitions/user_identifier.cpp +++ b/lib/csapi/definitions/user_identifier.cpp @@ -6,20 +6,18 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const UserIdentifier& pod) +void JsonObjectConverter<UserIdentifier>::dumpTo( + QJsonObject& jo, const UserIdentifier& pod) { - QJsonObject jo = toJson(pod.additionalProperties); + fillJson(jo, pod.additionalProperties); addParam<>(jo, QStringLiteral("type"), pod.type); - return jo; } -UserIdentifier FromJsonObject<UserIdentifier>::operator()(QJsonObject jo) const +void JsonObjectConverter<UserIdentifier>::fillFrom( + QJsonObject jo, UserIdentifier& result) { - UserIdentifier result; - result.type = - fromJson<QString>(jo.take("type"_ls)); + fromJson(jo.take("type"_ls), result.type); - result.additionalProperties = fromJson<QVariantHash>(jo); - return result; + fromJson(jo, result.additionalProperties); } diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h index 42614436..cbb1550f 100644 --- a/lib/csapi/definitions/user_identifier.h +++ b/lib/csapi/definitions/user_identifier.h @@ -20,12 +20,10 @@ namespace QMatrixClient /// Identification information for a user QVariantHash additionalProperties; }; - - QJsonObject toJson(const UserIdentifier& pod); - - template <> struct FromJsonObject<UserIdentifier> + template <> struct JsonObjectConverter<UserIdentifier> { - UserIdentifier operator()(QJsonObject jo) const; + static void dumpTo(QJsonObject& jo, const UserIdentifier& pod); + static void fillFrom(QJsonObject jo, UserIdentifier& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/wellknown/full.cpp b/lib/csapi/definitions/wellknown/full.cpp new file mode 100644 index 00000000..5ecef34f --- /dev/null +++ b/lib/csapi/definitions/wellknown/full.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "full.h" + +using namespace QMatrixClient; + +void JsonObjectConverter<DiscoveryInformation>::dumpTo( + QJsonObject& jo, const DiscoveryInformation& pod) +{ + fillJson(jo, pod.additionalProperties); + addParam<>(jo, QStringLiteral("m.homeserver"), pod.homeserver); + addParam<IfNotEmpty>(jo, QStringLiteral("m.identity_server"), pod.identityServer); +} + +void JsonObjectConverter<DiscoveryInformation>::fillFrom( + QJsonObject jo, DiscoveryInformation& result) +{ + fromJson(jo.take("m.homeserver"_ls), result.homeserver); + fromJson(jo.take("m.identity_server"_ls), result.identityServer); + + fromJson(jo, result.additionalProperties); +} + diff --git a/lib/csapi/definitions/wellknown/full.h b/lib/csapi/definitions/wellknown/full.h new file mode 100644 index 00000000..d9346acb --- /dev/null +++ b/lib/csapi/definitions/wellknown/full.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +#include <QtCore/QJsonObject> +#include "converters.h" +#include "csapi/definitions/wellknown/homeserver.h" +#include "csapi/definitions/wellknown/identity_server.h" +#include <QtCore/QHash> + +namespace QMatrixClient +{ + // Data structures + + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + struct DiscoveryInformation + { + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + HomeserverInformation homeserver; + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + Omittable<IdentityServerInformation> identityServer; + /// Application-dependent keys using Java package naming convention. + QHash<QString, QJsonObject> additionalProperties; + }; + template <> struct JsonObjectConverter<DiscoveryInformation> + { + static void dumpTo(QJsonObject& jo, const DiscoveryInformation& pod); + static void fillFrom(QJsonObject jo, DiscoveryInformation& pod); + }; + +} // namespace QMatrixClient diff --git a/lib/csapi/definitions/wellknown/homeserver.cpp b/lib/csapi/definitions/wellknown/homeserver.cpp index f1482ee4..0783f11b 100644 --- a/lib/csapi/definitions/wellknown/homeserver.cpp +++ b/lib/csapi/definitions/wellknown/homeserver.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const HomeserverInformation& pod) +void JsonObjectConverter<HomeserverInformation>::dumpTo( + QJsonObject& jo, const HomeserverInformation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); - return jo; } -HomeserverInformation FromJsonObject<HomeserverInformation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<HomeserverInformation>::fillFrom( + const QJsonObject& jo, HomeserverInformation& result) { - HomeserverInformation result; - result.baseUrl = - fromJson<QString>(jo.value("base_url"_ls)); - - return result; + fromJson(jo.value("base_url"_ls), result.baseUrl); } diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h index 09d6ba63..f6761c30 100644 --- a/lib/csapi/definitions/wellknown/homeserver.h +++ b/lib/csapi/definitions/wellknown/homeserver.h @@ -17,12 +17,10 @@ namespace QMatrixClient /// The base URL for the homeserver for client-server connections. QString baseUrl; }; - - QJsonObject toJson(const HomeserverInformation& pod); - - template <> struct FromJsonObject<HomeserverInformation> + template <> struct JsonObjectConverter<HomeserverInformation> { - HomeserverInformation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const HomeserverInformation& pod); + static void fillFrom(const QJsonObject& jo, HomeserverInformation& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/wellknown/identity_server.cpp b/lib/csapi/definitions/wellknown/identity_server.cpp index f9d7bc37..99f36641 100644 --- a/lib/csapi/definitions/wellknown/identity_server.cpp +++ b/lib/csapi/definitions/wellknown/identity_server.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const IdentityServerInformation& pod) +void JsonObjectConverter<IdentityServerInformation>::dumpTo( + QJsonObject& jo, const IdentityServerInformation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); - return jo; } -IdentityServerInformation FromJsonObject<IdentityServerInformation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<IdentityServerInformation>::fillFrom( + const QJsonObject& jo, IdentityServerInformation& result) { - IdentityServerInformation result; - result.baseUrl = - fromJson<QString>(jo.value("base_url"_ls)); - - return result; + fromJson(jo.value("base_url"_ls), result.baseUrl); } diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h index cb8ffcee..67d8b08d 100644 --- a/lib/csapi/definitions/wellknown/identity_server.h +++ b/lib/csapi/definitions/wellknown/identity_server.h @@ -17,12 +17,10 @@ namespace QMatrixClient /// The base URL for the identity server for client-server connections. QString baseUrl; }; - - QJsonObject toJson(const IdentityServerInformation& pod); - - template <> struct FromJsonObject<IdentityServerInformation> + template <> struct JsonObjectConverter<IdentityServerInformation> { - IdentityServerInformation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const IdentityServerInformation& pod); + static void fillFrom(const QJsonObject& jo, IdentityServerInformation& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index 861e1994..9c31db5d 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -43,7 +43,7 @@ const QVector<Device>& GetDevicesJob::devices() const BaseJob::Status GetDevicesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->devices = fromJson<QVector<Device>>(json.value("devices"_ls)); + fromJson(json.value("devices"_ls), d->devices); return Success; } @@ -77,7 +77,7 @@ const Device& GetDeviceJob::data() const BaseJob::Status GetDeviceJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Device>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 5353f3bc..4af86f7b 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -60,8 +60,8 @@ const QStringList& GetRoomIdByAliasJob::servers() const BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->roomId = fromJson<QString>(json.value("room_id"_ls)); - d->servers = fromJson<QStringList>(json.value("servers"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); + fromJson(json.value("servers"_ls), d->servers); return Success; } diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index 806c1613..bb1f5301 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -82,12 +82,12 @@ StateEvents&& GetEventContextJob::state() BaseJob::Status GetEventContextJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->eventsBefore = fromJson<RoomEvents>(json.value("events_before"_ls)); - d->event = fromJson<RoomEventPtr>(json.value("event"_ls)); - d->eventsAfter = fromJson<RoomEvents>(json.value("events_after"_ls)); - d->state = fromJson<StateEvents>(json.value("state"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("events_before"_ls), d->eventsBefore); + fromJson(json.value("event"_ls), d->event); + fromJson(json.value("events_after"_ls), d->eventsAfter); + fromJson(json.value("state"_ls), d->state); return Success; } diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 77dc9b92..982e60b5 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -41,7 +41,7 @@ BaseJob::Status DefineFilterJob::parseJson(const QJsonDocument& data) if (!json.contains("filter_id"_ls)) return { JsonParseError, "The key 'filter_id' not found in the response" }; - d->filterId = fromJson<QString>(json.value("filter_id"_ls)); + fromJson(json.value("filter_id"_ls), d->filterId); return Success; } @@ -75,7 +75,7 @@ const Filter& GetFilterJob::data() const BaseJob::Status GetFilterJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Filter>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml index cb5e553c..a44f803a 100644 --- a/lib/csapi/gtad.yaml +++ b/lib/csapi/gtad.yaml @@ -5,12 +5,15 @@ analyzer: identifiers: signed: signedData unsigned: unsignedData - default: isDefault + PushRule/default: isDefault + default: defaultVersion # getCapabilities/RoomVersionsCapability origin_server_ts: originServerTimestamp # Instead of originServerTs start: begin # Because start() is a method in BaseJob m.upload.size: uploadSize m.homeserver: homeserver m.identity_server: identityServer + m.change_password: changePassword + m.room_versions: roomVersions AuthenticationData/additionalProperties: authInfo # Structure inside `types`: @@ -38,7 +41,7 @@ analyzer: - number: - float: float - //: double - - boolean: { type: bool, omittedValue: 'false' } + - boolean: bool - string: - byte: &ByteStream type: QIODevice* @@ -86,12 +89,9 @@ analyzer: - /m\.room\.member$/: type: "EventsArray<RoomMemberEvent>" imports: '"events/roommemberevent.h"' - - /state_event.yaml$/: - type: StateEvents - - /room_event.yaml$/: - type: RoomEvents - - /event.yaml$/: - type: Events + - /state_event.yaml$/: StateEvents + - /room_event.yaml$/: RoomEvents + - /event.yaml$/: Events - //: { type: "QVector<{{1}}>", imports: <QtCore/QVector> } - map: # `additionalProperties` in OpenAPI - RoomState: diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 71781154..00d930fa 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -16,15 +16,16 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const JoinRoomByIdJob::ThirdPartySigned& pod) + template <> struct JsonObjectConverter<JoinRoomByIdJob::ThirdPartySigned> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; - } + static void dumpTo(QJsonObject& jo, const JoinRoomByIdJob::ThirdPartySigned& pod) + { + addParam<>(jo, QStringLiteral("sender"), pod.sender); + addParam<>(jo, QStringLiteral("mxid"), pod.mxid); + addParam<>(jo, QStringLiteral("token"), pod.token); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + }; } // namespace QMatrixClient class JoinRoomByIdJob::Private @@ -58,7 +59,7 @@ BaseJob::Status JoinRoomByIdJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } @@ -66,22 +67,24 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const JoinRoomJob::Signed& pod) + template <> struct JsonObjectConverter<JoinRoomJob::Signed> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; - } - - QJsonObject toJson(const JoinRoomJob::ThirdPartySigned& pod) + static void dumpTo(QJsonObject& jo, const JoinRoomJob::Signed& pod) + { + addParam<>(jo, QStringLiteral("sender"), pod.sender); + addParam<>(jo, QStringLiteral("mxid"), pod.mxid); + addParam<>(jo, QStringLiteral("token"), pod.token); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + }; + + template <> struct JsonObjectConverter<JoinRoomJob::ThirdPartySigned> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("signed"), pod.signedData); - return jo; - } + static void dumpTo(QJsonObject& jo, const JoinRoomJob::ThirdPartySigned& pod) + { + addParam<>(jo, QStringLiteral("signed"), pod.signedData); + } + }; } // namespace QMatrixClient class JoinRoomJob::Private @@ -123,7 +126,7 @@ BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index 137afbfc..52c8ea42 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -59,7 +59,7 @@ namespace QMatrixClient // Result properties - /// The joined room id + /// The joined room ID. const QString& roomId() const; protected: @@ -138,7 +138,7 @@ namespace QMatrixClient // Result properties - /// The joined room id + /// The joined room ID. const QString& roomId() const; protected: diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index c7492411..6c16a8a3 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -44,7 +44,7 @@ BaseJob::Status UploadKeysJob::parseJson(const QJsonDocument& data) if (!json.contains("one_time_key_counts"_ls)) return { JsonParseError, "The key 'one_time_key_counts' not found in the response" }; - d->oneTimeKeyCounts = fromJson<QHash<QString, int>>(json.value("one_time_key_counts"_ls)); + fromJson(json.value("one_time_key_counts"_ls), d->oneTimeKeyCounts); return Success; } @@ -52,27 +52,20 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<QueryKeysJob::UnsignedDeviceInfo> + template <> struct JsonObjectConverter<QueryKeysJob::UnsignedDeviceInfo> { - QueryKeysJob::UnsignedDeviceInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, QueryKeysJob::UnsignedDeviceInfo& result) { - QueryKeysJob::UnsignedDeviceInfo result; - result.deviceDisplayName = - fromJson<QString>(jo.value("device_display_name"_ls)); - - return result; + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); } }; - template <> struct FromJsonObject<QueryKeysJob::DeviceInformation> + template <> struct JsonObjectConverter<QueryKeysJob::DeviceInformation> { - QueryKeysJob::DeviceInformation operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, QueryKeysJob::DeviceInformation& result) { - QueryKeysJob::DeviceInformation result; - result.unsignedData = - fromJson<QueryKeysJob::UnsignedDeviceInfo>(jo.value("unsigned"_ls)); - - return result; + fillFromJson<DeviceKeys>(jo, result); + fromJson(jo.value("unsigned"_ls), result.unsignedData); } }; } // namespace QMatrixClient @@ -113,8 +106,8 @@ const QHash<QString, QHash<QString, QueryKeysJob::DeviceInformation>>& QueryKeys BaseJob::Status QueryKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->failures = fromJson<QHash<QString, QJsonObject>>(json.value("failures"_ls)); - d->deviceKeys = fromJson<QHash<QString, QHash<QString, DeviceInformation>>>(json.value("device_keys"_ls)); + fromJson(json.value("failures"_ls), d->failures); + fromJson(json.value("device_keys"_ls), d->deviceKeys); return Success; } @@ -153,8 +146,8 @@ const QHash<QString, QHash<QString, QVariant>>& ClaimKeysJob::oneTimeKeys() cons BaseJob::Status ClaimKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->failures = fromJson<QHash<QString, QJsonObject>>(json.value("failures"_ls)); - d->oneTimeKeys = fromJson<QHash<QString, QHash<QString, QVariant>>>(json.value("one_time_keys"_ls)); + fromJson(json.value("failures"_ls), d->failures); + fromJson(json.value("one_time_keys"_ls), d->oneTimeKeys); return Success; } @@ -205,8 +198,8 @@ const QStringList& GetKeysChangesJob::left() const BaseJob::Status GetKeysChangesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->changed = fromJson<QStringList>(json.value("changed"_ls)); - d->left = fromJson<QStringList>(json.value("left"_ls)); + fromJson(json.value("changed"_ls), d->changed); + fromJson(json.value("left"_ls), d->left); return Success; } diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 5968187e..714079cf 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -29,7 +29,7 @@ namespace QMatrixClient * \param userId * The fully qualified user ID of the user being kicked. * \param reason - * The reason the user has been kicked. This will be supplied as the + * The reason the user has been kicked. This will be supplied as the * ``reason`` on the target's updated `m.room.member`_ event. */ explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index a745dba1..85a9cae4 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetJoinedRoomsJob::parseJson(const QJsonDocument& data) if (!json.contains("joined_rooms"_ls)) return { JsonParseError, "The key 'joined_rooms' not found in the response" }; - d->joinedRooms = fromJson<QStringList>(json.value("joined_rooms"_ls)); + fromJson(json.value("joined_rooms"_ls), d->joinedRooms); return Success; } diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 2fdb2005..71b3c541 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -43,7 +43,7 @@ const QString& GetRoomVisibilityOnDirectoryJob::visibility() const BaseJob::Status GetRoomVisibilityOnDirectoryJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->visibility = fromJson<QString>(json.value("visibility"_ls)); + fromJson(json.value("visibility"_ls), d->visibility); return Success; } @@ -100,7 +100,7 @@ const PublicRoomsResponse& GetPublicRoomsJob::data() const BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<PublicRoomsResponse>(data); + fromJson(data, d->data); return Success; } @@ -108,12 +108,13 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const QueryPublicRoomsJob::Filter& pod) + template <> struct JsonObjectConverter<QueryPublicRoomsJob::Filter> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); - return jo; - } + static void dumpTo(QJsonObject& jo, const QueryPublicRoomsJob::Filter& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); + } + }; } // namespace QMatrixClient class QueryPublicRoomsJob::Private @@ -131,7 +132,7 @@ BaseJob::Query queryToQueryPublicRooms(const QString& server) static const auto QueryPublicRoomsJobName = QStringLiteral("QueryPublicRoomsJob"); -QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<int> limit, const QString& since, const Omittable<Filter>& filter, bool includeAllNetworks, const QString& thirdPartyInstanceId) +QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<int> limit, const QString& since, const Omittable<Filter>& filter, Omittable<bool> includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QueryPublicRoomsJobName, basePath % "/publicRooms", queryToQueryPublicRooms(server)) @@ -155,7 +156,7 @@ const PublicRoomsResponse& QueryPublicRoomsJob::data() const BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<PublicRoomsResponse>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 8401c134..a6498745 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -156,7 +156,7 @@ namespace QMatrixClient * The specific third party network/protocol to request from the * homeserver. Can only be used if ``include_all_networks`` is false. */ - explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, const QString& since = {}, const Omittable<Filter>& filter = none, bool includeAllNetworks = false, const QString& thirdPartyInstanceId = {}); + explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, const QString& since = {}, const Omittable<Filter>& filter = none, Omittable<bool> includeAllNetworks = none, const QString& thirdPartyInstanceId = {}); ~QueryPublicRoomsJob() override; // Result properties diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index 4d15a30b..5e369b9a 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -16,15 +16,11 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetLoginFlowsJob::LoginFlow> + template <> struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> { - GetLoginFlowsJob::LoginFlow operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetLoginFlowsJob::LoginFlow& result) { - GetLoginFlowsJob::LoginFlow result; - result.type = - fromJson<QString>(jo.value("type"_ls)); - - return result; + fromJson(jo.value("type"_ls), result.type); } }; } // namespace QMatrixClient @@ -60,7 +56,7 @@ const QVector<GetLoginFlowsJob::LoginFlow>& GetLoginFlowsJob::flows() const BaseJob::Status GetLoginFlowsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->flows = fromJson<QVector<LoginFlow>>(json.value("flows"_ls)); + fromJson(json.value("flows"_ls), d->flows); return Success; } @@ -71,6 +67,7 @@ class LoginJob::Private QString accessToken; QString homeServer; QString deviceId; + Omittable<DiscoveryInformation> wellKnown; }; static const auto LoginJobName = QStringLiteral("LoginJob"); @@ -115,13 +112,19 @@ const QString& LoginJob::deviceId() const return d->deviceId; } +const Omittable<DiscoveryInformation>& LoginJob::wellKnown() const +{ + return d->wellKnown; +} + BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); - d->homeServer = fromJson<QString>(json.value("home_server"_ls)); - d->deviceId = fromJson<QString>(json.value("device_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("access_token"_ls), d->accessToken); + fromJson(json.value("home_server"_ls), d->homeServer); + fromJson(json.value("device_id"_ls), d->deviceId); + fromJson(json.value("well_known"_ls), d->wellKnown); return Success; } diff --git a/lib/csapi/login.h b/lib/csapi/login.h index 957d8881..648316df 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -7,6 +7,7 @@ #include "jobs/basejob.h" #include <QtCore/QVector> +#include "csapi/definitions/wellknown/full.h" #include "csapi/definitions/user_identifier.h" #include "converters.h" @@ -118,6 +119,11 @@ namespace QMatrixClient /// ID of the logged-in device. Will be the same as the /// corresponding parameter in the request, if one was specified. const QString& deviceId() const; + /// Optional client configuration provided by the server. If present, + /// clients SHOULD use the provided object to reconfigure themselves, + /// optionally validating the URLs within. This object takes the same + /// form as the one returned from .well-known autodiscovery. + const Omittable<DiscoveryInformation>& wellKnown() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index c59a51ab..9aca7ec9 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -68,9 +68,9 @@ RoomEvents&& GetRoomEventsJob::chunk() BaseJob::Status GetRoomEventsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->chunk = fromJson<RoomEvents>(json.value("chunk"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index 785a0a8a..c00b7cb0 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -16,25 +16,16 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetNotificationsJob::Notification> + template <> struct JsonObjectConverter<GetNotificationsJob::Notification> { - GetNotificationsJob::Notification operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetNotificationsJob::Notification& result) { - GetNotificationsJob::Notification result; - result.actions = - fromJson<QVector<QVariant>>(jo.value("actions"_ls)); - result.event = - fromJson<EventPtr>(jo.value("event"_ls)); - result.profileTag = - fromJson<QString>(jo.value("profile_tag"_ls)); - result.read = - fromJson<bool>(jo.value("read"_ls)); - result.roomId = - fromJson<QString>(jo.value("room_id"_ls)); - result.ts = - fromJson<int>(jo.value("ts"_ls)); - - return result; + fromJson(jo.value("actions"_ls), result.actions); + fromJson(jo.value("event"_ls), result.event); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("read"_ls), result.read); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("ts"_ls), result.ts); } }; } // namespace QMatrixClient @@ -87,11 +78,11 @@ std::vector<GetNotificationsJob::Notification>&& GetNotificationsJob::notificati BaseJob::Status GetNotificationsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->nextToken = fromJson<QString>(json.value("next_token"_ls)); + fromJson(json.value("next_token"_ls), d->nextToken); if (!json.contains("notifications"_ls)) return { JsonParseError, "The key 'notifications' not found in the response" }; - d->notifications = fromJson<std::vector<Notification>>(json.value("notifications"_ls)); + fromJson(json.value("notifications"_ls), d->notifications); return Success; } diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 2547f0c8..b27fe0b8 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -59,19 +59,19 @@ BaseJob::Status RequestOpenIdTokenJob::parseJson(const QJsonDocument& data) if (!json.contains("access_token"_ls)) return { JsonParseError, "The key 'access_token' not found in the response" }; - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); + fromJson(json.value("access_token"_ls), d->accessToken); if (!json.contains("token_type"_ls)) return { JsonParseError, "The key 'token_type' not found in the response" }; - d->tokenType = fromJson<QString>(json.value("token_type"_ls)); + fromJson(json.value("token_type"_ls), d->tokenType); if (!json.contains("matrix_server_name"_ls)) return { JsonParseError, "The key 'matrix_server_name' not found in the response" }; - d->matrixServerName = fromJson<QString>(json.value("matrix_server_name"_ls)); + fromJson(json.value("matrix_server_name"_ls), d->matrixServerName); if (!json.contains("expires_in"_ls)) return { JsonParseError, "The key 'expires_in' not found in the response" }; - d->expiresIn = fromJson<int>(json.value("expires_in"_ls)); + fromJson(json.value("expires_in"_ls), d->expiresIn); return Success; } diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index e046a62e..3208d48d 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -66,9 +66,9 @@ RoomEvents&& PeekEventsJob::chunk() BaseJob::Status PeekEventsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->chunk = fromJson<RoomEvents>(json.value("chunk"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 7aba8b61..024d7a34 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -30,7 +30,7 @@ class GetPresenceJob::Private QString presence; Omittable<int> lastActiveAgo; QString statusMsg; - bool currentlyActive; + Omittable<bool> currentlyActive; }; QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) @@ -65,7 +65,7 @@ const QString& GetPresenceJob::statusMsg() const return d->statusMsg; } -bool GetPresenceJob::currentlyActive() const +Omittable<bool> GetPresenceJob::currentlyActive() const { return d->currentlyActive; } @@ -76,56 +76,10 @@ BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data) if (!json.contains("presence"_ls)) return { JsonParseError, "The key 'presence' not found in the response" }; - d->presence = fromJson<QString>(json.value("presence"_ls)); - d->lastActiveAgo = fromJson<int>(json.value("last_active_ago"_ls)); - d->statusMsg = fromJson<QString>(json.value("status_msg"_ls)); - d->currentlyActive = fromJson<bool>(json.value("currently_active"_ls)); - return Success; -} - -static const auto ModifyPresenceListJobName = QStringLiteral("ModifyPresenceListJob"); - -ModifyPresenceListJob::ModifyPresenceListJob(const QString& userId, const QStringList& invite, const QStringList& drop) - : BaseJob(HttpVerb::Post, ModifyPresenceListJobName, - basePath % "/presence/list/" % userId) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("invite"), invite); - addParam<IfNotEmpty>(_data, QStringLiteral("drop"), drop); - setRequestData(_data); -} - -class GetPresenceForListJob::Private -{ - public: - Events data; -}; - -QUrl GetPresenceForListJob::makeRequestUrl(QUrl baseUrl, const QString& userId) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/presence/list/" % userId); -} - -static const auto GetPresenceForListJobName = QStringLiteral("GetPresenceForListJob"); - -GetPresenceForListJob::GetPresenceForListJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetPresenceForListJobName, - basePath % "/presence/list/" % userId, false) - , d(new Private) -{ -} - -GetPresenceForListJob::~GetPresenceForListJob() = default; - -Events&& GetPresenceForListJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetPresenceForListJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<Events>(data); + fromJson(json.value("presence"_ls), d->presence); + fromJson(json.value("last_active_ago"_ls), d->lastActiveAgo); + fromJson(json.value("status_msg"_ls), d->statusMsg); + fromJson(json.value("currently_active"_ls), d->currentlyActive); return Success; } diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index 86b9d395..5e132d24 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -6,7 +6,6 @@ #include "jobs/basejob.h" -#include "events/eventloader.h" #include "converters.h" namespace QMatrixClient @@ -65,59 +64,7 @@ namespace QMatrixClient /// The state message for this user if one was set. const QString& statusMsg() const; /// Whether the user is currently active - bool currentlyActive() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Add or remove users from this presence list. - /// - /// Adds or removes users from this presence list. - class ModifyPresenceListJob : public BaseJob - { - public: - /*! Add or remove users from this presence list. - * \param userId - * The user whose presence list is being modified. - * \param invite - * A list of user IDs to add to the list. - * \param drop - * A list of user IDs to remove from the list. - */ - explicit ModifyPresenceListJob(const QString& userId, const QStringList& invite = {}, const QStringList& drop = {}); - }; - - /// Get presence events for this presence list. - /// - /// Retrieve a list of presence events for every user on this list. - class GetPresenceForListJob : public BaseJob - { - public: - /*! Get presence events for this presence list. - * \param userId - * The user whose presence list should be retrieved. - */ - explicit GetPresenceForListJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPresenceForListJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetPresenceForListJob() override; - - // Result properties - - /// A list of presence events for this list. - Events&& data(); + Omittable<bool> currentlyActive() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index bb053062..4ed3ad9b 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -54,7 +54,7 @@ const QString& GetDisplayNameJob::displayname() const BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->displayname = fromJson<QString>(json.value("displayname"_ls)); + fromJson(json.value("displayname"_ls), d->displayname); return Success; } @@ -100,7 +100,7 @@ const QString& GetAvatarUrlJob::avatarUrl() const BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->avatarUrl = fromJson<QString>(json.value("avatar_url"_ls)); + fromJson(json.value("avatar_url"_ls), d->avatarUrl); return Success; } @@ -141,8 +141,8 @@ const QString& GetUserProfileJob::displayname() const BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->avatarUrl = fromJson<QString>(json.value("avatar_url"_ls)); - d->displayname = fromJson<QString>(json.value("displayname"_ls)); + fromJson(json.value("avatar_url"_ls), d->avatarUrl); + fromJson(json.value("displayname"_ls), d->displayname); return Success; } diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index d20db88a..664959f4 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -16,43 +16,27 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetPushersJob::PusherData> + template <> struct JsonObjectConverter<GetPushersJob::PusherData> { - GetPushersJob::PusherData operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetPushersJob::PusherData& result) { - GetPushersJob::PusherData result; - result.url = - fromJson<QString>(jo.value("url"_ls)); - result.format = - fromJson<QString>(jo.value("format"_ls)); - - return result; + fromJson(jo.value("url"_ls), result.url); + fromJson(jo.value("format"_ls), result.format); } }; - template <> struct FromJsonObject<GetPushersJob::Pusher> + template <> struct JsonObjectConverter<GetPushersJob::Pusher> { - GetPushersJob::Pusher operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetPushersJob::Pusher& result) { - GetPushersJob::Pusher result; - result.pushkey = - fromJson<QString>(jo.value("pushkey"_ls)); - result.kind = - fromJson<QString>(jo.value("kind"_ls)); - result.appId = - fromJson<QString>(jo.value("app_id"_ls)); - result.appDisplayName = - fromJson<QString>(jo.value("app_display_name"_ls)); - result.deviceDisplayName = - fromJson<QString>(jo.value("device_display_name"_ls)); - result.profileTag = - fromJson<QString>(jo.value("profile_tag"_ls)); - result.lang = - fromJson<QString>(jo.value("lang"_ls)); - result.data = - fromJson<GetPushersJob::PusherData>(jo.value("data"_ls)); - - return result; + fromJson(jo.value("pushkey"_ls), result.pushkey); + fromJson(jo.value("kind"_ls), result.kind); + fromJson(jo.value("app_id"_ls), result.appId); + fromJson(jo.value("app_display_name"_ls), result.appDisplayName); + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("lang"_ls), result.lang); + fromJson(jo.value("data"_ls), result.data); } }; } // namespace QMatrixClient @@ -88,7 +72,7 @@ const QVector<GetPushersJob::Pusher>& GetPushersJob::pushers() const BaseJob::Status GetPushersJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->pushers = fromJson<QVector<Pusher>>(json.value("pushers"_ls)); + fromJson(json.value("pushers"_ls), d->pushers); return Success; } @@ -96,18 +80,19 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const PostPusherJob::PusherData& pod) + template <> struct JsonObjectConverter<PostPusherJob::PusherData> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url); - addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format); - return jo; - } + static void dumpTo(QJsonObject& jo, const PostPusherJob::PusherData& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url); + addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format); + } + }; } // namespace QMatrixClient static const auto PostPusherJobName = QStringLiteral("PostPusherJob"); -PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, bool append) +PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, Omittable<bool> append) : BaseJob(HttpVerb::Post, PostPusherJobName, basePath % "/pushers/set") { diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index 2b506183..da3303fe 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -164,6 +164,6 @@ namespace QMatrixClient * other pushers with the same App ID and pushkey for different * users. The default is ``false``. */ - explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, bool append = false); + explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, Omittable<bool> append = none); }; } // namespace QMatrixClient diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index ea8ad02a..b91d18f7 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetPushRulesJob::parseJson(const QJsonDocument& data) if (!json.contains("global"_ls)) return { JsonParseError, "The key 'global' not found in the response" }; - d->global = fromJson<PushRuleset>(json.value("global"_ls)); + fromJson(json.value("global"_ls), d->global); return Success; } @@ -80,7 +80,7 @@ const PushRule& GetPushRuleJob::data() const BaseJob::Status GetPushRuleJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<PushRule>(data); + fromJson(data, d->data); return Success; } @@ -154,7 +154,7 @@ BaseJob::Status IsPushRuleEnabledJob::parseJson(const QJsonDocument& data) if (!json.contains("enabled"_ls)) return { JsonParseError, "The key 'enabled' not found in the response" }; - d->enabled = fromJson<bool>(json.value("enabled"_ls)); + fromJson(json.value("enabled"_ls), d->enabled); return Success; } @@ -203,7 +203,7 @@ BaseJob::Status GetPushRuleActionsJob::parseJson(const QJsonDocument& data) if (!json.contains("actions"_ls)) return { JsonParseError, "The key 'actions' not found in the response" }; - d->actions = fromJson<QStringList>(json.value("actions"_ls)); + fromJson(json.value("actions"_ls), d->actions); return Success; } diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index f19f46b0..d982b477 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -26,7 +26,7 @@ namespace QMatrixClient * event MUST belong to the room. * \param mRead * The event ID to set the read receipt location at. This is - * equivalent to calling ``/receipt/m.read/$elsewhere:domain.com`` + * equivalent to calling ``/receipt/m.read/$elsewhere:example.org`` * and is provided here to save that extra call. */ explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead = {}); diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index 64098670..1d54e36d 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -40,7 +40,7 @@ const QString& RedactEventJob::eventId() const BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 320ec796..5dc9c1e5 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -30,7 +30,7 @@ BaseJob::Query queryToRegister(const QString& kind) static const auto RegisterJobName = QStringLiteral("RegisterJob"); -RegisterJob::RegisterJob(const QString& kind, const Omittable<AuthenticationData>& auth, bool bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, bool inhibitLogin) +RegisterJob::RegisterJob(const QString& kind, const Omittable<AuthenticationData>& auth, Omittable<bool> bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, Omittable<bool> inhibitLogin) : BaseJob(HttpVerb::Post, RegisterJobName, basePath % "/register", queryToRegister(kind), @@ -76,10 +76,10 @@ BaseJob::Status RegisterJob::parseJson(const QJsonDocument& data) if (!json.contains("user_id"_ls)) return { JsonParseError, "The key 'user_id' not found in the response" }; - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); - d->homeServer = fromJson<QString>(json.value("home_server"_ls)); - d->deviceId = fromJson<QString>(json.value("device_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("access_token"_ls), d->accessToken); + fromJson(json.value("home_server"_ls), d->homeServer); + fromJson(json.value("device_id"_ls), d->deviceId); return Success; } @@ -114,7 +114,7 @@ const Sid& RequestTokenToRegisterEmailJob::data() const BaseJob::Status RequestTokenToRegisterEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -150,7 +150,7 @@ const Sid& RequestTokenToRegisterMSISDNJob::data() const BaseJob::Status RequestTokenToRegisterMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -197,7 +197,7 @@ const Sid& RequestTokenToResetPasswordEmailJob::data() const BaseJob::Status RequestTokenToResetPasswordEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -233,7 +233,7 @@ const Sid& RequestTokenToResetPasswordMSISDNJob::data() const BaseJob::Status RequestTokenToResetPasswordMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -251,7 +251,7 @@ DeactivateAccountJob::DeactivateAccountJob(const Omittable<AuthenticationData>& class CheckUsernameAvailabilityJob::Private { public: - bool available; + Omittable<bool> available; }; BaseJob::Query queryToCheckUsernameAvailability(const QString& username) @@ -281,7 +281,7 @@ CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& userna CheckUsernameAvailabilityJob::~CheckUsernameAvailabilityJob() = default; -bool CheckUsernameAvailabilityJob::available() const +Omittable<bool> CheckUsernameAvailabilityJob::available() const { return d->available; } @@ -289,7 +289,7 @@ bool CheckUsernameAvailabilityJob::available() const BaseJob::Status CheckUsernameAvailabilityJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->available = fromJson<bool>(json.value("available"_ls)); + fromJson(json.value("available"_ls), d->available); return Success; } diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 9002b5c8..ca1a1c21 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -80,7 +80,7 @@ namespace QMatrixClient * returned from this call, therefore preventing an automatic * login. Defaults to false. */ - explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, bool bindEmail = false, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, bool inhibitLogin = false); + explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, Omittable<bool> bindEmail = none, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, Omittable<bool> inhibitLogin = none); ~RegisterJob() override; // Result properties @@ -418,7 +418,7 @@ namespace QMatrixClient /// A flag to indicate that the username is available. This should always /// be ``true`` when the server replies with 200 OK. - bool available() const; + Omittable<bool> available() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 2b39ede2..0d25eb69 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -38,7 +38,7 @@ const QString& SendMessageJob::eventId() const BaseJob::Status SendMessageJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index 8f87979d..3aa7d736 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -38,7 +38,7 @@ const QString& SetRoomStateWithKeyJob::eventId() const BaseJob::Status SetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } @@ -68,7 +68,7 @@ const QString& SetRoomStateJob::eventId() const BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp new file mode 100644 index 00000000..f58fd675 --- /dev/null +++ b/lib/csapi/room_upgrades.cpp @@ -0,0 +1,49 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "room_upgrades.h" + +#include "converters.h" + +#include <QtCore/QStringBuilder> + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +class UpgradeRoomJob::Private +{ + public: + QString replacementRoom; +}; + +static const auto UpgradeRoomJobName = QStringLiteral("UpgradeRoomJob"); + +UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) + : BaseJob(HttpVerb::Post, UpgradeRoomJobName, + basePath % "/rooms/" % roomId % "/upgrade") + , d(new Private) +{ + QJsonObject _data; + addParam<>(_data, QStringLiteral("new_version"), newVersion); + setRequestData(_data); +} + +UpgradeRoomJob::~UpgradeRoomJob() = default; + +const QString& UpgradeRoomJob::replacementRoom() const +{ + return d->replacementRoom; +} + +BaseJob::Status UpgradeRoomJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("replacement_room"_ls)) + return { JsonParseError, + "The key 'replacement_room' not found in the response" }; + fromJson(json.value("replacement_room"_ls), d->replacementRoom); + return Success; +} + diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h new file mode 100644 index 00000000..4da5941a --- /dev/null +++ b/lib/csapi/room_upgrades.h @@ -0,0 +1,41 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + + +namespace QMatrixClient +{ + // Operations + + /// Upgrades a room to a new room version. + /// + /// Upgrades the given room to a particular room version. + class UpgradeRoomJob : public BaseJob + { + public: + /*! Upgrades a room to a new room version. + * \param roomId + * The ID of the room to upgrade. + * \param newVersion + * The new version for the room. + */ + explicit UpgradeRoomJob(const QString& roomId, const QString& newVersion); + ~UpgradeRoomJob() override; + + // Result properties + + /// The ID of the new room. + const QString& replacementRoom() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer<Private> d; + }; +} // namespace QMatrixClient diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 3befeee5..0b08ccec 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -42,16 +42,10 @@ EventPtr&& GetOneRoomEventJob::data() BaseJob::Status GetOneRoomEventJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<EventPtr>(data); + fromJson(data, d->data); return Success; } -class GetRoomStateWithKeyJob::Private -{ - public: - StateEventPtr data; -}; - QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), @@ -63,29 +57,9 @@ static const auto GetRoomStateWithKeyJobName = QStringLiteral("GetRoomStateWithK GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) : BaseJob(HttpVerb::Get, GetRoomStateWithKeyJobName, basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) - , d(new Private) -{ -} - -GetRoomStateWithKeyJob::~GetRoomStateWithKeyJob() = default; - -StateEventPtr&& GetRoomStateWithKeyJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<StateEventPtr>(data); - return Success; } -class GetRoomStateByTypeJob::Private -{ - public: - StateEventPtr data; -}; - QUrl GetRoomStateByTypeJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType) { return BaseJob::makeRequestUrl(std::move(baseUrl), @@ -97,21 +71,7 @@ static const auto GetRoomStateByTypeJobName = QStringLiteral("GetRoomStateByType GetRoomStateByTypeJob::GetRoomStateByTypeJob(const QString& roomId, const QString& eventType) : BaseJob(HttpVerb::Get, GetRoomStateByTypeJobName, basePath % "/rooms/" % roomId % "/state/" % eventType) - , d(new Private) -{ -} - -GetRoomStateByTypeJob::~GetRoomStateByTypeJob() = default; - -StateEventPtr&& GetRoomStateByTypeJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetRoomStateByTypeJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<StateEventPtr>(data); - return Success; } class GetRoomStateJob::Private @@ -144,7 +104,7 @@ StateEvents&& GetRoomStateJob::data() BaseJob::Status GetRoomStateJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<StateEvents>(data); + fromJson(data, d->data); return Success; } @@ -154,17 +114,28 @@ class GetMembersByRoomJob::Private EventsArray<RoomMemberEvent> chunk; }; -QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +BaseJob::Query queryToGetMembersByRoom(const QString& at, const QString& membership, const QString& notMembership) +{ + BaseJob::Query _q; + addParam<IfNotEmpty>(_q, QStringLiteral("at"), at); + addParam<IfNotEmpty>(_q, QStringLiteral("membership"), membership); + addParam<IfNotEmpty>(_q, QStringLiteral("not_membership"), notMembership); + return _q; +} + +QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/members"); + basePath % "/rooms/" % roomId % "/members", + queryToGetMembersByRoom(at, membership, notMembership)); } static const auto GetMembersByRoomJobName = QStringLiteral("GetMembersByRoomJob"); -GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId) +GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) : BaseJob(HttpVerb::Get, GetMembersByRoomJobName, - basePath % "/rooms/" % roomId % "/members") + basePath % "/rooms/" % roomId % "/members", + queryToGetMembersByRoom(at, membership, notMembership)) , d(new Private) { } @@ -179,7 +150,7 @@ EventsArray<RoomMemberEvent>&& GetMembersByRoomJob::chunk() BaseJob::Status GetMembersByRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->chunk = fromJson<EventsArray<RoomMemberEvent>>(json.value("chunk"_ls)); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } @@ -187,17 +158,12 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetJoinedMembersByRoomJob::RoomMember> + template <> struct JsonObjectConverter<GetJoinedMembersByRoomJob::RoomMember> { - GetJoinedMembersByRoomJob::RoomMember operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetJoinedMembersByRoomJob::RoomMember& result) { - GetJoinedMembersByRoomJob::RoomMember result; - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; } // namespace QMatrixClient @@ -233,7 +199,7 @@ const QHash<QString, GetJoinedMembersByRoomJob::RoomMember>& GetJoinedMembersByR BaseJob::Status GetJoinedMembersByRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->joined = fromJson<QHash<QString, RoomMember>>(json.value("joined"_ls)); + fromJson(json.value("joined"_ls), d->joined); return Success; } diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 2366918b..b4d3d9b6 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -80,19 +80,6 @@ namespace QMatrixClient */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey); - ~GetRoomStateWithKeyJob() override; - - // Result properties - - /// The content of the state event. - StateEventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; }; /// Get the state identified by the type, with the empty state key. @@ -122,19 +109,6 @@ namespace QMatrixClient */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType); - ~GetRoomStateByTypeJob() override; - - // Result properties - - /// The content of the state event. - StateEventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; }; /// Get all state events in the current state of a room. @@ -184,8 +158,17 @@ namespace QMatrixClient /*! Get the m.room.member events for the room. * \param roomId * The room to get the member events for. + * \param at + * The token defining the timeline position as-of which to return + * the list of members. This token can be obtained from a batch token + * returned for each room by the sync API, or from + * a ``start``/``end`` token returned by a ``/messages`` request. + * \param membership + * Only return users with the specified membership + * \param notMembership + * Only return users with membership state other than specified */ - explicit GetMembersByRoomJob(const QString& roomId); + explicit GetMembersByRoomJob(const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); /*! Construct a URL without creating a full-fledged job object * @@ -193,7 +176,7 @@ namespace QMatrixClient * GetMembersByRoomJob is necessary but the job * itself isn't. */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); ~GetMembersByRoomJob() override; diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 9436eb47..a5f83c79 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -16,146 +16,113 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const SearchJob::IncludeEventContext& pod) + template <> struct JsonObjectConverter<SearchJob::IncludeEventContext> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), pod.beforeLimit); - addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit); - addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), pod.includeProfile); - return jo; - } - - QJsonObject toJson(const SearchJob::Group& pod) - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::IncludeEventContext& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), pod.beforeLimit); + addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit); + addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), pod.includeProfile); + } + }; - QJsonObject toJson(const SearchJob::Groupings& pod) + template <> struct JsonObjectConverter<SearchJob::Group> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); + } + }; - QJsonObject toJson(const SearchJob::RoomEventsCriteria& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); - addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys); - addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter); - addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy); - addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), pod.eventContext); - addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), pod.includeState); - addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings); - return jo; - } - - QJsonObject toJson(const SearchJob::Categories& pod) + template <> struct JsonObjectConverter<SearchJob::Groupings> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy); + } + }; - template <> struct FromJsonObject<SearchJob::UserProfile> + template <> struct JsonObjectConverter<SearchJob::RoomEventsCriteria> { - SearchJob::UserProfile operator()(const QJsonObject& jo) const + static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod) { - SearchJob::UserProfile result; - result.displayname = - fromJson<QString>(jo.value("displayname"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); + addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); + addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys); + addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter); + addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy); + addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), pod.eventContext); + addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), pod.includeState); + addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings); + } + }; - return result; + template <> struct JsonObjectConverter<SearchJob::Categories> + { + static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents); } }; - template <> struct FromJsonObject<SearchJob::EventContext> + template <> struct JsonObjectConverter<SearchJob::UserProfile> { - SearchJob::EventContext operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result) { - SearchJob::EventContext result; - result.begin = - fromJson<QString>(jo.value("start"_ls)); - result.end = - fromJson<QString>(jo.value("end"_ls)); - result.profileInfo = - fromJson<QHash<QString, SearchJob::UserProfile>>(jo.value("profile_info"_ls)); - result.eventsBefore = - fromJson<RoomEvents>(jo.value("events_before"_ls)); - result.eventsAfter = - fromJson<RoomEvents>(jo.value("events_after"_ls)); - - return result; + fromJson(jo.value("displayname"_ls), result.displayname); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; - template <> struct FromJsonObject<SearchJob::Result> + template <> struct JsonObjectConverter<SearchJob::EventContext> { - SearchJob::Result operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result) { - SearchJob::Result result; - result.rank = - fromJson<double>(jo.value("rank"_ls)); - result.result = - fromJson<RoomEventPtr>(jo.value("result"_ls)); - result.context = - fromJson<SearchJob::EventContext>(jo.value("context"_ls)); - - return result; + fromJson(jo.value("start"_ls), result.begin); + fromJson(jo.value("end"_ls), result.end); + fromJson(jo.value("profile_info"_ls), result.profileInfo); + fromJson(jo.value("events_before"_ls), result.eventsBefore); + fromJson(jo.value("events_after"_ls), result.eventsAfter); } }; - template <> struct FromJsonObject<SearchJob::GroupValue> + template <> struct JsonObjectConverter<SearchJob::Result> { - SearchJob::GroupValue operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::Result& result) { - SearchJob::GroupValue result; - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - result.order = - fromJson<int>(jo.value("order"_ls)); - result.results = - fromJson<QStringList>(jo.value("results"_ls)); - - return result; + fromJson(jo.value("rank"_ls), result.rank); + fromJson(jo.value("result"_ls), result.result); + fromJson(jo.value("context"_ls), result.context); } }; - template <> struct FromJsonObject<SearchJob::ResultRoomEvents> + template <> struct JsonObjectConverter<SearchJob::GroupValue> { - SearchJob::ResultRoomEvents operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result) { - SearchJob::ResultRoomEvents result; - result.count = - fromJson<int>(jo.value("count"_ls)); - result.highlights = - fromJson<QStringList>(jo.value("highlights"_ls)); - result.results = - fromJson<std::vector<SearchJob::Result>>(jo.value("results"_ls)); - result.state = - fromJson<std::unordered_map<QString, StateEvents>>(jo.value("state"_ls)); - result.groups = - fromJson<QHash<QString, QHash<QString, SearchJob::GroupValue>>>(jo.value("groups"_ls)); - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - - return result; + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("order"_ls), result.order); + fromJson(jo.value("results"_ls), result.results); } }; - template <> struct FromJsonObject<SearchJob::ResultCategories> + template <> struct JsonObjectConverter<SearchJob::ResultRoomEvents> { - SearchJob::ResultCategories operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::ResultRoomEvents& result) { - SearchJob::ResultCategories result; - result.roomEvents = - fromJson<SearchJob::ResultRoomEvents>(jo.value("room_events"_ls)); + fromJson(jo.value("count"_ls), result.count); + fromJson(jo.value("highlights"_ls), result.highlights); + fromJson(jo.value("results"_ls), result.results); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("groups"_ls), result.groups); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + } + }; - return result; + template <> struct JsonObjectConverter<SearchJob::ResultCategories> + { + static void fillFrom(const QJsonObject& jo, SearchJob::ResultCategories& result) + { + fromJson(jo.value("room_events"_ls), result.roomEvents); } }; } // namespace QMatrixClient @@ -199,7 +166,7 @@ BaseJob::Status SearchJob::parseJson(const QJsonDocument& data) if (!json.contains("search_categories"_ls)) return { JsonParseError, "The key 'search_categories' not found in the response" }; - d->searchCategories = fromJson<ResultCategories>(json.value("search_categories"_ls)); + fromJson(json.value("search_categories"_ls), d->searchCategories); return Success; } diff --git a/lib/csapi/search.h b/lib/csapi/search.h index 85b0886b..86a0ee92 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -39,7 +39,7 @@ namespace QMatrixClient /// historic profile information for the users /// that sent the events that were returned. /// By default, this is ``false``. - bool includeProfile; + Omittable<bool> includeProfile; }; /// Configuration for group. @@ -74,7 +74,7 @@ namespace QMatrixClient Omittable<IncludeEventContext> eventContext; /// Requests the server return the current state for /// each room returned. - bool includeState; + Omittable<bool> includeState; /// Requests that the server partitions the result set /// based on the provided list of keys. Omittable<Groupings> groupings; diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp new file mode 100644 index 00000000..7323951c --- /dev/null +++ b/lib/csapi/sso_login_redirect.cpp @@ -0,0 +1,38 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "sso_login_redirect.h" + +#include "converters.h" + +#include <QtCore/QStringBuilder> + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +BaseJob::Query queryToRedirectToSSO(const QString& redirectUrl) +{ + BaseJob::Query _q; + addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); + return _q; +} + +QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/login/sso/redirect", + queryToRedirectToSSO(redirectUrl)); +} + +static const auto RedirectToSSOJobName = QStringLiteral("RedirectToSSOJob"); + +RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) + : BaseJob(HttpVerb::Get, RedirectToSSOJobName, + basePath % "/login/sso/redirect", + queryToRedirectToSSO(redirectUrl), + {}, false) +{ +} + diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h new file mode 100644 index 00000000..c09365b0 --- /dev/null +++ b/lib/csapi/sso_login_redirect.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + + +namespace QMatrixClient +{ + // Operations + + /// Redirect the user's browser to the SSO interface. + /// + /// A web-based Matrix client should instruct the user's browser to + /// navigate to this endpoint in order to log in via SSO. + /// + /// The server MUST respond with an HTTP redirect to the SSO interface. + class RedirectToSSOJob : public BaseJob + { + public: + /*! Redirect the user's browser to the SSO interface. + * \param redirectUrl + * URI to which the user will be redirected after the homeserver has + * authenticated the user with SSO. + */ + explicit RedirectToSSOJob(const QString& redirectUrl); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * RedirectToSSOJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); + + }; +} // namespace QMatrixClient diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index 808915ac..94026bb9 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -16,16 +16,12 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetRoomTagsJob::Tag> + template <> struct JsonObjectConverter<GetRoomTagsJob::Tag> { - GetRoomTagsJob::Tag operator()(QJsonObject jo) const + static void fillFrom(QJsonObject jo, GetRoomTagsJob::Tag& result) { - GetRoomTagsJob::Tag result; - result.order = - fromJson<float>(jo.take("order"_ls)); - - result.additionalProperties = fromJson<QVariantHash>(jo); - return result; + fromJson(jo.take("order"_ls), result.order); + fromJson(jo, result.additionalProperties); } }; } // namespace QMatrixClient @@ -61,7 +57,7 @@ const QHash<QString, GetRoomTagsJob::Tag>& GetRoomTagsJob::tags() const BaseJob::Status GetRoomTagsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->tags = fromJson<QHash<QString, Tag>>(json.value("tags"_ls)); + fromJson(json.value("tags"_ls), d->tags); return Success; } diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index 3ba1a5ad..12cb7c59 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -42,7 +42,7 @@ const QHash<QString, ThirdPartyProtocol>& GetProtocolsJob::data() const BaseJob::Status GetProtocolsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QHash<QString, ThirdPartyProtocol>>(data); + fromJson(data, d->data); return Success; } @@ -76,7 +76,7 @@ const ThirdPartyProtocol& GetProtocolMetadataJob::data() const BaseJob::Status GetProtocolMetadataJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<ThirdPartyProtocol>(data); + fromJson(data, d->data); return Success; } @@ -119,7 +119,7 @@ const QVector<ThirdPartyLocation>& QueryLocationByProtocolJob::data() const BaseJob::Status QueryLocationByProtocolJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QVector<ThirdPartyLocation>>(data); + fromJson(data, d->data); return Success; } @@ -162,7 +162,7 @@ const QVector<ThirdPartyUser>& QueryUserByProtocolJob::data() const BaseJob::Status QueryUserByProtocolJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QVector<ThirdPartyUser>>(data); + fromJson(data, d->data); return Success; } @@ -205,7 +205,7 @@ const QVector<ThirdPartyLocation>& QueryLocationByAliasJob::data() const BaseJob::Status QueryLocationByAliasJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QVector<ThirdPartyLocation>>(data); + fromJson(data, d->data); return Success; } @@ -248,7 +248,7 @@ const QVector<ThirdPartyUser>& QueryUserByIDJob::data() const BaseJob::Status QueryUserByIDJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QVector<ThirdPartyUser>>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index deb9cb8a..97d8962d 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -16,19 +16,13 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<SearchUserDirectoryJob::User> + template <> struct JsonObjectConverter<SearchUserDirectoryJob::User> { - SearchUserDirectoryJob::User operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchUserDirectoryJob::User& result) { - SearchUserDirectoryJob::User result; - result.userId = - fromJson<QString>(jo.value("user_id"_ls)); - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("user_id"_ls), result.userId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; } // namespace QMatrixClient @@ -71,11 +65,11 @@ BaseJob::Status SearchUserDirectoryJob::parseJson(const QJsonDocument& data) if (!json.contains("results"_ls)) return { JsonParseError, "The key 'results' not found in the response" }; - d->results = fromJson<QVector<User>>(json.value("results"_ls)); + fromJson(json.value("results"_ls), d->results); if (!json.contains("limited"_ls)) return { JsonParseError, "The key 'limited' not found in the response" }; - d->limited = fromJson<bool>(json.value("limited"_ls)); + fromJson(json.value("limited"_ls), d->limited); return Success; } diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp index 128902e2..6ee6725d 100644 --- a/lib/csapi/versions.cpp +++ b/lib/csapi/versions.cpp @@ -16,6 +16,7 @@ class GetVersionsJob::Private { public: QStringList versions; + QHash<QString, bool> unstableFeatures; }; QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) @@ -40,10 +41,19 @@ const QStringList& GetVersionsJob::versions() const return d->versions; } +const QHash<QString, bool>& GetVersionsJob::unstableFeatures() const +{ + return d->unstableFeatures; +} + BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->versions = fromJson<QStringList>(json.value("versions"_ls)); + if (!json.contains("versions"_ls)) + return { JsonParseError, + "The key 'versions' not found in the response" }; + fromJson(json.value("versions"_ls), d->versions); + fromJson(json.value("unstable_features"_ls), d->unstableFeatures); return Success; } diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 309de184..b56f293f 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -6,6 +6,8 @@ #include "jobs/basejob.h" +#include <QtCore/QHash> +#include "converters.h" namespace QMatrixClient { @@ -19,6 +21,19 @@ namespace QMatrixClient /// /// Only the latest ``Z`` value will be reported for each supported ``X.Y`` value. /// i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``, it will report ``r0.0.1`` and ``r1.2.0``. + /// + /// The server may additionally advertise experimental features it supports + /// through ``unstable_features``. These features should be namespaced and + /// may optionally include version information within their name if desired. + /// Features listed here are not for optionally toggling parts of the Matrix + /// specification and should only be used to advertise support for a feature + /// which has not yet landed in the spec. For example, a feature currently + /// undergoing the proposal process may appear here and eventually be taken + /// off this list once the feature lands in the spec and the server deems it + /// reasonable to do so. Servers may wish to keep advertising features here + /// after they've been released into the spec to give clients a chance to + /// upgrade appropriately. Additionally, clients should avoid using unstable + /// features in their stable releases. class GetVersionsJob : public BaseJob { public: @@ -38,6 +53,10 @@ namespace QMatrixClient /// The supported versions. const QStringList& versions() const; + /// Experimental features the server supports. Features not listed here, + /// or the lack of this property all together, indicate that a feature is + /// not supported. + const QHash<QString, bool>& unstableFeatures() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index 0479b645..e8158723 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -42,7 +42,7 @@ const QJsonObject& GetTurnServerJob::data() const BaseJob::Status GetTurnServerJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QJsonObject>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp index d42534a0..a6107f86 100644 --- a/lib/csapi/wellknown.cpp +++ b/lib/csapi/wellknown.cpp @@ -15,8 +15,7 @@ static const auto basePath = QStringLiteral("/.well-known"); class GetWellknownJob::Private { public: - HomeserverInformation homeserver; - Omittable<IdentityServerInformation> identityServer; + DiscoveryInformation data; }; QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl) @@ -36,24 +35,14 @@ GetWellknownJob::GetWellknownJob() GetWellknownJob::~GetWellknownJob() = default; -const HomeserverInformation& GetWellknownJob::homeserver() const +const DiscoveryInformation& GetWellknownJob::data() const { - return d->homeserver; -} - -const Omittable<IdentityServerInformation>& GetWellknownJob::identityServer() const -{ - return d->identityServer; + return d->data; } BaseJob::Status GetWellknownJob::parseJson(const QJsonDocument& data) { - auto json = data.object(); - if (!json.contains("m.homeserver"_ls)) - return { JsonParseError, - "The key 'm.homeserver' not found in the response" }; - d->homeserver = fromJson<HomeserverInformation>(json.value("m.homeserver"_ls)); - d->identityServer = fromJson<IdentityServerInformation>(json.value("m.identity_server"_ls)); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index df4c8c6e..8da9ce9f 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -6,9 +6,8 @@ #include "jobs/basejob.h" +#include "csapi/definitions/wellknown/full.h" #include "converters.h" -#include "csapi/definitions/wellknown/identity_server.h" -#include "csapi/definitions/wellknown/homeserver.h" namespace QMatrixClient { @@ -41,10 +40,8 @@ namespace QMatrixClient // Result properties - /// Information about the homeserver to connect to. - const HomeserverInformation& homeserver() const; - /// Optional. Information about the identity server to connect to. - const Omittable<IdentityServerInformation>& identityServer() const; + /// Server discovery information. + const DiscoveryInformation& data() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index cb6439ef..aebdf5d3 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data) if (!json.contains("user_id"_ls)) return { JsonParseError, "The key 'user_id' not found in the response" }; - d->userId = fromJson<QString>(json.value("user_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); return Success; } diff --git a/lib/csapi/{{base}}.cpp.mustache b/lib/csapi/{{base}}.cpp.mustache index 64fd8bf3..ff888d76 100644 --- a/lib/csapi/{{base}}.cpp.mustache +++ b/lib/csapi/{{base}}.cpp.mustache @@ -8,49 +8,52 @@ {{/operations}} using namespace QMatrixClient; {{#models.model}}{{#in?}} -QJsonObject QMatrixClient::toJson(const {{qualifiedName}}& pod) +void JsonObjectConverter<{{qualifiedName}}>::dumpTo( + QJsonObject& jo, const {{qualifiedName}}& pod) { - QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}} - return jo; -} +{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}}); +{{/propertyMap}}{{#parents}} fillJson<{{name}}>(jo, pod); +{{/parents}}{{#vars}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); +{{/vars}}}{{!<- dumpTo() ends here}} {{/in?}}{{#out?}} -{{qualifiedName}} FromJsonObject<{{qualifiedName}}>::operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const +void JsonObjectConverter<{{qualifiedName}}>::fillFrom( + {{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result) { - {{qualifiedName}} result; -{{#vars}} result.{{nameCamelCase}} = - fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls)); +{{#parents}} fillFromJson<{{qualifiedName}}>(jo, result); +{{/parents}}{{#vars}} fromJson(jo.{{#propertyMap}}take{{/propertyMap + }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}}); {{/vars}}{{#propertyMap}} - result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}} - return result; -} + fromJson(jo, result.{{nameCamelCase}}); +{{/propertyMap}}} {{/out?}}{{/models.model}}{{#operations}} static const auto basePath = QStringLiteral("{{basePathWithoutHost}}"); {{# operation}}{{#models}} namespace QMatrixClient { // Converters -{{#model}}{{#in?}} - QJsonObject toJson(const {{qualifiedName}}& pod) - { - QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}} - return jo; - } -{{/in?}}{{#out?}} - template <> struct FromJsonObject<{{qualifiedName}}> +{{#model}} + template <> struct JsonObjectConverter<{{qualifiedName}}> { - {{qualifiedName}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const +{{#in?}} static void dumpTo(QJsonObject& jo, const {{qualifiedName}}& pod) { - {{qualifiedName}} result; -{{#vars}} result.{{nameCamelCase}} = - fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls)); -{{/vars}}{{#propertyMap}} - result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}} - return result; - } - }; -{{/out?}}{{/model}}} // namespace QMatrixClient +{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}}); + {{/propertyMap}}{{#parents}}fillJson<{{name}}>(jo, pod); + {{/parents}}{{#vars +}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); +{{/vars}} } +{{/in?}}{{#out? +}} static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result) + { +{{#parents}} fillFromJson<{{qualifiedName}}{{!of the parent!}}>(jo, result); + {{/parents}}{{#vars +}} fromJson(jo.{{#propertyMap}}take{{/propertyMap + }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}}); +{{/vars}}{{#propertyMap}} fromJson(jo, result.{{nameCamelCase}}); +{{/propertyMap}} } +{{/out?}} }; +{{/model}}} // namespace QMatrixClient {{/ models}}{{#responses}}{{#normalResponse?}}{{#allProperties?}} class {{camelCaseOperationId}}Job::Private { @@ -109,12 +112,12 @@ BaseJob::Status {{camelCaseOperationId}}Job::parseReply(QNetworkReply* reply) }{{/ producesNonJson?}}{{^producesNonJson?}} BaseJob::Status {{camelCaseOperationId}}Job::parseJson(const QJsonDocument& data) { -{{#inlineResponse}} d->{{paramName}} = fromJson<{{dataType.name}}>(data); +{{#inlineResponse}} fromJson(data, d->{{paramName}}); {{/inlineResponse}}{{^inlineResponse}} auto json = data.object(); {{#properties}}{{#required?}} if (!json.contains("{{baseName}}"_ls)) return { JsonParseError, "The key '{{baseName}}' not found in the response" }; -{{/required?}} d->{{paramName}} = fromJson<{{dataType.name}}>(json.value("{{baseName}}"_ls)); +{{/required?}} fromJson(json.value("{{baseName}}"_ls), d->{{paramName}}); {{/properties}}{{/inlineResponse}} return Success; }{{/ producesNonJson?}} {{/allProperties?}}{{/normalResponse?}}{{/responses}}{{/operation}}{{/operations}} diff --git a/lib/csapi/{{base}}.h.mustache b/lib/csapi/{{base}}.h.mustache index 147c8607..a9c3a63a 100644 --- a/lib/csapi/{{base}}.h.mustache +++ b/lib/csapi/{{base}}.h.mustache @@ -18,14 +18,13 @@ namespace QMatrixClient {{/vars}}{{#propertyMap}}{{#description}} /// {{_}} {{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; {{/propertyMap}} }; -{{#in?}} - QJsonObject toJson(const {{name}}& pod); -{{/in?}}{{#out?}} - template <> struct FromJsonObject<{{name}}> + template <> struct JsonObjectConverter<{{name}}> { - {{name}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const; - }; -{{/ out?}}{{/model}} + {{#in?}}static void dumpTo(QJsonObject& jo, const {{name}}& pod); + {{/in?}}{{#out?}}static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{name}}& pod); +{{/out?}} }; +{{/model}} {{/models}}{{#operations}} // Operations {{# operation}}{{#summary}} /// {{summary}}{{#description?}}{{!add a linebreak between summary and description if both exist}} diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 79ef769c..8ec3fe48 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -17,3 +17,29 @@ */ #include "eventitem.h" + +#include "events/roommessageevent.h" +#include "events/roomavatarevent.h" + +using namespace QMatrixClient; + +void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) +{ + // TODO: eventually we might introduce hasFileContent to RoomEvent, + // and unify the code below. + if (auto* rme = getAs<RoomMessageEvent>()) + { + Q_ASSERT(rme->hasFileContent()); + rme->editContent([remoteUrl] (EventContent::TypedBase& ec) { + ec.fileInfo()->url = remoteUrl; + }); + } + if (auto* rae = getAs<RoomAvatarEvent>()) + { + Q_ASSERT(rae->content().fileInfo()); + rae->editContent([remoteUrl] (EventContent::FileInfo& fi) { + fi.url = remoteUrl; + }); + } + setStatus(EventStatus::FileUploaded); +} diff --git a/lib/eventitem.h b/lib/eventitem.h index 5f1d10c9..36ed2132 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -33,16 +33,17 @@ namespace QMatrixClient /** Special marks an event can assume * * This is used to hint at a special status of some events in UI. - * Most status values are mutually exclusive. + * All values except Redacted and Hidden are mutually exclusive. */ enum Code { Normal = 0x0, //< No special designation Submitted = 0x01, //< The event has just been submitted for sending - Departed = 0x02, //< The event has left the client - ReachedServer = 0x03, //< The server has received the event - SendingFailed = 0x04, //< The server could not receive the event + FileUploaded = 0x02, //< The file attached to the event has been uploaded to the server + Departed = 0x03, //< The event has left the client + ReachedServer = 0x04, //< The server has received the event + SendingFailed = 0x05, //< The server could not receive the event Redacted = 0x08, //< The event has been redacted - Hidden = 0x10, //< The event should be hidden + Hidden = 0x10, //< The event should not be shown in the timeline }; Q_DECLARE_FLAGS(Status, Code) Q_FLAG(Status) @@ -70,6 +71,9 @@ namespace QMatrixClient return std::exchange(evt, move(other)); } + protected: + template <typename EventT> + EventT* getAs() { return eventCast<EventT>(evt); } private: RoomEventPtr evt; }; @@ -116,6 +120,7 @@ namespace QMatrixClient QString annotation() const { return _annotation; } void setDeparted() { setStatus(EventStatus::Departed); } + void setFileUploaded(const QUrl& remoteUrl); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index d1c1abc8..a99d85ac 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -36,37 +36,38 @@ namespace QMatrixClient order_type order; TagRecord (order_type order = none) : order(order) { } - explicit TagRecord(const QJsonObject& jo) + + bool operator<(const TagRecord& other) const + { + // Per The Spec, rooms with no order should be after those with order + return !order.omitted() && + (other.order.omitted() || order.value() < other.order.value()); + } + }; + + template <> struct JsonObjectConverter<TagRecord> + { + static void fillFrom(const QJsonObject& jo, TagRecord& rec) { // Parse a float both from JSON double and JSON string because // libqmatrixclient previously used to use strings to store order. const auto orderJv = jo.value("order"_ls); if (orderJv.isDouble()) - order = fromJson<float>(orderJv); - else if (orderJv.isString()) + rec.order = fromJson<float>(orderJv); + if (orderJv.isString()) { bool ok; - order = orderJv.toString().toFloat(&ok); + rec.order = orderJv.toString().toFloat(&ok); if (!ok) - order = none; + rec.order = none; } } - - bool operator<(const TagRecord& other) const + static void dumpTo(QJsonObject& jo, const TagRecord& rec) { - // Per The Spec, rooms with no order should be after those with order - return !order.omitted() && - (other.order.omitted() || order.value() < other.order.value()); + addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order); } }; - inline QJsonValue toJson(const TagRecord& rec) - { - QJsonObject o; - addParam<IfNotEmpty>(o, QStringLiteral("order"), rec.order); - return o; - } - using TagsMap = QHash<QString, TagRecord>; #define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ diff --git a/lib/events/event.cpp b/lib/events/event.cpp index fd6e3939..6505d89a 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -38,7 +38,8 @@ event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) QString EventTypeRegistry::getMatrixType(event_type_t typeId) { - return typeId < get().eventTypes.size() ? get().eventTypes[typeId] : ""; + return typeId < get().eventTypes.size() + ? get().eventTypes[typeId] : QString(); } Event::Event(Type type, const QJsonObject& json) @@ -77,3 +78,8 @@ const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); } + +void Event::dumpTo(QDebug dbg) const +{ + dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact); +} diff --git a/lib/events/event.h b/lib/events/event.h index e0d83976..b7bbd83e 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -32,19 +32,23 @@ namespace QMatrixClient template <typename EventT> using event_ptr_tt = std::unique_ptr<EventT>; + /// Unwrap a plain pointer from a smart pointer template <typename EventT> - inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) // unwrap + inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) { return ptr.get(); } + /// Unwrap a plain pointer and downcast it to the specified type template <typename TargetEventT, typename EventT> inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) { return static_cast<TargetEventT*>(rawPtr(ptr)); } + /// Re-wrap a smart pointer to base into a smart pointer to derived template <typename TargetT, typename SourceT> + [[deprecated("Consider using eventCast() or visit() instead")]] inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr) { return unique_ptr_cast<TargetT>(ptr); @@ -208,6 +212,9 @@ namespace QMatrixClient template <typename EventT> inline auto registerEventType() { + // Initialise exactly once, even if this function is called twice for + // the same type (for whatever reason - you never know the ways of + // static initialisation is done). static const auto _ = setupFactory<EventT>(); return _; // Only to facilitate usage in static initialisation } @@ -257,8 +264,18 @@ namespace QMatrixClient return fromJson<T>(contentJson()[key]); } + friend QDebug operator<<(QDebug dbg, const Event& e) + { + QDebugStateSaver _dss { dbg }; + dbg.noquote().nospace() + << e.matrixType() << '(' << e.type() << "): "; + e.dumpTo(dbg); + return dbg; + } + virtual bool isStateEvent() const { return false; } virtual bool isCallEvent() const { return false; } + virtual void dumpTo(QDebug dbg) const; protected: QJsonObject& editJson() { return _json; } @@ -327,7 +344,8 @@ namespace QMatrixClient -> decltype(static_cast<EventT*>(&*eptr)) { Q_ASSERT(eptr); - return is<EventT>(*eptr) ? static_cast<EventT*>(&*eptr) : nullptr; + return is<std::decay_t<EventT>>(*eptr) + ? static_cast<EventT*>(&*eptr) : nullptr; } // A single generic catch-all visitor @@ -359,7 +377,7 @@ namespace QMatrixClient visit(const BaseEventT& event, FnT&& visitor) { using event_type = fn_arg_t<FnT>; - if (is<event_type>(event)) + if (is<std::decay_t<event_type>>(event)) visitor(static_cast<event_type>(event)); } @@ -373,7 +391,7 @@ namespace QMatrixClient fn_return_t<FnT>&& defaultValue = {}) { using event_type = fn_arg_t<FnT>; - if (is<event_type>(event)) + if (is<std::decay_t<event_type>>(event)) return visitor(static_cast<event_type>(event)); return std::forward<fn_return_t<FnT>>(defaultValue); } @@ -386,7 +404,7 @@ namespace QMatrixClient FnTs&&... visitors) { using event_type1 = fn_arg_t<FnT1>; - if (is<event_type1>(event)) + if (is<std::decay_t<event_type1>>(event)) return visitor1(static_cast<event_type1&>(event)); return visit(event, std::forward<FnT2>(visitor2), std::forward<FnTs>(visitors)...); diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index a6b1c763..77f756cd 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -17,6 +17,8 @@ */ #include "eventcontent.h" + +#include "converters.h" #include "util.h" #include <QtCore/QMimeDatabase> @@ -30,7 +32,7 @@ QJsonObject Base::toJson() const return o; } -FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, +FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, const QString& originalFilename) : mimeType(mimeType), url(u), payloadSize(payloadSize) , originalName(originalFilename) @@ -41,23 +43,31 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, : originalInfoJson(infoJson) , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) , url(u) - , payloadSize(infoJson["size"_ls].toInt()) + , payloadSize(fromJson<qint64>(infoJson["size"_ls])) , originalName(originalFilename) { if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } +bool FileInfo::isValid() const +{ + return url.scheme() == "mxc" + && (url.authority() + url.path()).count('/') == 1; +} + void FileInfo::fillInfoJson(QJsonObject* infoJson) const { Q_ASSERT(infoJson); - infoJson->insert(QStringLiteral("size"), payloadSize); - infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + if (payloadSize != -1) + infoJson->insert(QStringLiteral("size"), payloadSize); + if (mimeType.isValid()) + infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); } -ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType, - const QSize& imageSize) - : FileInfo(u, fileSize, mimeType), imageSize(imageSize) +ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, + const QSize& imageSize, const QString& originalFilename) + : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize) { } ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, @@ -69,8 +79,10 @@ ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, void ImageInfo::fillInfoJson(QJsonObject* infoJson) const { FileInfo::fillInfoJson(infoJson); - infoJson->insert(QStringLiteral("w"), imageSize.width()); - infoJson->insert(QStringLiteral("h"), imageSize.height()); + if (imageSize.width() != -1) + infoJson->insert(QStringLiteral("w"), imageSize.width()); + if (imageSize.height() != -1) + infoJson->insert(QStringLiteral("h"), imageSize.height()); } Thumbnail::Thumbnail(const QJsonObject& infoJson) @@ -80,7 +92,9 @@ Thumbnail::Thumbnail(const QJsonObject& infoJson) void Thumbnail::fillInfoJson(QJsonObject* infoJson) const { - infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); - infoJson->insert(QStringLiteral("thumbnail_info"), - toInfoJson<ImageInfo>(*this)); + if (url.isValid()) + infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); + if (!imageSize.isEmpty()) + infoJson->insert(QStringLiteral("thumbnail_info"), + toInfoJson<ImageInfo>(*this)); } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 91d7a8c8..ab31a75d 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -43,9 +43,10 @@ namespace QMatrixClient class Base { public: - explicit Base (const QJsonObject& o = {}) : originalJson(o) { } + explicit Base (QJsonObject o = {}) : originalJson(std::move(o)) { } virtual ~Base() = default; + // FIXME: make toJson() from converters.* work on base classes QJsonObject toJson() const; public: @@ -87,12 +88,14 @@ namespace QMatrixClient class FileInfo { public: - explicit FileInfo(const QUrl& u, int payloadSize = -1, + explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, const QMimeType& mimeType = {}, const QString& originalFilename = {}); FileInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); + bool isValid() const; + void fillInfoJson(QJsonObject* infoJson) const; /** @@ -108,7 +111,7 @@ namespace QMatrixClient QJsonObject originalInfoJson; QMimeType mimeType; QUrl url; - int payloadSize; + qint64 payloadSize; QString originalName; }; @@ -126,9 +129,10 @@ namespace QMatrixClient class ImageInfo : public FileInfo { public: - explicit ImageInfo(const QUrl& u, int fileSize = -1, + explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, QMimeType mimeType = {}, - const QSize& imageSize = {}); + const QSize& imageSize = {}, + const QString& originalFilename = {}); ImageInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); @@ -148,10 +152,10 @@ namespace QMatrixClient class Thumbnail : public ImageInfo { public: + Thumbnail() : ImageInfo(QUrl()) { } // To allow empty thumbnails Thumbnail(const QJsonObject& infoJson); - Thumbnail(const ImageInfo& info) - : ImageInfo(info) - { } + Thumbnail(const ImageInfo& info) : ImageInfo(info) { } + using ImageInfo::ImageInfo; /** * Writes thumbnail information to "thumbnail_info" subobject @@ -166,6 +170,7 @@ namespace QMatrixClient explicit TypedBase(const QJsonObject& o = {}) : Base(o) { } virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } + virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } }; @@ -183,9 +188,7 @@ namespace QMatrixClient class UrlBasedContent : public TypedBase, public InfoT { public: - UrlBasedContent(QUrl url, InfoT&& info, QString filename = {}) - : InfoT(url, std::forward<InfoT>(info), filename) - { } + using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(json["url"].toString(), json["info"].toObject(), @@ -197,6 +200,7 @@ namespace QMatrixClient QMimeType type() const override { return InfoT::mimeType; } const FileInfo* fileInfo() const override { return this; } + FileInfo* fileInfo() override { return this; } protected: void fillJson(QJsonObject* json) const override @@ -213,7 +217,7 @@ namespace QMatrixClient class UrlWithThumbnailContent : public UrlBasedContent<InfoT> { public: - // TODO: POD constructor + using UrlBasedContent<InfoT>::UrlBasedContent; explicit UrlWithThumbnailContent(const QJsonObject& json) : UrlBasedContent<InfoT>(json) , thumbnail(InfoT::originalInfoJson) diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 3ee9a181..da663392 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -19,7 +19,6 @@ #pragma once #include "stateevent.h" -#include "converters.h" namespace QMatrixClient { namespace _impl { @@ -58,11 +57,15 @@ namespace QMatrixClient { matrixType); } - template <typename EventT> struct FromJsonObject<event_ptr_tt<EventT>> + template <typename EventT> struct JsonConverter<event_ptr_tt<EventT>> { - auto operator()(const QJsonObject& jo) const + static auto load(const QJsonValue& jv) { - return loadEvent<EventT>(jo); + return loadEvent<EventT>(jv.toObject()); + } + static auto load(const QJsonDocument& jd) + { + return loadEvent<EventT>(jd.object()); } }; } // namespace QMatrixClient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 491861b1..a43d3a85 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -18,8 +18,7 @@ #pragma once -#include "event.h" - +#include "stateevent.h" #include "eventcontent.h" namespace QMatrixClient diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp new file mode 100644 index 00000000..8fd0f1de --- /dev/null +++ b/lib/events/roomcreateevent.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "roomcreateevent.h" + +using namespace QMatrixClient; + +bool RoomCreateEvent::isFederated() const +{ + return fromJson<bool>(contentJson()["m.federate"_ls]); +} + +QString RoomCreateEvent::version() const +{ + return fromJson<QString>(contentJson()["room_version"_ls]); +} + +RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const +{ + const auto predJson = contentJson()["predecessor"_ls].toObject(); + return { + fromJson<QString>(predJson["room_id"_ls]), + fromJson<QString>(predJson["event_id"_ls]) + }; +} + +bool RoomCreateEvent::isUpgrade() const +{ + return contentJson().contains("predecessor"_ls); +} diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h new file mode 100644 index 00000000..0a8f27cc --- /dev/null +++ b/lib/events/roomcreateevent.h @@ -0,0 +1,49 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "stateevent.h" + +namespace QMatrixClient +{ + class RoomCreateEvent : public StateEventBase + { + public: + DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) + + explicit RoomCreateEvent() + : StateEventBase(typeId(), matrixTypeId()) + { } + explicit RoomCreateEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + { } + + struct Predecessor + { + QString roomId; + QString eventId; + }; + + bool isFederated() const; + QString version() const; + Predecessor predecessor() const; + bool isUpgrade() const; + }; + REGISTER_EVENT_TYPE(RoomCreateEvent) +} diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 80d121de..3d03509f 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -42,10 +42,6 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& json) _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); return; } - - const auto& txnId = transactionId(); - if (!txnId.isEmpty()) - qCDebug(EVENTS) << "Event transactionId:" << txnId; } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job @@ -90,7 +86,6 @@ void RoomEvent::setTransactionId(const QString& txnId) auto unsignedData = fullJson()[UnsignedKeyL].toObject(); unsignedData.insert(QStringLiteral("transaction_id"), txnId); editJson().insert(UnsignedKey, unsignedData); - qCDebug(EVENTS) << "New event transactionId:" << txnId; Q_ASSERT(transactionId() == txnId); } diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index eaa3302c..6da76526 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -23,20 +23,17 @@ #include <array> -using namespace QMatrixClient; - static const std::array<QString, 5> membershipStrings = { { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"), QStringLiteral("leave"), QStringLiteral("ban") } }; -namespace QMatrixClient -{ +namespace QMatrixClient { template <> - struct FromJson<MembershipType> + struct JsonConverter<MembershipType> { - MembershipType operator()(const QJsonValue& jv) const + static MembershipType load(const QJsonValue& jv) { const auto& membershipString = jv.toString(); for (auto it = membershipStrings.begin(); @@ -48,13 +45,14 @@ namespace QMatrixClient return MembershipType::Undefined; } }; - } +using namespace QMatrixClient; + MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson<MembershipType>(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) - , displayName(json["displayname"_ls].toString()) + , displayName(sanitized(json["displayname"_ls].toString())) , avatarUrl(json["avatar_url"_ls].toString()) { } diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index db25d026..b8224033 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -29,13 +29,10 @@ namespace QMatrixClient enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, Undefined }; - explicit MemberEventContent(MembershipType mt = MembershipType::Join) + explicit MemberEventContent(MembershipType mt = Join) : membership(mt) { } explicit MemberEventContent(const QJsonObject& json); - explicit MemberEventContent(const QJsonValue& jv) - : MemberEventContent(jv.toObject()) - { } MembershipType membership; bool isDirect = false; @@ -60,11 +57,19 @@ namespace QMatrixClient : StateEvent(typeId(), obj) { } RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), c.toJson()) + : StateEvent(typeId(), matrixTypeId(), c) { } - // This is a special constructor enabling RoomMemberEvent to be - // a base class for more specific member events. + /// A special constructor to create unknown RoomMemberEvents + /** + * This is needed in order to use RoomMemberEvent as a "base event + * class" in cases like GetMembersByRoomJob when RoomMemberEvents + * (rather than RoomEvents or StateEvents) are resolved from JSON. + * For such cases loadEvent<> requires an underlying class to be + * constructible with unknownTypeId() instead of its genuine id. + * Don't use it directly. + * \sa GetMembersByRoomJob, loadEvent, unknownTypeId + */ RoomMemberEvent(Type type, const QJsonObject& fullJson) : StateEvent(type, fullJson) { } @@ -84,6 +89,18 @@ namespace QMatrixClient private: REGISTER_ENUM(MembershipType) }; + + template <> + class EventFactory<RoomMemberEvent> + { + public: + static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json, + const QString&) + { + return makeEvent<RoomMemberEvent>(json); + } + }; + REGISTER_EVENT_TYPE(RoomMemberEvent) DEFINE_EVENTTYPE_ALIAS(RoomMember, RoomMemberEvent) } // namespace QMatrixClient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 1c5cf058..8f4e0ebc 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -21,18 +21,38 @@ #include "logging.h" #include <QtCore/QMimeDatabase> +#include <QtCore/QFileInfo> +#include <QtGui/QImageReader> +#include <QtMultimedia/QMediaResource> using namespace QMatrixClient; using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; +static const auto RelatesToKey = "m.relates_to"_ls; +static const auto MsgTypeKey = "msgtype"_ls; +static const auto BodyKey = "body"_ls; +static const auto FormattedBodyKey = "formatted_body"_ls; + +static const auto TextTypeKey = "m.text"; +static const auto NoticeTypeKey = "m.notice"; + +static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); + template <typename ContentT> TypedBase* make(const QJsonObject& json) { return new ContentT(json); } +template <> +TypedBase* make<TextContent>(const QJsonObject& json) +{ + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) + ? new TextContent(json) : nullptr; +} + struct MsgTypeDesc { QString matrixType; @@ -41,9 +61,9 @@ struct MsgTypeDesc }; const std::vector<MsgTypeDesc> msgTypes = - { { QStringLiteral("m.text"), MsgType::Text, make<TextContent> } + { { TextTypeKey, MsgType::Text, make<TextContent> } , { QStringLiteral("m.emote"), MsgType::Emote, make<TextContent> } - , { QStringLiteral("m.notice"), MsgType::Notice, make<TextContent> } + , { NoticeTypeKey, MsgType::Notice, make<TextContent> } , { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> } , { QStringLiteral("m.file"), MsgType::File, make<FileContent> } , { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> } @@ -71,22 +91,26 @@ MsgType jsonToMsgType(const QString& matrixType) return MsgType::Unknown; } -inline QJsonObject toMsgJson(const QString& plainBody, const QString& jsonMsgType, - TypedBase* content) +QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); + if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && + json.contains(RelatesToKey)) + { + json.remove(RelatesToKey); + qCWarning(EVENTS) << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; + } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); return json; } -static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; - RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) : RoomEvent(typeId(), matrixTypeId(), - toMsgJson(plainBody, jsonMsgType, content)) + assembleContentJson(plainBody, jsonMsgType, content)) , _content(content) { } @@ -95,6 +119,40 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) { } +TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) +{ + auto filePath = file.absoluteFilePath(); + auto localUrl = QUrl::fromLocalFile(filePath); + auto mimeType = QMimeDatabase().mimeTypeForFile(file); + if (!asGenericFile) + { + auto mimeTypeName = mimeType.name(); + if (mimeTypeName.startsWith("image/")) + return new ImageContent(localUrl, file.size(), mimeType, + QImageReader(filePath).size(), + file.fileName()); + + // duration can only be obtained asynchronously and can only be reliably + // done by starting to play the file. Left for a future implementation. + if (mimeTypeName.startsWith("video/")) + return new VideoContent(localUrl, file.size(), mimeType, + QMediaResource(localUrl).resolution(), + file.fileName()); + + if (mimeTypeName.startsWith("audio/")) + return new AudioContent(localUrl, file.size(), mimeType, + file.fileName()); + } + return new FileContent(localUrl, file.size(), mimeType, file.fileName()); +} + +RoomMessageEvent::RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, bool asGenericFile) + : RoomMessageEvent(plainBody, + asGenericFile ? QStringLiteral("m.file") : rawMsgTypeForFile(file), + contentFromFile(file, asGenericFile)) +{ } + RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj), _content(nullptr) { @@ -104,13 +162,17 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) { auto msgtype = content[MsgTypeKey].toString(); + bool msgTypeFound = false; for (const auto& mt: msgTypes) if (mt.matrixType == msgtype) + { _content.reset(mt.maker(content)); + msgTypeFound = true; + } - if (!_content) + if (!msgTypeFound) { - qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content," + qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type," << " full content dump follows"; qCWarning(EVENTS) << formatJson << content; } @@ -142,14 +204,13 @@ QMimeType RoomMessageEvent::mimeType() const static const auto PlainTextMimeType = QMimeDatabase().mimeTypeForName("text/plain"); return _content ? _content->type() : PlainTextMimeType; - ; } bool RoomMessageEvent::hasTextContent() const { - return content() && + return !content() || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || - msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes + msgtype() == MsgType::Notice); } bool RoomMessageEvent::hasFileContent() const @@ -162,10 +223,31 @@ bool RoomMessageEvent::hasThumbnail() const return content() && content()->thumbnailInfo(); } -TextContent::TextContent(const QString& text, const QString& contentType) +QString rawMsgTypeForMimeType(const QMimeType& mimeType) +{ + auto name = mimeType.name(); + return name.startsWith("image/") ? QStringLiteral("m.image") : + name.startsWith("video/") ? QStringLiteral("m.video") : + name.startsWith("audio/") ? QStringLiteral("m.audio") : + QStringLiteral("m.file"); +} + +QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url)); +} + +QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); +} + +TextContent::TextContent(const QString& text, const QString& contentType, + Omittable<RelatesTo> relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) + , relatesTo(std::move(relatesTo)) { - if (contentType == "org.matrix.custom.html") + if (contentType == HtmlContentTypeId) mimeType = QMimeDatabase().mimeTypeForName("text/html"); } @@ -177,16 +259,20 @@ TextContent::TextContent(const QJsonObject& json) // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (json["format"_ls].toString() == "org.matrix.custom.html") + if (json["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json["formatted_body"_ls].toString(); + body = json[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; body = json[BodyKey].toString(); } + const auto replyJson = json[RelatesToKey].toObject() + .value(RelatesTo::ReplyTypeId()).toObject(); + if (!replyJson.isEmpty()) + relatesTo = replyTo(fromJson<QString>(replyJson[EventIdKeyL])); } void TextContent::fillJson(QJsonObject* json) const @@ -194,13 +280,16 @@ void TextContent::fillJson(QJsonObject* json) const Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(QStringLiteral("format"), - QStringLiteral("org.matrix.custom.html")); + json->insert(QStringLiteral("format"), HtmlContentTypeId); json->insert(QStringLiteral("formatted_body"), body); } + if (!relatesTo.omitted()) + json->insert(QStringLiteral("m.relates_to"), + QJsonObject { { relatesTo->type, relatesTo->eventId } }); } -LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail) +LocationContent::LocationContent(const QString& geoUri, + const Thumbnail& thumbnail) : geoUri(geoUri), thumbnail(thumbnail) { } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 4c29a93e..c2e075eb 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -21,6 +21,8 @@ #include "roomevent.h" #include "eventcontent.h" +class QFileInfo; + namespace QMatrixClient { namespace MessageEventContent = EventContent; // Back-compatibility @@ -49,6 +51,9 @@ namespace QMatrixClient explicit RoomMessageEvent(const QString& plainBody, MsgType msgType = MsgType::Text, EventContent::TypedBase* content = nullptr); + explicit RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, + bool asGenericFile = false); explicit RoomMessageEvent(const QJsonObject& obj); MsgType msgtype() const; @@ -56,14 +61,27 @@ namespace QMatrixClient QString plainBody() const; EventContent::TypedBase* content() const { return _content.data(); } + template <typename VisitorT> + void editContent(VisitorT visitor) + { + visitor(*_content); + editJson()[ContentKeyL] = + assembleContentJson(plainBody(), rawMsgtype(), content()); + } QMimeType mimeType() const; bool hasTextContent() const; bool hasFileContent() const; bool hasThumbnail() const; + static QString rawMsgTypeForUrl(const QUrl& url); + static QString rawMsgTypeForFile(const QFileInfo& fi); + private: QScopedPointer<EventContent::TypedBase> _content; + static QJsonObject assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, EventContent::TypedBase* content); + REGISTER_ENUM(MsgType) }; REGISTER_EVENT_TYPE(RoomMessageEvent) @@ -74,6 +92,17 @@ namespace QMatrixClient { // Additional event content types + struct RelatesTo + { + static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } + QString type; // The only supported relation so far + QString eventId; + }; + inline RelatesTo replyTo(QString eventId) + { + return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + } + /** * Rich text content for m.text, m.emote, m.notice * @@ -83,13 +112,15 @@ namespace QMatrixClient class TextContent: public TypedBase { public: - TextContent(const QString& text, const QString& contentType); + TextContent(const QString& text, const QString& contentType, + Omittable<RelatesTo> relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; + Omittable<RelatesTo> relatesTo; protected: void fillJson(QJsonObject* json) const override; @@ -112,7 +143,7 @@ namespace QMatrixClient { public: LocationContent(const QString& geoUri, - const ImageInfo& thumbnail); + const Thumbnail& thumbnail = {}); explicit LocationContent(const QJsonObject& json); QMimeType type() const override; @@ -132,6 +163,7 @@ namespace QMatrixClient class PlayableContent : public ContentT { public: + using ContentT::ContentT; PlayableContent(const QJsonObject& json) : ContentT(json) , duration(ContentT::originalInfoJson["duration"_ls].toInt()) diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp new file mode 100644 index 00000000..9c3bafd4 --- /dev/null +++ b/lib/events/roomtombstoneevent.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "roomtombstoneevent.h" + +using namespace QMatrixClient; + +QString RoomTombstoneEvent::serverMessage() const +{ + return fromJson<QString>(contentJson()["body"_ls]); +} + +QString RoomTombstoneEvent::successorRoomId() const +{ + return fromJson<QString>(contentJson()["replacement_room"_ls]); +} diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h new file mode 100644 index 00000000..c7008ec4 --- /dev/null +++ b/lib/events/roomtombstoneevent.h @@ -0,0 +1,41 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "stateevent.h" + +namespace QMatrixClient +{ + class RoomTombstoneEvent : public StateEventBase + { + public: + DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) + + explicit RoomTombstoneEvent() + : StateEventBase(typeId(), matrixTypeId()) + { } + explicit RoomTombstoneEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + { } + + QString serverMessage() const; + QString successorRoomId() const; + }; + REGISTER_EVENT_TYPE(RoomTombstoneEvent) +} diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 56be947c..2c23d9ca 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -19,7 +19,6 @@ #pragma once #include "stateevent.h" -#include "eventcontent.h" #include "converters.h" @@ -28,7 +27,7 @@ namespace QMatrixClient namespace EventContent { template <typename T> - class SimpleContent: public Base + class SimpleContent { public: using value_type = T; @@ -39,41 +38,39 @@ namespace QMatrixClient : value(std::forward<TT>(value)), key(std::move(keyName)) { } SimpleContent(const QJsonObject& json, QString keyName) - : Base(json) - , value(QMatrixClient::fromJson<T>(json[keyName])) + : value(fromJson<T>(json[keyName])) , key(std::move(keyName)) { } + QJsonObject toJson() const + { + return { { key, QMatrixClient::toJson(value) } }; + } public: T value; protected: QString key; - - private: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert(key, QMatrixClient::toJson(value)); - } }; } // namespace EventContent -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public StateEvent<EventContent::SimpleContent<_ContentType>> \ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ + class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> \ { \ public: \ - using content_type = _ContentType; \ + using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(const QJsonObject& obj) \ - : StateEvent(typeId(), obj, QStringLiteral(#_ContentKey)) \ - { } \ + explicit _Name() : _Name(value_type()) { } \ template <typename T> \ explicit _Name(T&& value) \ : StateEvent(typeId(), matrixTypeId(), \ QStringLiteral(#_ContentKey), \ std::forward<T>(value)) \ { } \ + explicit _Name(QJsonObject obj) \ + : StateEvent(typeId(), std::move(obj), \ + QStringLiteral(#_ContentKey)) \ + { } \ auto _ContentKey() const { return content().value; } \ }; \ REGISTER_EVENT_TYPE(_Name) \ diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 877d0fae..a84f302b 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -20,17 +20,20 @@ using namespace QMatrixClient; +// Aside from the normal factory to instantiate StateEventBase inheritors +// StateEventBase itself can be instantiated if there's a state_key JSON key +// but the event type is unknown. [[gnu::unused]] static auto stateEventTypeInitialised = RoomEvent::factory_t::addMethod( [] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains("state_key")) + if (!json.contains("state_key"_ls)) return nullptr; if (auto e = StateEventBase::factory_t::make(json, matrixType)) return e; - return nullptr; + return makeEvent<StateEventBase>(unknownEventTypeId(), json); }); bool StateEventBase::repeatsState() const @@ -38,3 +41,18 @@ bool StateEventBase::repeatsState() const const auto prevContentJson = unsignedJson().value(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } + +QString StateEventBase::replacedState() const +{ + return unsignedJson().value("replaces_state"_ls).toString(); +} + +void StateEventBase::dumpTo(QDebug dbg) const +{ + if (!stateKey().isEmpty()) + dbg << '<' << stateKey() << "> "; + if (unsignedJson().contains(PrevContentKeyL)) + dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) + .toJson(QJsonDocument::Compact) << " -> "; + RoomEvent::dumpTo(dbg); +} diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 6032132e..3f54f7bf 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -30,11 +30,24 @@ namespace QMatrixClient { ~StateEventBase() override = default; bool isStateEvent() const override { return true; } + QString replacedState() const; + void dumpTo(QDebug dbg) const override; + virtual bool repeatsState() const; }; using StateEventPtr = event_ptr_tt<StateEventBase>; using StateEvents = EventsArray<StateEventBase>; + template <> + inline bool is<StateEventBase>(const Event& e) { return e.isStateEvent(); } + + /** + * A combination of event type and state key uniquely identifies a piece + * of state in Matrix. + * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events + */ + using StateEventKey = QPair<QString, QString>; + template <typename ContentT> struct Prev { @@ -78,6 +91,12 @@ namespace QMatrixClient { } const ContentT& content() const { return _content; } + template <typename VisitorT> + void editContent(VisitorT&& visitor) + { + visitor(_content); + editJson()[ContentKeyL] = _content.toJson(); + } [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const { return prevContent(); } const ContentT* prevContent() const @@ -85,8 +104,18 @@ namespace QMatrixClient { QString prevSenderId() const { return _prev ? _prev->senderId : QString(); } - protected: + private: ContentT _content; std::unique_ptr<Prev<ContentT>> _prev; }; } // namespace QMatrixClient + +namespace std { + template <> struct hash<QMatrixClient::StateEventKey> + { + size_t operator()(const QMatrixClient::StateEventKey& k) const Q_DECL_NOEXCEPT + { + return qHash(k); + } + }; +} diff --git a/lib/identity/definitions/request_email_validation.cpp b/lib/identity/definitions/request_email_validation.cpp index 95088bcb..47463a8b 100644 --- a/lib/identity/definitions/request_email_validation.cpp +++ b/lib/identity/definitions/request_email_validation.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RequestEmailValidation& pod) +void JsonObjectConverter<RequestEmailValidation>::dumpTo( + QJsonObject& jo, const RequestEmailValidation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("email"), pod.email); addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); - return jo; } -RequestEmailValidation FromJsonObject<RequestEmailValidation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<RequestEmailValidation>::fillFrom( + const QJsonObject& jo, RequestEmailValidation& result) { - RequestEmailValidation result; - result.clientSecret = - fromJson<QString>(jo.value("client_secret"_ls)); - result.email = - fromJson<QString>(jo.value("email"_ls)); - result.sendAttempt = - fromJson<int>(jo.value("send_attempt"_ls)); - result.nextLink = - fromJson<QString>(jo.value("next_link"_ls)); - - return result; + fromJson(jo.value("client_secret"_ls), result.clientSecret); + fromJson(jo.value("email"_ls), result.email); + fromJson(jo.value("send_attempt"_ls), result.sendAttempt); + fromJson(jo.value("next_link"_ls), result.nextLink); } diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h index 3e72275f..eb7d8ed6 100644 --- a/lib/identity/definitions/request_email_validation.h +++ b/lib/identity/definitions/request_email_validation.h @@ -33,12 +33,10 @@ namespace QMatrixClient /// server will redirect the user to this URL. QString nextLink; }; - - QJsonObject toJson(const RequestEmailValidation& pod); - - template <> struct FromJsonObject<RequestEmailValidation> + template <> struct JsonObjectConverter<RequestEmailValidation> { - RequestEmailValidation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RequestEmailValidation& pod); + static void fillFrom(const QJsonObject& jo, RequestEmailValidation& pod); }; } // namespace QMatrixClient diff --git a/lib/identity/definitions/request_msisdn_validation.cpp b/lib/identity/definitions/request_msisdn_validation.cpp index 125baa9c..a123d326 100644 --- a/lib/identity/definitions/request_msisdn_validation.cpp +++ b/lib/identity/definitions/request_msisdn_validation.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RequestMsisdnValidation& pod) +void JsonObjectConverter<RequestMsisdnValidation>::dumpTo( + QJsonObject& jo, const RequestMsisdnValidation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("country"), pod.country); addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber); addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); - return jo; } -RequestMsisdnValidation FromJsonObject<RequestMsisdnValidation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<RequestMsisdnValidation>::fillFrom( + const QJsonObject& jo, RequestMsisdnValidation& result) { - RequestMsisdnValidation result; - result.clientSecret = - fromJson<QString>(jo.value("client_secret"_ls)); - result.country = - fromJson<QString>(jo.value("country"_ls)); - result.phoneNumber = - fromJson<QString>(jo.value("phone_number"_ls)); - result.sendAttempt = - fromJson<int>(jo.value("send_attempt"_ls)); - result.nextLink = - fromJson<QString>(jo.value("next_link"_ls)); - - return result; + fromJson(jo.value("client_secret"_ls), result.clientSecret); + fromJson(jo.value("country"_ls), result.country); + fromJson(jo.value("phone_number"_ls), result.phoneNumber); + fromJson(jo.value("send_attempt"_ls), result.sendAttempt); + fromJson(jo.value("next_link"_ls), result.nextLink); } diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h index 77bea2bc..b48ed6d5 100644 --- a/lib/identity/definitions/request_msisdn_validation.h +++ b/lib/identity/definitions/request_msisdn_validation.h @@ -36,12 +36,10 @@ namespace QMatrixClient /// server will redirect the user to this URL. QString nextLink; }; - - QJsonObject toJson(const RequestMsisdnValidation& pod); - - template <> struct FromJsonObject<RequestMsisdnValidation> + template <> struct JsonObjectConverter<RequestMsisdnValidation> { - RequestMsisdnValidation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RequestMsisdnValidation& pod); + static void fillFrom(const QJsonObject& jo, RequestMsisdnValidation& pod); }; } // namespace QMatrixClient diff --git a/lib/identity/definitions/sid.cpp b/lib/identity/definitions/sid.cpp index 443dbedf..1ba4b3b5 100644 --- a/lib/identity/definitions/sid.cpp +++ b/lib/identity/definitions/sid.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const Sid& pod) +void JsonObjectConverter<Sid>::dumpTo( + QJsonObject& jo, const Sid& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("sid"), pod.sid); - return jo; } -Sid FromJsonObject<Sid>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<Sid>::fillFrom( + const QJsonObject& jo, Sid& result) { - Sid result; - result.sid = - fromJson<QString>(jo.value("sid"_ls)); - - return result; + fromJson(jo.value("sid"_ls), result.sid); } diff --git a/lib/identity/definitions/sid.h b/lib/identity/definitions/sid.h index eae60c47..ac8c4130 100644 --- a/lib/identity/definitions/sid.h +++ b/lib/identity/definitions/sid.h @@ -19,12 +19,10 @@ namespace QMatrixClient /// must not be empty. QString sid; }; - - QJsonObject toJson(const Sid& pod); - - template <> struct FromJsonObject<Sid> + template <> struct JsonObjectConverter<Sid> { - Sid operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Sid& pod); + static void fillFrom(const QJsonObject& jo, Sid& pod); }; } // namespace QMatrixClient diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index b21173ae..0d9b9f10 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -186,7 +186,7 @@ QUrl BaseJob::makeRequestUrl(QUrl baseUrl, if (!pathBase.endsWith('/') && !path.startsWith('/')) pathBase.push_back('/'); - baseUrl.setPath( pathBase + path ); + baseUrl.setPath(pathBase + path, QUrl::TolerantMode); baseUrl.setQuery(query); return baseUrl; } @@ -325,9 +325,16 @@ void BaseJob::gotReply() d->status.code = UserConsentRequiredError; d->errorUrl = json.value("consent_uri"_ls).toString(); } - else if (!json.isEmpty()) // Not localisable on the client side - setStatus(IncorrectRequestError, - json.value("error"_ls).toString()); + else if (errCode == "M_UNSUPPORTED_ROOM_VERSION" || + errCode == "M_INCOMPATIBLE_ROOM_VERSION") + { + d->status.code = UnsupportedRoomVersionError; + if (json.contains("room_version")) + d->status.message = + tr("Requested room version: %1") + .arg(json.value("room_version").toString()); + } else if (!json.isEmpty()) // Not localisable on the client side + setStatus(d->status.code, json.value("error"_ls).toString()); } } @@ -422,12 +429,12 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) { d->rawResponse = reply->readAll(); - QJsonParseError error; + QJsonParseError error { 0, QJsonParseError::MissingObject }; const auto& json = QJsonDocument::fromJson(d->rawResponse, &error); if( error.error == QJsonParseError::NoError ) return parseJson(json); - else - return { IncorrectResponseError, error.errorString() }; + + return { IncorrectResponseError, error.errorString() }; } BaseJob::Status BaseJob::parseJson(const QJsonDocument&) @@ -519,8 +526,19 @@ BaseJob::Status BaseJob::status() const QByteArray BaseJob::rawData(int bytesAtMost) const { - return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost ? - d->rawResponse.left(bytesAtMost) + "...(truncated)" : d->rawResponse; + return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost + ? d->rawResponse.left(bytesAtMost) : d->rawResponse; +} + +QString BaseJob::rawDataSample(int bytesAtMost) const +{ + auto data = rawData(bytesAtMost); + Q_ASSERT(data.size() <= d->rawResponse.size()); + return data.size() == d->rawResponse.size() + ? data : data + tr("...(truncated, %Ln bytes in total)", + "Comes after trimmed raw network response", + d->rawResponse.size()); + } QString BaseJob::statusCaption() const @@ -557,6 +575,8 @@ QString BaseJob::statusCaption() const return tr("Network authentication required"); case UserConsentRequiredError: return tr("User consent required"); + case UnsupportedRoomVersionError: + return tr("The server does not support the needed room version"); default: return tr("Request failed"); } @@ -579,10 +599,25 @@ QUrl BaseJob::errorUrl() const void BaseJob::setStatus(Status s) { + // The crash that led to this code has been reported in + // https://github.com/QMatrixClient/Quaternion/issues/566 - basically, + // when cleaning up childrent of a deleted Connection, there's a chance + // of pending jobs being abandoned, calling setStatus(Abandoned). + // There's nothing wrong with this; however, the safety check for + // cleartext access tokens below uses d->connection - which is a dangling + // pointer. + // To alleviate that, a stricter condition is applied, that for Abandoned + // and to-be-Abandoned jobs the status message will be disregarded entirely. + // For 0.6 we might rectify the situation by making d->connection + // a QPointer<> (and derive ConnectionData from QObject, respectively). + if (d->status.code == Abandoned || s.code == Abandoned) + s.message.clear(); + if (d->status == s) return; - if (!d->connection->accessToken().isEmpty()) + if (!s.message.isEmpty() + && d->connection && !d->connection->accessToken().isEmpty()) s.message.replace(d->connection->accessToken(), "(REDACTED)"); if (!s.good()) qCWarning(d->logCat) << this << "status" << s; @@ -597,9 +632,8 @@ void BaseJob::setStatus(int code, QString message) void BaseJob::abandon() { - beforeAbandon(d->reply.data()); + beforeAbandon(d->reply ? d->reply.data() : nullptr); setStatus(Abandoned); - this->disconnect(); if (d->reply) d->reply->disconnect(this); emit finished(this); diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 4ef25ab8..4c1c7706 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -64,6 +64,7 @@ namespace QMatrixClient , IncorrectResponseError , TooManyRequestsError , RequestNotImplementedError + , UnsupportedRoomVersionError , NetworkAuthRequiredError , UserConsentRequiredError , UserDefinedError = 200 @@ -138,8 +139,20 @@ namespace QMatrixClient Status status() const; /** Short human-friendly message on the job status */ QString statusCaption() const; - /** Raw response body as received from the server */ + /** Get raw response body as received from the server + * \param bytesAtMost return this number of leftmost bytes, or -1 + * to return the entire response + */ QByteArray rawData(int bytesAtMost = -1) const; + /** Get UI-friendly sample of raw data + * + * This is almost the same as rawData but appends the "truncated" + * suffix if not all data fit in bytesAtMost. This call is + * recommended to present a sample of raw data as "details" next to + * error messages. Note that the default \p bytesAtMost value is + * also tailored to UI cases. + */ + QString rawDataSample(int bytesAtMost = 65535) const; /** Error (more generally, status) code * Equivalent to status().code @@ -203,9 +216,9 @@ namespace QMatrixClient * * In general, to be notified of a job's completion, client code * should connect to result(), success(), or failure() - * rather than finished(). However if you store a list of jobs - * and need to track their lifecycle, then you should connect to this - * instead of result(), to avoid dangling pointers in your list. + * rather than finished(). However if you need to track the job's + * lifecycle you should connect to this instead of result(); + * in particular, only this signal will be emitted on abandoning. * * @param job the job that emitted this signal * diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 2bf9dd8f..672a7b2d 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -22,7 +22,8 @@ class DownloadFileJob::Private QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) { - return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1)); + return makeRequestUrl( + std::move(baseUrl), mxcUri.authority(), mxcUri.path().mid(1)); } DownloadFileJob::DownloadFileJob(const QString& serverName, @@ -31,7 +32,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) { - setObjectName("DownloadFileJob"); + setObjectName(QStringLiteral("DownloadFileJob")); } QString DownloadFileJob::targetFileName() const diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index aeb49839..edb9b156 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -59,5 +59,5 @@ BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) if( _thumbnail.loadFromData(data()->readAll()) ) return Success; - return { IncorrectResponseError, "Could not read image data" }; + return { IncorrectResponseError, QStringLiteral("Could not read image data") }; } diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 6baf388e..84385b55 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -18,10 +18,6 @@ #include "syncjob.h" -#include "events/eventloader.h" - -#include <QtCore/QElapsedTimer> - using namespace QMatrixClient; static size_t jobId = 0; @@ -46,111 +42,21 @@ SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, setMaxRetries(std::numeric_limits<int>::max()); } -QString SyncData::nextBatch() const -{ - return nextBatch_; -} - -SyncDataList&& SyncData::takeRoomData() -{ - return std::move(roomData); -} - -Events&& SyncData::takePresenceData() -{ - return std::move(presenceData); -} - -Events&& SyncData::takeAccountData() -{ - return std::move(accountData); -} - -Events&&SyncData::takeToDeviceEvents() -{ - return std::move(toDeviceEvents); -} - -template <typename EventsArrayT, typename StrT> -inline EventsArrayT load(const QJsonObject& batches, StrT keyName) -{ - return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); -} +SyncJob::SyncJob(const QString& since, const Filter& filter, + int timeout, const QString& presence) + : SyncJob(since, + QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact), + timeout, presence) +{ } BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { - return d.parseJson(data); -} - -BaseJob::Status SyncData::parseJson(const QJsonDocument &data) -{ - QElapsedTimer et; et.start(); - - auto json = data.object(); - nextBatch_ = json.value("next_batch"_ls).toString(); - presenceData = load<Events>(json, "presence"_ls); - accountData = load<Events>(json, "account_data"_ls); - toDeviceEvents = load<Events>(json, "to_device"_ls); + d.parseJson(data.object()); + if (d.unresolvedRooms().isEmpty()) + return BaseJob::Success; - auto rooms = json.value("rooms"_ls).toObject(); - JoinStates::Int ii = 1; // ii is used to make a JoinState value - auto totalRooms = 0; - auto totalEvents = 0; - for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) - { - const auto rs = rooms.value(JoinStateStrings[i]).toObject(); - // We have a Qt container on the right and an STL one on the left - roomData.reserve(static_cast<size_t>(rs.size())); - for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) - { - roomData.emplace_back(roomIt.key(), JoinState(ii), - roomIt.value().toObject()); - const auto& r = roomData.back(); - totalEvents += r.state.size() + r.ephemeral.size() + - r.accountData.size() + r.timeline.size(); - } - totalRooms += rs.size(); - } - if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" - << totalRooms << "room(s)," - << totalEvents << "event(s) in" << et; - return BaseJob::Success; + qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" + << d.unresolvedRooms().join(','); + return BaseJob::IncorrectResponseError; } -const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-qmatrixclient.unread_count"); - -SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, - const QJsonObject& room_) - : roomId(roomId_) - , joinState(joinState_) - , state(load<StateEvents>(room_, - joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) -{ - switch (joinState) { - case JoinState::Join: - ephemeral = load<Events>(room_, "ephemeral"_ls); - FALLTHROUGH; - case JoinState::Leave: - { - accountData = load<Events>(room_, "account_data"_ls); - timeline = load<RoomEvents>(room_, "timeline"_ls); - const auto timelineJson = room_.value("timeline"_ls).toObject(); - timelineLimited = timelineJson.value("limited"_ls).toBool(); - timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); - - break; - } - default: /* nothing on top of state */; - } - - const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); - unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); - highlightCount = unreadJson.value("highlight_count"_ls).toInt(); - notificationCount = unreadJson.value("notification_count"_ls).toInt(); - if (highlightCount > 0 || notificationCount > 0) - qCDebug(SYNCJOB) << "Room" << roomId_ - << "has highlights:" << highlightCount - << "and notifications:" << notificationCount; -} diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 6b9bedfa..036b25d0 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -20,62 +20,19 @@ #include "basejob.h" -#include "joinstate.h" -#include "events/stateevent.h" -#include "util.h" +#include "../syncdata.h" +#include "../csapi/definitions/sync_filter.h" namespace QMatrixClient { - class SyncRoomData - { - public: - QString roomId; - JoinState joinState; - StateEvents state; - RoomEvents timeline; - Events ephemeral; - Events accountData; - - bool timelineLimited; - QString timelinePrevBatch; - int unreadCount; - int highlightCount; - int notificationCount; - - SyncRoomData(const QString& roomId, JoinState joinState_, - const QJsonObject& room_); - SyncRoomData(SyncRoomData&&) = default; - SyncRoomData& operator=(SyncRoomData&&) = default; - - static const QString UnreadCountKey; - }; - // QVector cannot work with non-copiable objects, std::vector can. - using SyncDataList = std::vector<SyncRoomData>; - - class SyncData - { - public: - BaseJob::Status parseJson(const QJsonDocument &data); - Events&& takePresenceData(); - Events&& takeAccountData(); - Events&& takeToDeviceEvents(); - SyncDataList&& takeRoomData(); - QString nextBatch() const; - - private: - QString nextBatch_; - Events presenceData; - Events accountData; - Events toDeviceEvents; - SyncDataList roomData; - }; - class SyncJob: public BaseJob { public: explicit SyncJob(const QString& since = {}, const QString& filter = {}, int timeout = -1, const QString& presence = {}); + explicit SyncJob(const QString& since, const Filter& filter, + int timeout = -1, const QString& presence = {}); SyncData &&takeData() { return std::move(d); } diff --git a/lib/joinstate.h b/lib/joinstate.h index c172f576..379183f6 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -24,7 +24,7 @@ namespace QMatrixClient { - enum class JoinState + enum class JoinState : unsigned int { Join = 0x1, Invite = 0x2, diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 89967a8a..7d9cb360 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -29,7 +29,8 @@ class NetworkAccessManager::Private QList<QSslError> ignoredSslErrors; }; -NetworkAccessManager::NetworkAccessManager(QObject* parent) : d(std::make_unique<Private>()) +NetworkAccessManager::NetworkAccessManager(QObject* parent) + : QNetworkAccessManager(parent), d(std::make_unique<Private>()) { } QList<QSslError> NetworkAccessManager::ignoredSslErrors() const diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index 48bd09f3..6ff2bc1f 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -27,5 +27,5 @@ void NetworkSettings::setupApplicationProxy() const } QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName) +QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName) QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h new file mode 100644 index 00000000..c2bde8df --- /dev/null +++ b/lib/qt_connection_util.h @@ -0,0 +1,107 @@ +/****************************************************************************** + * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "util.h" + +#include <QtCore/QPointer> + +namespace QMatrixClient { + namespace _impl { + template <typename SenderT, typename SignalT, + typename ContextT, typename... ArgTs> + inline QMetaObject::Connection connectUntil( + SenderT* sender, SignalT signal, ContextT* context, + std::function<bool(ArgTs...)> slot, Qt::ConnectionType connType) + { + // See https://bugreports.qt.io/browse/QTBUG-60339 +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + auto pc = std::make_shared<QMetaObject::Connection>(); +#else + auto pc = std::make_unique<QMetaObject::Connection>(); +#endif + auto& c = *pc; // Resolve a reference before pc is moved to lambda + c = QObject::connect(sender, signal, context, + [pc=std::move(pc),slot] (ArgTs... args) { + Q_ASSERT(*pc); // If it's been triggered, it should exist + if (slot(std::forward<ArgTs>(args)...)) + QObject::disconnect(*pc); + }, connType); + return c; + } + } + + template <typename SenderT, typename SignalT, + typename ContextT, typename FunctorT> + inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, + const FunctorT& slot, + Qt::ConnectionType connType = Qt::AutoConnection) + { + return _impl::connectUntil(sender, signal, context, + typename function_traits<FunctorT>::function_type(slot), + connType); + } + + /** Create a single-shot connection that triggers on the signal and + * then self-disconnects + * + * Only supports DirectConnection type. + */ + template <typename SenderT, typename SignalT, + typename ReceiverT, typename SlotT> + inline auto connectSingleShot(SenderT* sender, SignalT signal, + ReceiverT* receiver, SlotT slot) + { + QMetaObject::Connection connection; + connection = QObject::connect(sender, signal, receiver, slot, + Qt::DirectConnection); + Q_ASSERT(connection); + QObject::connect(sender, signal, receiver, + [connection] { QObject::disconnect(connection); }, + Qt::DirectConnection); + return connection; + } + + /** A guard pointer that disconnects an interested object upon destruction + * It's almost QPointer<> except that you have to initialise it with one + * more additional parameter - a pointer to a QObject that will be + * disconnected from signals of the underlying pointer upon the guard's + * destruction. + */ + template <typename T> + class ConnectionsGuard : public QPointer<T> + { + public: + ConnectionsGuard(T* publisher, QObject* subscriber) + : QPointer<T>(publisher), subscriber(subscriber) + { } + ~ConnectionsGuard() + { + if (*this) + (*this)->disconnect(subscriber); + } + ConnectionsGuard(ConnectionsGuard&&) = default; + ConnectionsGuard& operator=(ConnectionsGuard&&) = default; + Q_DISABLE_COPY(ConnectionsGuard) + using QPointer<T>::operator=; + + private: + QObject* subscriber; + }; +} diff --git a/lib/room.cpp b/lib/room.cpp index ea771f17..9e7ff8d2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -25,11 +25,14 @@ #include "csapi/receipts.h" #include "csapi/redaction.h" #include "csapi/account-data.h" -#include "csapi/message_pagination.h" #include "csapi/room_state.h" #include "csapi/room_send.h" +#include "csapi/rooms.h" #include "csapi/tags.h" +#include "csapi/room_upgrades.h" #include "events/simplestateevents.h" +#include "events/roomcreateevent.h" +#include "events/roomtombstoneevent.h" #include "events/roomavatarevent.h" #include "events/roommemberevent.h" #include "events/typingevent.h" @@ -46,13 +49,15 @@ #include "connection.h" #include "user.h" #include "converters.h" +#include "syncdata.h" #include <QtCore/QHash> #include <QtCore/QStringBuilder> // for efficient string concats (operator%) -#include <QtCore/QElapsedTimer> #include <QtCore/QPointer> #include <QtCore/QDir> #include <QtCore/QTemporaryFile> +#include <QtCore/QRegularExpression> +#include <QtCore/QMimeDatabase> #include <array> #include <functional> @@ -67,12 +72,6 @@ using std::llround; enum EventsPlacement : int { Older = -1, Newer = 1 }; -// A workaround for MSVC 2015 that fails with "error C2440: 'return': -// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'" -#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) -# define WORKAROUND_EXTENDED_INITIALIZER_LIST -#endif - class Room::Private { public: @@ -86,29 +85,27 @@ class Room::Private Room* q; - // This updates the room displayname field (which is the way a room - // should be shown in the room list) It should be called whenever the - // list of members or the room name (m.room.name) or canonical alias change. - void updateDisplayname(); - Connection* connection; + QString id; + JoinState joinState; + RoomSummary summary = { none, 0, none }; + /// The state of the room at timeline position before-0 + /// \sa timelineBase + std::unordered_map<StateEventKey, StateEventPtr> baseState; + /// The state of the room at timeline position after-maxTimelineIndex() + /// \sa Room::syncEdge + QHash<StateEventKey, const StateEventBase*> currentState; Timeline timeline; PendingEvents unsyncedEvents; QHash<QString, TimelineItem::index_t> eventsIndex; - QString id; - QStringList aliases; - QString canonicalAlias; - QString name; QString displayname; - QString topic; - QString encryptionAlgorithm; Avatar avatar; - JoinState joinState; int highlightCount = 0; int notificationCount = 0; members_map_t membersMap; QList<User*> usersTyping; QMultiHash<QString, User*> eventIdReadUsers; + QList<User*> usersInvited; QList<User*> membersLeft; int unreadMessages = 0; bool displayed = false; @@ -120,18 +117,21 @@ class Room::Private std::unordered_map<QString, EventPtr> accountData; QString prevBatch; QPointer<GetRoomEventsJob> eventsHistoryJob; + QPointer<GetMembersByRoomJob> allMembersJob; struct FileTransferPrivateInfo { -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST FileTransferPrivateInfo() = default; - FileTransferPrivateInfo(BaseJob* j, QString fileName) - : job(j), localFileInfo(fileName) + FileTransferPrivateInfo(BaseJob* j, const QString& fileName, + bool isUploading = false) + : status(FileTransferInfo::Started), job(j) + , localFileInfo(fileName), isUpload(isUploading) { } -#endif + + FileTransferInfo::Status status = FileTransferInfo::None; QPointer<BaseJob> job = nullptr; QFileInfo localFileInfo { }; - FileTransferInfo::Status status = FileTransferInfo::Started; + bool isUpload = false; qint64 progress = 0; qint64 total = -1; @@ -164,13 +164,37 @@ class Room::Private const RoomMessageEvent* getEventWithFile(const QString& eventId) const; QString fileNameToDownload(const RoomMessageEvent* event) const; + Changes setSummary(RoomSummary&& newSummary); + //void inviteUser(User* u); // We might get it at some point in time. void insertMemberIntoMap(User* u); - void renameMember(User* u, QString oldName); + void renameMember(User* u, const QString& oldName); void removeMemberFromMap(const QString& username, User* u); + // This updates the room displayname field (which is the way a room + // should be shown in the room list); called whenever the list of + // members, the room name (m.room.name) or canonical alias change. + void updateDisplayname(); + // This is used by updateDisplayname() but only calculates the new name + // without any updates. + QString calculateDisplayname() const; + + /// A point in the timeline corresponding to baseState + rev_iter_t timelineBase() const { return q->findInTimeline(-1); } + void getPreviousContent(int limit = 10); + template <typename EventT> + const EventT* getCurrentState(const QString& stateKey = {}) const + { + static const EventT empty; + const auto* evt = + currentState.value({EventT::matrixTypeId(), stateKey}, &empty); + Q_ASSERT(evt->type() == EventT::typeId() && + evt->matrixType() == EventT::matrixTypeId()); + return static_cast<const EventT*>(evt); + } + bool isEventNotable(const TimelineItem& ti) const { return !ti->isRedacted() && @@ -178,7 +202,29 @@ class Room::Private is<RoomMessageEvent>(*ti); } - void addNewMessageEvents(RoomEvents&& events); + template <typename EventArrayT> + Changes updateStateFrom(EventArrayT&& events) + { + Changes changes = NoChange; + if (!events.empty()) + { + QElapsedTimer et; et.start(); + for (auto&& eptr: events) + { + const auto& evt = *eptr; + Q_ASSERT(evt.isStateEvent()); + // Update baseState afterwards to make sure that the old state + // is valid and usable inside processStateEvent + changes |= q->processStateEvent(evt); + baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); + } + if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():" + << events.size() << "event(s)," << et; + } + return changes; + } + Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); /** Move events into the timeline @@ -190,20 +236,22 @@ class Room::Private * @param placement - position and direction of insertion: Older for * historical messages, Newer for new ones */ - Timeline::difference_type moveEventsToTimeline(RoomEventsRange events, - EventsPlacement placement); + Timeline::size_type moveEventsToTimeline(RoomEventsRange events, + EventsPlacement placement); /** * Remove events from the passed container that are already in the timeline */ void dropDuplicateEvents(RoomEvents& events) const; - void setLastReadEvent(User* u, QString eventId); + Changes setLastReadEvent(User* u, QString eventId); void updateUnreadCount(rev_iter_t from, rev_iter_t to); - void promoteReadMarker(User* u, rev_iter_t newMarker, - bool force = false); + Changes promoteReadMarker(User* u, rev_iter_t newMarker, + bool force = false); - void markMessagesAsRead(rev_iter_t upToMarker); + Changes markMessagesAsRead(rev_iter_t upToMarker); + + void getAllMembers(); QString sendEvent(RoomEventPtr&& event); @@ -213,17 +261,23 @@ class Room::Private return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...)); } + RoomEvent* addAsPending(RoomEventPtr&& event); + QString doSendEvent(const RoomEvent* pEvent); - PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr); - void onEventSendingFailure(const RoomEvent* pEvent, - const QString& txnId, BaseJob* call = nullptr); + void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); template <typename EvT> - auto requestSetState(const QString& stateKey, const EvT& event) + SetRoomStateWithKeyJob* requestSetState(const QString& stateKey, + const EvT& event) { - // TODO: Queue up state events sending (see #133). - return connection->callApi<SetRoomStateWithKeyJob>( + if (q->successorId().isEmpty()) + { + // TODO: Queue up state events sending (see #133). + return connection->callApi<SetRoomStateWithKeyJob>( id, EvT::matrixTypeId(), stateKey, event.contentJson()); + } + qCWarning(MAIN) << q << "has been upgraded, state won't be set"; + return nullptr; } template <typename EvT> @@ -246,8 +300,10 @@ class Room::Private QJsonObject toJson() const; private: - QString calculateDisplayname() const; - QString roomNameFromMemberNames(const QList<User*>& userlist) const; + using users_shortlist_t = std::array<User*, 3>; + template<typename ContT> + users_shortlist_t buildShortlist(const ContT& users) const; + users_shortlist_t buildShortlist(const QStringList& userIds) const; bool isLocalUser(const User* u) const { @@ -262,9 +318,13 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; - connect(this, &Room::userAdded, this, &Room::memberListChanged); - connect(this, &Room::userRemoved, this, &Room::memberListChanged); - connect(this, &Room::memberRenamed, this, &Room::memberListChanged); + d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name + connectUntil(connection, &Connection::loadedRoomState, this, + [this] (Room* r) { + if (this == r) + emit baseStateLoaded(); + return this == r; // loadedRoomState fires only once per room + }); qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; } @@ -278,6 +338,28 @@ const QString& Room::id() const return d->id; } +QString Room::version() const +{ + const auto v = d->getCurrentState<RoomCreateEvent>()->version(); + return v.isEmpty() ? QStringLiteral("1") : v; +} + +bool Room::isUnstable() const +{ + return !connection()->loadingCapabilities() && + !connection()->stableRoomVersions().contains(version()); +} + +QString Room::predecessorId() const +{ + return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId; +} + +QString Room::successorId() const +{ + return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId(); +} + const Room::Timeline& Room::messageEvents() const { return d->timeline; @@ -290,17 +372,17 @@ const Room::PendingEvents& Room::pendingEvents() const QString Room::name() const { - return d->name; + return d->getCurrentState<RoomNameEvent>()->name(); } QStringList Room::aliases() const { - return d->aliases; + return d->getCurrentState<RoomAliasesEvent>()->aliases(); } QString Room::canonicalAlias() const { - return d->canonicalAlias; + return d->getCurrentState<RoomCanonicalAliasEvent>()->alias(); } QString Room::displayName() const @@ -308,9 +390,14 @@ QString Room::displayName() const return d->displayname; } +void Room::refreshDisplayName() +{ + d->updateDisplayname(); +} + QString Room::topic() const { - return d->topic; + return d->getCurrentState<RoomTopicEvent>()->topic(); } QString Room::avatarMediaId() const @@ -323,6 +410,11 @@ QUrl Room::avatarUrl() const return d->avatar.url(); } +const Avatar& Room::avatarObject() const +{ + return d->avatar; +} + QImage Room::avatar(int dimension) { return avatar(dimension, dimension); @@ -368,14 +460,15 @@ void Room::setJoinState(JoinState state) d->joinState = state; qCDebug(MAIN) << "Room" << id() << "changed state: " << int(oldState) << "->" << int(state); + emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } -void Room::Private::setLastReadEvent(User* u, QString eventId) +Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) { auto& storedId = lastReadEventIds[u]; if (storedId == eventId) - return; + return Change::NoChange; eventIdReadUsers.remove(storedId, u); eventIdReadUsers.insert(eventId, u); swap(storedId, eventId); @@ -386,7 +479,9 @@ void Room::Private::setLastReadEvent(User* u, QString eventId) if (storedId != serverReadMarker) connection->callApi<PostReadMarkersJob>(id, storedId); emit q->readMarkerMoved(eventId, storedId); + return Change::ReadMarkerChange; } + return Change::NoChange; } void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) @@ -429,14 +524,15 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) } } -void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) +Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, + bool force) { Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); const auto prevMarker = q->readMarker(u); if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators - return; + return Change::NoChange; Q_ASSERT(newMarker < timeline.crend()); @@ -445,13 +541,13 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) auto eagerMarker = find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); - setLastReadEvent(u, (*(eagerMarker - 1))->id()); + auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); if (isLocalUser(u)) { const auto oldUnreadCount = unreadMessages; QElapsedTimer et; et.start(); - unreadMessages = count_if(eagerMarker, timeline.cend(), - std::bind(&Room::Private::isEventNotable, this, _1)); + unreadMessages = int(count_if(eagerMarker, timeline.cend(), + std::bind(&Room::Private::isEventNotable, this, _1))); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; @@ -469,14 +565,16 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) qCDebug(MAIN) << "Room" << displayname << "still has" << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); + changes |= Change::UnreadNotifsChange; } } + return changes; } -void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) +Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) { const auto prevMarker = q->readMarker(); - promoteReadMarker(q->localUser(), upToMarker); + auto changes = promoteReadMarker(q->localUser(), upToMarker); if (prevMarker != upToMarker) qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); @@ -487,11 +585,12 @@ void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) { if ((*upToMarker)->senderId() != q->localUser()->id()) { - connection->callApi<PostReceiptJob>(id, "m.read", - (*upToMarker)->id()); + connection->callApi<PostReceiptJob>(id, QStringLiteral("m.read"), + QUrl::toPercentEncoding((*upToMarker)->id())); break; } } + return changes; } void Room::markMessagesAsRead(QString uptoEventId) @@ -505,6 +604,29 @@ void Room::markAllMessagesAsRead() d->markMessagesAsRead(d->timeline.crbegin()); } +bool Room::canSwitchVersions() const +{ + if (!successorId().isEmpty()) + return false; // Noone can upgrade a room that's already upgraded + + // TODO, #276: m.room.power_levels + const auto* plEvt = + d->currentState.value({QStringLiteral("m.room.power_levels"), {}}); + if (!plEvt) + return true; + + const auto plJson = plEvt->contentJson(); + const auto currentUserLevel = + plJson.value("users"_ls).toObject() + .value(localUser()->id()).toInt( + plJson.value("users_default"_ls).toInt()); + const auto tombstonePowerLevel = + plJson.value("events"_ls).toObject() + .value("m.room.tombstone"_ls).toInt( + plJson.value("state_default"_ls).toInt()); + return currentUserLevel >= tombstonePowerLevel; +} + bool Room::hasUnreadMessages() const { return unreadCount() >= 0; @@ -515,11 +637,21 @@ int Room::unreadCount() const return d->unreadMessages; } -Room::rev_iter_t Room::timelineEdge() const +Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); } +Room::Timeline::const_iterator Room::syncEdge() const +{ + return d->timeline.cend(); +} + +Room::rev_iter_t Room::timelineEdge() const +{ + return historyEdge(); +} + TimelineItem::index_t Room::minTimelineIndex() const { return d->timeline.empty() ? 0 : d->timeline.front().index(); @@ -554,6 +686,44 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const return timelineEdge(); } +Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) +{ + return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), + [txnId] (const auto& item) { return item->transactionId() == txnId; }); +} + +Room::PendingEvents::const_iterator +Room::findPendingEvent(const QString& txnId) const +{ + return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(), + [txnId] (const auto& item) { return item->transactionId() == txnId; }); +} + +void Room::Private::getAllMembers() +{ + // If already loaded or already loading, there's nothing to do here. + if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob)) + return; + + allMembersJob = connection->callApi<GetMembersByRoomJob>( + id, connection->nextBatchToken(), "join"); + auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; + connect( allMembersJob, &BaseJob::success, q, [=] { + Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1); + auto roomChanges = updateStateFrom(allMembersJob->chunk()); + // Replay member events that arrived after the point for which + // the full members list was requested. + if (!timeline.empty() ) + for (auto it = q->findInTimeline(nextIndex).base(); + it != timeline.cend(); ++it) + if (is<RoomMemberEvent>(**it)) + roomChanges |= q->processStateEvent(**it); + if (roomChanges&MembersChange) + emit q->memberListChanged(); + emit q->allMembersLoaded(); + }); +} + bool Room::displayed() const { return d->displayed; @@ -570,6 +740,7 @@ void Room::setDisplayed(bool displayed) { resetHighlightCount(); resetNotificationCount(); + d->getAllMembers(); } } @@ -669,6 +840,14 @@ void Room::resetHighlightCount() emit highlightCountChanged(this); } +void Room::switchVersion(QString newVersion) +{ + auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion); + connect(job, &BaseJob::failure, this, [this,job] { + emit upgradeFailed(job->errorString()); + }); +} + bool Room::hasAccountData(const QString& type) const { return d->accountData.find(type) != d->accountData.end(); @@ -768,7 +947,7 @@ void Room::Private::setTags(TagsMap newTags) } tags = move(newTags); qCDebug(MAIN) << "Room" << q->objectName() << "is tagged with" - << q->tagNames().join(", "); + << q->tagNames().join(QStringLiteral(", ")); emit q->tagsChanged(); } @@ -839,7 +1018,7 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const return fileName; } -QUrl Room::urlToThumbnail(const QString& eventId) +QUrl Room::urlToThumbnail(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) if (event->hasThumbnail()) @@ -853,7 +1032,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) return {}; } -QUrl Room::urlToDownload(const QString& eventId) +QUrl Room::urlToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) { @@ -865,7 +1044,7 @@ QUrl Room::urlToDownload(const QString& eventId) return {}; } -QString Room::fileNameToDownload(const QString& eventId) +QString Room::fileNameToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) return d->fileNameToDownload(event); @@ -890,7 +1069,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const total = INT_MAX; } -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST +#ifdef BROKEN_INITIALIZER_LISTS FileTransferInfo fti; fti.status = infoIt->status; fti.progress = int(progress); @@ -899,13 +1078,28 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); return fti; #else - return { infoIt->status, int(progress), int(total), + return { infoIt->status, infoIt->isUpload, int(progress), int(total), QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) }; #endif } +QUrl Room::fileSource(const QString& id) const +{ + auto url = urlToDownload(id); + if (url.isValid()) + return url; + + // No urlToDownload means it's a pending or completed upload. + auto infoIt = d->fileTransfers.find(id); + if (infoIt != d->fileTransfers.end()) + return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); + + qCWarning(MAIN) << "File source for identifier" << id << "not found"; + return {}; +} + QString Room::prettyPrint(const QString& plainText) const { return QMatrixClient::prettyPrint(plainText); @@ -947,7 +1141,41 @@ int Room::timelineSize() const bool Room::usesEncryption() const { - return !d->encryptionAlgorithm.isEmpty(); + return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty(); +} + +int Room::joinedCount() const +{ + return d->summary.joinedMemberCount.omitted() + ? d->membersMap.size() + : d->summary.joinedMemberCount.value(); +} + +int Room::invitedCount() const +{ + // TODO: Store invited users in Room too + Q_ASSERT(!d->summary.invitedMemberCount.omitted()); + return d->summary.invitedMemberCount.value(); +} + +int Room::totalMemberCount() const +{ + return joinedCount() + invitedCount(); +} + +GetRoomEventsJob* Room::eventsHistoryJob() const +{ + return d->eventsHistoryJob; +} + +Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) +{ + if (!summary.merge(newSummary)) + return Change::NoChange; + qCDebug(MAIN).nospace().noquote() + << "Updated room summary for " << q->objectName() << ": " << summary; + emit q->memberListChanged(); + return Change::SummaryChange; } void Room::Private::insertMemberIntoMap(User *u) @@ -955,7 +1183,11 @@ void Room::Private::insertMemberIntoMap(User *u) const auto userName = u->name(q); // If there is exactly one namesake of the added user, signal member renaming // for that other one because the two should be disambiguated now. - auto namesakes = membersMap.values(userName); + const auto namesakes = membersMap.values(userName); + + // Callers should check they are not adding an existing user once more. + Q_ASSERT(!namesakes.contains(u)); + if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), namesakes.front()->fullName(q)); @@ -964,7 +1196,7 @@ void Room::Private::insertMemberIntoMap(User *u) emit q->memberRenamed(namesakes.front()); } -void Room::Private::renameMember(User* u, QString oldName) +void Room::Private::renameMember(User* u, const QString& oldName) { if (u->name(q) == oldName) { @@ -977,7 +1209,6 @@ void Room::Private::renameMember(User* u, QString oldName) removeMemberFromMap(oldName, u); insertMemberIntoMap(u); } - emit q->memberRenamed(u); } void Room::Private::removeMemberFromMap(const QString& username, User* u) @@ -993,7 +1224,6 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u) membersMap.remove(username, u); // If there was one namesake besides the removed user, signal member renaming // for it because it doesn't need to be disambiguated anymore. - // TODO: Think about left users. if (namesake) emit q->memberRenamed(namesake); } @@ -1003,13 +1233,14 @@ inline auto makeErrorStr(const Event& e, QByteArray msg) return msg.append("; event dump follows:\n").append(e.originalJson()); } -Room::Timeline::difference_type Room::Private::moveEventsToTimeline( +Room::Timeline::size_type Room::Private::moveEventsToTimeline( RoomEventsRange events, EventsPlacement placement) { Q_ASSERT(!events.empty()); // Historical messages arrive in newest-to-oldest order, so the process for - // them is symmetric to the one for new messages. - auto index = timeline.empty() ? -int(placement) : + // them is almost symmetric to the one for new messages. New messages get + // appended from index 0; old messages go backwards from index -1. + auto index = timeline.empty() ? -((placement+1)/2) /* 1 -> -1; -1 -> 0 */ : placement == Older ? timeline.front().index() : timeline.back().index(); auto baseIndex = index; @@ -1030,7 +1261,7 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline( eventsIndex.insert(eId, index); Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); } - const auto insertedSize = (index - baseIndex) * int(placement); + const auto insertedSize = (index - baseIndex) * placement; Q_ASSERT(insertedSize == int(events.size())); return insertedSize; } @@ -1076,50 +1307,40 @@ QString Room::roomMembername(const QString& userId) const return roomMembername(user(userId)); } -void Room::updateData(SyncRoomData&& data) +void Room::updateData(SyncRoomData&& data, bool fromCache) { if( d->prevBatch.isEmpty() ) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); + Changes roomChanges = Change::NoChange; QElapsedTimer et; et.start(); for (auto&& event: data.accountData) - processAccountDataEvent(move(event)); + roomChanges |= processAccountDataEvent(move(event)); - bool emitNamesChanged = false; - if (!data.state.empty()) - { - et.restart(); - for (const auto& e: data.state) - emitNamesChanged |= processStateEvent(*e); + roomChanges |= d->updateStateFrom(data.state); - if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents():" - << data.state.size() << "event(s)," << et; - } if (!data.timeline.empty()) { et.restart(); - // State changes can arrive in a timeline event; so check those. - for (const auto& e: data.timeline) - emitNamesChanged |= processStateEvent(*e); + roomChanges |= d->addNewMessageEvents(move(data.timeline)); if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" + qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << data.timeline.size() << "event(s)," << et; } - if (emitNamesChanged) + if (roomChanges&TopicChange) + emit topicChanged(); + + if (roomChanges&NameChange) emit namesChanged(this); - d->updateDisplayname(); - if (!data.timeline.empty()) - { - et.restart(); - d->addNewMessageEvents(move(data.timeline)); - if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; - } + if (roomChanges&MembersChange) + emit memberListChanged(); + + roomChanges |= d->setSummary(move(data.summary)); + for( auto&& ephemeralEvent: data.ephemeral ) - processEphemeralEvent(move(ephemeralEvent)); + roomChanges |= processEphemeralEvent(move(ephemeralEvent)); // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) @@ -1139,29 +1360,45 @@ void Room::updateData(SyncRoomData&& data) d->notificationCount = data.notificationCount; emit notificationCountChanged(this); } + if (roomChanges != Change::NoChange) + { + d->updateDisplayname(); + emit changed(roomChanges); + if (!fromCache) + connection()->saveRoomState(this); + } } -QString Room::Private::sendEvent(RoomEventPtr&& event) +RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) { if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); auto* pEvent = rawPtr(event); - emit q->pendingEventAboutToAdd(); + emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); - return doSendEvent(pEvent); + return pEvent; +} + +QString Room::Private::sendEvent(RoomEventPtr&& event) +{ + if (q->successorId().isEmpty()) + return doSendEvent(addAsPending(std::move(event))); + + qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; + return {}; } QString Room::Private::doSendEvent(const RoomEvent* pEvent) { - auto txnId = pEvent->transactionId(); + const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest, id, pEvent->matrixType(), txnId, pEvent->contentJson())) { Room::connect(call, &BaseJob::started, q, - [this,pEvent,txnId] { - auto it = findAsPending(pEvent); + [this,txnId] { + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qWarning(EVENTS) << "Pending event for transaction" << txnId @@ -1169,16 +1406,14 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) return; } it->setDeparted(); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); Room::connect(call, &BaseJob::failure, q, - std::bind(&Room::Private::onEventSendingFailure, - this, pEvent, txnId, call)); + std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); Room::connect(call, &BaseJob::success, q, - [this,call,pEvent,txnId] { - // Find an event by the pointer saved in the lambda (the pointer - // may be dangling by now but we can still search by it). - auto it = findAsPending(pEvent); + [this,call,txnId] { + emit q->messageSent(txnId, call->eventId()); + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qDebug(EVENTS) << "Pending event for transaction" << txnId @@ -1187,26 +1422,16 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) } it->setReachedServer(call->eventId()); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); } else - onEventSendingFailure(pEvent, txnId); + onEventSendingFailure(txnId); return txnId; } -Room::PendingEvents::iterator Room::Private::findAsPending( - const RoomEvent* rawEvtPtr) -{ - const auto comp = - [rawEvtPtr] (const auto& pe) { return pe.event() == rawEvtPtr; }; - - return std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), comp); -} - -void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, - const QString& txnId, BaseJob* call) +void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call) { - auto it = findAsPending(pEvent); + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qCritical(EVENTS) << "Pending event for transaction" << txnId @@ -1216,15 +1441,40 @@ void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, it->setSendingFailed(call ? call->statusCaption() % ": " % call->errorString() : tr("The call could not be started")); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } QString Room::retryMessage(const QString& txnId) { - auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), - [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); + const auto it = findPendingEvent(txnId); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Retrying transaction" << txnId; + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) + { + Q_ASSERT(transferIt->isUpload); + if (transferIt->status == FileTransferInfo::Completed) + { + qCDebug(MAIN) << "File for transaction" << txnId + << "has already been uploaded, bypassing re-upload"; + } else { + if (isJobRunning(transferIt->job)) + { + qCDebug(MAIN) << "Abandoning the upload job for transaction" + << txnId << "and starting again"; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, tr("File upload will be retried")); + } + uploadFile(txnId, + QUrl::fromLocalFile(transferIt->localFileInfo.absoluteFilePath())); + // FIXME: Content type is no more passed here but it should + } + } + if (it->deliveryStatus() == EventStatus::ReachedServer) + { + qCWarning(MAIN) << "The previous attempt has reached the server; two" + " events are likely to be in the timeline after retry"; + } it->resetStatus(); return d->doSendEvent(it->event()); } @@ -1235,7 +1485,22 @@ void Room::discardMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Discarding transaction" << txnId; - emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) + { + Q_ASSERT(transferIt->isUpload); + if (isJobRunning(transferIt->job)) + { + transferIt->status = FileTransferInfo::Cancelled; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, tr("File upload cancelled")); + } else if (transferIt->status == FileTransferInfo::Completed) + { + qCWarning(MAIN) << "File for transaction" << txnId + << "has been uploaded but the message was discarded"; + } + } + emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin())); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); } @@ -1251,7 +1516,7 @@ QString Room::postPlainText(const QString& plainText) } QString Room::postHtmlMessage(const QString& plainText, const QString& html, - MessageEventType type) + MessageEventType type) { return d->sendEvent<RoomMessageEvent>(plainText, type, new EventContent::TextContent(html, QStringLiteral("text/html"))); @@ -1259,7 +1524,65 @@ QString Room::postHtmlMessage(const QString& plainText, const QString& html, QString Room::postHtmlText(const QString& plainText, const QString& html) { - return postHtmlMessage(plainText, html, MessageEventType::Text); + return postHtmlMessage(plainText, html); +} + +QString Room::postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile) +{ + QFileInfo localFile { localPath.toLocalFile() }; + Q_ASSERT(localFile.isFile()); + + const auto txnId = connection()->generateTxnId(); + // Remote URL will only be known after upload; fill in the local path + // to enable the preview while the event is pending. + uploadFile(txnId, localPath); + { + auto&& event = + makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile); + event->setTransactionId(txnId); + d->addAsPending(std::move(event)); + } + auto* context = new QObject(this); + connect(this, &Room::fileTransferCompleted, context, + [context,this,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { + if (id == txnId) + { + auto it = findPendingEvent(txnId); + if (it != d->unsyncedEvents.end()) + { + it->setFileUploaded(mxcUri); + emit pendingEventChanged( + int(it - d->unsyncedEvents.begin())); + d->doSendEvent(it->get()); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) << "File uploaded to" << mxcUri + << "but the event referring to it was cancelled"; + } + context->deleteLater(); + } + }); + connect(this, &Room::fileTransferCancelled, this, + [context,this,txnId] (const QString& id) { + if (id == txnId) + { + auto it = findPendingEvent(txnId); + if (it != d->unsyncedEvents.end()) + { + const auto idx = int(it - d->unsyncedEvents.begin()); + emit pendingEventAboutToDiscard(idx); + // See #286 on why iterator may not be valid here. + d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx); + emit pendingEventDiscarded(); + } + context->deleteLater(); + } + }); + + return txnId; } QString Room::postEvent(RoomEvent* event) @@ -1288,6 +1611,11 @@ void Room::setCanonicalAlias(const QString& newAlias) d->requestSetState(RoomCanonicalAliasEvent(newAlias)); } +void Room::setAliases(const QStringList& aliases) +{ + d->requestSetState(RoomAliasesEvent(aliases)); +} + void Room::setTopic(const QString& newTopic) { d->requestSetState(RoomTopicEvent(newTopic)); @@ -1315,7 +1643,24 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) bool Room::supportsCalls() const { - return d->membersMap.size() == 2; + return joinedCount() == 2; +} + +void Room::checkVersion() +{ + const auto defaultVersion = connection()->defaultRoomVersion(); + const auto stableVersions = connection()->stableRoomVersions(); + Q_ASSERT(!defaultVersion.isEmpty()); + // This method is only called after the base state has been loaded + // or the server capabilities have been loaded. + emit stabilityUpdated(defaultVersion, stableVersions); + if (!stableVersions.contains(version())) + { + qCDebug(MAIN) << this << "version is" << version() + << "which the server doesn't count as stable"; + if (canSwitchVersions()) + qCDebug(MAIN) << "The current user has enough privileges to fix it"; + } } void Room::inviteCall(const QString& callId, const int lifetime, @@ -1358,15 +1703,18 @@ void Room::getPreviousContent(int limit) void Room::Private::getPreviousContent(int limit) { - if( !isJobRunning(eventsHistoryJob) ) - { - eventsHistoryJob = - connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit); - connect( eventsHistoryJob, &BaseJob::success, q, [=] { - prevBatch = eventsHistoryJob->end(); - addHistoricalMessageEvents(eventsHistoryJob->chunk()); - }); - } + if (isJobRunning(eventsHistoryJob)) + return; + + eventsHistoryJob = + connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit); + emit q->eventsHistoryJobChanged(); + connect( eventsHistoryJob, &BaseJob::success, q, [=] { + prevBatch = eventsHistoryJob->end(); + addHistoricalMessageEvents(eventsHistoryJob->chunk()); + }); + connect( eventsHistoryJob, &QObject::destroyed, + q, &Room::eventsHistoryJobChanged); } void Room::inviteToRoom(const QString& memberId) @@ -1376,7 +1724,8 @@ void Room::inviteToRoom(const QString& memberId) LeaveRoomJob* Room::leaveRoom() { - return connection()->callApi<LeaveRoomJob>(id()); + // FIXME, #63: It should be RoomManager, not Connection + return connection()->leaveRoom(this); } SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const @@ -1401,8 +1750,8 @@ void Room::unban(const QString& userId) void Room::redactEvent(const QString& eventId, const QString& reason) { - connection()->callApi<RedactEventJob>( - id(), eventId, connection()->generateTxnId(), reason); + connection()->callApi<RedactEventJob>(id(), + QUrl::toPercentEncoding(eventId), connection()->generateTxnId(), reason); } void Room::uploadFile(const QString& id, const QUrl& localFilename, @@ -1414,7 +1763,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobRunning(job)) { - d->fileTransfers.insert(id, { job, fileName }); + d->fileTransfers.insert(id, { job, fileName, true }); connect(job, &BaseJob::uploadProgress, this, [this,id] (qint64 sent, qint64 total) { d->fileTransfers[id].update(sent, total); @@ -1437,8 +1786,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) if (ongoingTransfer != d->fileTransfers.end() && ongoingTransfer->status == FileTransferInfo::Started) { - qCWarning(MAIN) << "Download for" << eventId - << "already started; to restart, cancel it first"; + qCWarning(MAIN) << "Transfer for" << eventId + << "is ongoing; download won't start"; return; } @@ -1452,13 +1801,21 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) Q_ASSERT(false); return; } - const auto fileUrl = event->content()->fileInfo()->url; + const auto* const fileInfo = event->content()->fileInfo(); + if (!fileInfo->isValid()) + { + qCWarning(MAIN) << "Event" << eventId + << "has an empty or malformed mxc URL; won't download"; + return; + } + const auto fileUrl = fileInfo->url; auto filePath = localFilename.toLocalFile(); if (filePath.isEmpty()) { // Build our own file path, starting with temp directory and eventId. filePath = eventId; - filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') % + filePath = QDir::tempPath() % '/' % + filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_") % '#' % d->fileNameToDownload(event); } auto job = connection()->downloadFile(fileUrl, filePath); @@ -1533,22 +1890,29 @@ RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { auto originalJson = target.originalJsonObject(); - static const QStringList keepKeys = - { EventIdKey, TypeKey, QStringLiteral("room_id"), - QStringLiteral("sender"), QStringLiteral("state_key"), - QStringLiteral("prev_content"), ContentKey, - QStringLiteral("origin_server_ts") }; + static const QStringList keepKeys { + EventIdKey, TypeKey, QStringLiteral("room_id"), + QStringLiteral("sender"), QStringLiteral("state_key"), + QStringLiteral("prev_content"), ContentKey, + QStringLiteral("hashes"), QStringLiteral("signatures"), + QStringLiteral("depth"), QStringLiteral("prev_events"), + QStringLiteral("prev_state"), QStringLiteral("auth_events"), + QStringLiteral("origin"), QStringLiteral("origin_server_ts"), + QStringLiteral("membership") + }; std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } } -// , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } + , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomPowerLevels::typeId(), // { QStringLiteral("ban"), QStringLiteral("events"), // QStringLiteral("events_default"), QStringLiteral("kick"), // QStringLiteral("redact"), QStringLiteral("state_default"), // QStringLiteral("users"), QStringLiteral("users_default") } } - , { RoomAliasesEvent::typeId(), { QStringLiteral("alias") } } + , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } +// , { RoomHistoryVisibility::typeId(), +// { QStringLiteral("history_visibility") } } }; for (auto it = originalJson.begin(); it != originalJson.end();) { @@ -1600,11 +1964,26 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) return true; } - // Make a new event from the redacted JSON, exchange events, - // notify everyone and delete the old event + // Make a new event from the redacted JSON and put it in the timeline + // instead of the redacted one. oldEvent will be deleted on return. auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); - q->onRedaction(*oldEvent, *ti.event()); qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id(); + if (oldEvent->isStateEvent()) + { + const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; + Q_ASSERT(currentState.contains(evtKey)); + if (currentState.value(evtKey) == oldEvent.get()) + { + Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState + qCDebug(MAIN).nospace() << "Redacting state " + << oldEvent->matrixType() << "/" << oldEvent->stateKey(); + // Retarget the current state to the newly made event. + if (q->processStateEvent(*ti)) + emit q->namesChanged(q); + updateDisplayname(); + } + } + q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } @@ -1626,11 +2005,11 @@ inline bool isRedaction(const RoomEventPtr& ep) return is<RedactionEvent>(*ep); } -void Room::Private::addNewMessageEvents(RoomEvents&& events) +Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) - return; + return Change::NoChange; // Pre-process redactions so that events that get redacted in the same // batch landed in the timeline already redacted. @@ -1655,8 +2034,17 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) // If the target event comes later, it comes already redacted. } + // State changes arrive as a part of timeline; the current room state gets + // updated before merging events to the timeline because that's what + // clients historically expect. This may eventually change though if we + // postulate that the current state is only current between syncs but not + // within a sync. + Changes roomChanges = Change::NoChange; + for (const auto& eptr: events) + roomChanges |= q->processStateEvent(*eptr); + auto timelineSize = timeline.size(); - auto totalInserted = 0; + size_t totalInserted = 0; for (auto it = events.begin(); it != events.end();) { auto nextPendingPair = findFirstOf(it, events.end(), @@ -1677,12 +2065,22 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) break; it = nextPending + 1; - emit q->pendingEventAboutToMerge(nextPending->get(), - nextPendingPair.second - unsyncedEvents.begin()); + auto* nextPendingEvt = nextPending->get(); + const auto pendingEvtIdx = + int(nextPendingPair.second - unsyncedEvents.begin()); + emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx); qDebug(EVENTS) << "Merging pending event from transaction" - << (*nextPending)->transactionId() << "into" - << (*nextPending)->id(); - unsyncedEvents.erase(nextPendingPair.second); + << nextPendingEvt->transactionId() << "into" + << nextPendingEvt->id(); + auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); + if (transfer.status != FileTransferInfo::None) + fileTransfers.insert(nextPendingEvt->id(), transfer); + // After emitting pendingEventAboutToMerge() above we cannot rely + // on the previously obtained nextPendingPair.second staying valid + // because a signal handler may send another message, thereby altering + // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at + // its back so we can rely on the index staying valid at least. + unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) { totalInserted += insertedSize; @@ -1713,15 +2111,17 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) auto firstWriter = q->user((*from)->senderId()); if (q->readMarker(firstWriter) != timeline.crend()) { - promoteReadMarker(firstWriter, rev_iter_t(from) - 1); + roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() << "to" << *q->readMarker(firstWriter); } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); + roomChanges |= Change::UnreadNotifsChange; } Q_ASSERT(timeline.size() == timelineSize + totalInserted); + return roomChanges; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) @@ -1730,14 +2130,25 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) const auto timelineSize = timeline.size(); dropDuplicateEvents(events); - RoomEventsRange normalEvents { - events.begin(), events.end() //remove_if(events.begin(), events.end(), isRedaction) - }; - if (normalEvents.empty()) + if (events.empty()) return; - emit q->aboutToAddHistoricalMessages(normalEvents); - const auto insertedSize = moveEventsToTimeline(normalEvents, Older); + // In case of lazy-loading new members may be loaded with historical + // messages. Also, the cache doesn't store events with empty content; + // so when such events show up in the timeline they should be properly + // incorporated. + for (const auto& eptr: events) + { + const auto& e = *eptr; + if (e.isStateEvent() && + !currentState.contains({e.matrixType(), e.stateKey()})) + { + q->processStateEvent(e); + } + } + + emit q->aboutToAddHistoricalMessages(events); + const auto insertedSize = moveEventsToTimeline(events, Older); const auto from = timeline.crend() - insertedSize; qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize @@ -1754,52 +2165,89 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) << insertedSize << "event(s)," << et; } -bool Room::processStateEvent(const RoomEvent& e) +Room::Changes Room::processStateEvent(const RoomEvent& e) { + if (!e.isStateEvent()) + return Change::NoChange; + + const auto* oldStateEvent = std::exchange( + d->currentState[{e.matrixType(),e.stateKey()}], + static_cast<const StateEventBase*>(&e)); + Q_ASSERT(!oldStateEvent || + (oldStateEvent->matrixType() == e.matrixType() && + oldStateEvent->stateKey() == e.stateKey())); + if (!is<RoomMemberEvent>(e)) // Room member events are too numerous + qCDebug(EVENTS) << "Room state event:" << e; + return visit(e - , [this] (const RoomNameEvent& evt) { - d->name = evt.name(); - qCDebug(MAIN) << "Room name updated:" << d->name; - return true; + , [] (const RoomNameEvent&) { + return NameChange; } - , [this] (const RoomAliasesEvent& evt) { - d->aliases = evt.aliases(); - qCDebug(MAIN) << "Room aliases updated:" << d->aliases; - return true; + , [this,oldStateEvent] (const RoomAliasesEvent& ae) { + const auto previousAliases = oldStateEvent + ? static_cast<const RoomAliasesEvent*>(oldStateEvent)->aliases() + : QStringList(); + connection()->updateRoomAliases(id(), previousAliases, ae.aliases()); + return OtherChange; } , [this] (const RoomCanonicalAliasEvent& evt) { - d->canonicalAlias = evt.alias(); - if (!d->canonicalAlias.isEmpty()) - setObjectName(d->canonicalAlias); - qCDebug(MAIN) << "Room canonical alias updated:" - << d->canonicalAlias; - return true; + setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); + return CanonicalAliasChange; } - , [this] (const RoomTopicEvent& evt) { - d->topic = evt.topic(); - qCDebug(MAIN) << "Room topic updated:" << d->topic; - emit topicChanged(); - return false; + , [] (const RoomTopicEvent&) { + return TopicChange; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) - { - qCDebug(MAIN) << "Room avatar URL updated:" - << evt.url().toString(); emit avatarChanged(); - } - return false; + return AvatarChange; } - , [this] (const RoomMemberEvent& evt) { + , [this,oldStateEvent] (const RoomMemberEvent& evt) { auto* u = user(evt.userId()); - u->processEvent(evt, this); - if (u == localUser() && memberJoinState(u) == JoinState::Invite + const auto* oldMemberEvent = + static_cast<const RoomMemberEvent*>(oldStateEvent); + u->processEvent(evt, this, oldMemberEvent == nullptr); + const auto prevMembership = oldMemberEvent + ? oldMemberEvent->membership() : MembershipType::Leave; + if (u == localUser() && evt.membership() == MembershipType::Invite && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); - if( evt.membership() == MembershipType::Join ) + switch (prevMembership) { - if (memberJoinState(u) != JoinState::Join) + case MembershipType::Invite: + if (evt.membership() != prevMembership) + { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case MembershipType::Join: + if (evt.membership() == MembershipType::Invite) + qCWarning(MAIN) + << "Invalid membership change from Join to Invite:" + << evt; + if (evt.membership() != prevMembership) + { + disconnect(u, &User::nameAboutToChange, this, nullptr); + disconnect(u, &User::nameChanged, this, nullptr); + d->removeMemberFromMap(u->name(this), u); + emit userRemoved(u); + } + break; + default: + if (evt.membership() == MembershipType::Invite + || evt.membership() == MembershipType::Join) + { + d->membersLeft.removeOne(u); + Q_ASSERT(!d->membersLeft.contains(u)); + } + } + + switch(evt.membership()) + { + case MembershipType::Join: + if (prevMembership != MembershipType::Join) { d->insertMemberIntoMap(u); connect(u, &User::nameAboutToChange, this, @@ -1810,35 +2258,50 @@ bool Room::processStateEvent(const RoomEvent& e) connect(u, &User::nameChanged, this, [=] (QString, QString oldName, const Room* context) { if (context == this) + { d->renameMember(u, oldName); + emit memberRenamed(u); + } }); emit userAdded(u); } + break; + case MembershipType::Invite: + if (!d->usersInvited.contains(u)) + d->usersInvited.push_back(u); + break; + default: + if (!d->membersLeft.contains(u)) + d->membersLeft.append(u); } - else if( evt.membership() == MembershipType::Leave ) - { - if (memberJoinState(u) == JoinState::Join) - { - if (!d->membersLeft.contains(u)) - d->membersLeft.append(u); - d->removeMemberFromMap(u->name(this), u); - emit userRemoved(u); - } - } - return false; + return MembersChange; } - , [this] (const EncryptionEvent& evt) { - d->encryptionAlgorithm = evt.algorithm(); - qCDebug(MAIN) << "Encryption switched on in room" << id() - << "with algorithm" << d->encryptionAlgorithm; - emit encryption(); - return false; + , [this] (const EncryptionEvent&) { + emit encryption(); // It can only be done once, so emit it here. + return OtherChange; + } + , [this] (const RoomTombstoneEvent& evt) { + const auto successorId = evt.successorRoomId(); + if (auto* successor = connection()->room(successorId)) + emit upgraded(evt.serverMessage(), successor); + else + connectUntil(connection(), &Connection::loadedRoomState, this, + [this,successorId,serverMsg=evt.serverMessage()] + (Room* newRoom) { + if (newRoom->id() != successorId) + return false; + emit upgraded(serverMsg, newRoom); + return true; + }); + + return OtherChange; } ); } -void Room::processEphemeralEvent(EventPtr&& event) +Room::Changes Room::processEphemeralEvent(EventPtr&& event) { + Changes changes = NoChange; QElapsedTimer et; et.start(); if (auto* evt = eventCast<TypingEvent>(event)) { @@ -1877,7 +2340,7 @@ void Room::processEphemeralEvent(EventPtr&& event) continue; // FIXME, #185 auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join) - d->promoteReadMarker(u, newMarker); + changes |= d->promoteReadMarker(u, newMarker); } } else { @@ -1894,7 +2357,7 @@ void Room::processEphemeralEvent(EventPtr&& event) auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join && readMarker(u) == timelineEdge()) - d->setLastReadEvent(u, p.evtId); + changes |= d->setLastReadEvent(u, p.evtId); } } } @@ -1904,12 +2367,17 @@ void Room::processEphemeralEvent(EventPtr&& event) << evt->eventsWithReceipts().size() << "event(s) with" << totalReceipts << "receipt(s)," << et; } + return changes; } -void Room::processAccountDataEvent(EventPtr&& event) +Room::Changes Room::processAccountDataEvent(EventPtr&& event) { + Changes changes = NoChange; if (auto* evt = eventCast<TagEvent>(event)) + { d->setTags(evt->tags()); + changes |= Change::TagsChange; + } if (auto* evt = eventCast<ReadMarkerEvent>(event)) { @@ -1917,10 +2385,9 @@ void Room::processAccountDataEvent(EventPtr&& event) qCDebug(MAIN) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); - if (newMarker != timelineEdge()) - d->markMessagesAsRead(newMarker); - else - d->setLastReadEvent(localUser(), readEventId); + changes |= newMarker != timelineEdge() + ? d->markMessagesAsRead(newMarker) + : d->setLastReadEvent(localUser(), readEventId); } // For all account data events auto& currentData = d->accountData[event->matrixType()]; @@ -1933,52 +2400,40 @@ void Room::processAccountDataEvent(EventPtr&& event) qCDebug(MAIN) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); + return Change::AccountDataChange; } + return Change::NoChange; } -QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) const +template <typename ContT> +Room::Private::users_shortlist_t +Room::Private::buildShortlist(const ContT& users) const { - // This is part 3(i,ii,iii) in the room displayname algorithm described - // in the CS spec (see also Room::Private::updateDisplayname() ). - // The spec requires to sort users lexicographically by state_key (user id) - // and use disambiguated display names of two topmost users excluding - // the current one to render the name of the room. - - // std::array is the leanest C++ container - std::array<User*, 2> first_two = { {nullptr, nullptr} }; + // To calculate room display name the spec requires to sort users + // lexicographically by state_key (user id) and use disambiguated + // display names of two topmost users excluding the current one to render + // the name of the room. The below code selects 3 topmost users, + // slightly extending the spec. + users_shortlist_t shortlist { }; // Prefill with nullptrs std::partial_sort_copy( - userlist.begin(), userlist.end(), - first_two.begin(), first_two.end(), - [this](const User* u1, const User* u2) { - // Filter out the "me" user so that it never hits the room name + users.begin(), users.end(), + shortlist.begin(), shortlist.end(), + [this] (const User* u1, const User* u2) { + // localUser(), if it's in the list, is sorted below all others return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id()); } ); + return shortlist; +} - // Spec extension. A single person in the chat but not the local user - // (the local user is invited). - if (userlist.size() == 1 && !isLocalUser(first_two.front()) && - joinState == JoinState::Invite) - return tr("Invitation from %1") - .arg(q->roomMembername(first_two.front())); - - // i. One-on-one chat. first_two[1] == localUser() in this case. - if (userlist.size() == 2) - return q->roomMembername(first_two[0]); - - // ii. Two users besides the current one. - if (userlist.size() == 3) - return tr("%1 and %2") - .arg(q->roomMembername(first_two[0]), - q->roomMembername(first_two[1])); - - // iii. More users. - if (userlist.size() > 3) - return tr("%1 and %Ln other(s)", "", userlist.size() - 3) - .arg(q->roomMembername(first_two[0])); - - // userlist.size() < 2 - apparently, there's only current user in the room - return QString(); +Room::Private::users_shortlist_t +Room::Private::buildShortlist(const QStringList& userIds) const +{ + QList<User*> users; + users.reserve(userIds.size()); + for (const auto& h: userIds) + users.push_back(q->user(h)); + return buildShortlist(users); } QString Room::Private::calculateDisplayname() const @@ -1987,27 +2442,73 @@ QString Room::Private::calculateDisplayname() const // Numbers below refer to respective parts in the spec. // 1. Name (from m.room.name) - if (!name.isEmpty()) { - return name; + auto dispName = q->name(); + if (!dispName.isEmpty()) { + return dispName; } // 2. Canonical alias - if (!canonicalAlias.isEmpty()) - return canonicalAlias; + dispName = q->canonicalAlias(); + if (!dispName.isEmpty()) + return dispName; // Using m.room.aliases in naming is explicitly discouraged by the spec - //if (!aliases.empty() && !aliases.at(0).isEmpty()) - // return aliases.at(0); + + // Supplementary code for 3 and 4: build the shortlist of users whose names + // will be used to construct the room name. Takes into account MSC688's + // "heroes" if available. + + const bool localUserIsIn = joinState == JoinState::Join; + const bool emptyRoom = membersMap.isEmpty() || + (membersMap.size() == 1 && isLocalUser(*membersMap.begin())); + const bool nonEmptySummary = + !summary.heroes.omitted() && !summary.heroes->empty(); + auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value()) : + !emptyRoom ? buildShortlist(membersMap) : + users_shortlist_t { }; + + // When lazy-loading is on, we can rely on the heroes list. + // If it's off, the below code gathers invited and left members. + // NB: including invitations, if any, into naming is a spec extension. + // This kicks in when there's no lazy loading and it's a room with + // the local user as the only member, with more users invited. + if (!shortlist.front() && localUserIsIn) + shortlist = buildShortlist(usersInvited); + + if (!shortlist.front()) // Still empty shortlist; use left members + shortlist = buildShortlist(membersLeft); + + QStringList names; + for (auto u: shortlist) + { + if (u == nullptr || isLocalUser(u)) + break; + // Only disambiguate if the room is not empty + names.push_back(u->displayname(emptyRoom ? nullptr : q)); + } + + const auto usersCountExceptLocal = + !emptyRoom ? q->joinedCount() - int(joinState == JoinState::Join) : + !usersInvited.empty() ? usersInvited.count() : + membersLeft.size() - int(joinState == JoinState::Leave); + if (usersCountExceptLocal > int(shortlist.size())) + names << + tr("%Ln other(s)", + "Used to make a room name from user names: A, B and _N others_", + usersCountExceptLocal - int(shortlist.size())); + const auto namesList = QLocale().createSeparatedList(names); // 3. Room members - QString topMemberNames = roomNameFromMemberNames(membersMap.values()); - if (!topMemberNames.isEmpty()) - return topMemberNames; + if (!emptyRoom) + return namesList; + + // (Spec extension) Invited users + if (!usersInvited.empty()) + return tr("Empty room (invited: %1)").arg(namesList); // 4. Users that previously left the room - topMemberNames = roomNameFromMemberNames(membersLeft); - if (!topMemberNames.isEmpty()) - return tr("Empty room (was: %1)").arg(topMemberNames); + if (membersLeft.size() > 0) + return tr("Empty room (was: %1)").arg(namesList); // 5. Fail miserably return tr("Empty room (%1)").arg(id); @@ -2026,58 +2527,27 @@ void Room::Private::updateDisplayname() } } -void appendStateEvent(QJsonArray& events, const QString& type, - const QJsonObject& content, const QString& stateKey = {}) -{ - if (!content.isEmpty() || !stateKey.isEmpty()) - { - auto json = basicEventJson(type, content); - json.insert(QStringLiteral("state_key"), stateKey); - events.append(json); - } -} - -#define ADD_STATE_EVENT(events, type, name, content) \ - appendStateEvent((events), QStringLiteral(type), \ - {{ QStringLiteral(name), content }}); - -void appendEvent(QJsonArray& events, const QString& type, - const QJsonObject& content) -{ - if (!content.isEmpty()) - events.append(basicEventJson(type, content)); -} - -template <typename EvtT> -void appendEvent(QJsonArray& events, const EvtT& event) -{ - appendEvent(events, EvtT::matrixTypeId(), event.toJson()); -} - QJsonObject Room::Private::toJson() const { QElapsedTimer et; et.start(); QJsonObject result; + addParam<IfNotEmpty>(result, QStringLiteral("summary"), summary); { QJsonArray stateEvents; - ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name); - ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic); - ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url", - avatar.url().toString()); - ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases", - QJsonArray::fromStringList(aliases)); - ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias", - canonicalAlias); - ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm", - encryptionAlgorithm); - - for (const auto *m : membersMap) - appendStateEvent(stateEvents, QStringLiteral("m.room.member"), - { { QStringLiteral("membership"), QStringLiteral("join") } - , { QStringLiteral("displayname"), m->rawName(q) } - , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() } - }, m->id()); + for (const auto* evt: currentState) + { + Q_ASSERT(evt->isStateEvent()); + if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) || + evt->contentJson().isEmpty()) + continue; + + auto json = evt->fullJson(); + auto unsignedJson = evt->unsignedJson(); + unsignedJson.remove(QStringLiteral("prev_content")); + json[UnsignedKeyL] = unsignedJson; + stateEvents.append(json); + } const auto stateObjName = joinState == JoinState::Invite ? QStringLiteral("invite_state") : QStringLiteral("state"); @@ -2085,14 +2555,17 @@ QJsonObject Room::Private::toJson() const QJsonObject {{ QStringLiteral("events"), stateEvents }}); } - QJsonArray accountDataEvents; if (!accountData.empty()) { + QJsonArray accountDataEvents; for (const auto& e: accountData) - appendEvent(accountDataEvents, e.first, e.second->contentJson()); + { + if (!e.second->contentJson().isEmpty()) + accountDataEvents.append(e.second->fullJson()); + } + result.insert(QStringLiteral("account_data"), + QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } - result.insert(QStringLiteral("account_data"), - QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, unreadMessages } }; @@ -18,7 +18,7 @@ #pragma once -#include "jobs/syncjob.h" +#include "csapi/message_pagination.h" #include "events/roommessageevent.h" #include "events/accountdataevents.h" #include "eventitem.h" @@ -33,6 +33,8 @@ namespace QMatrixClient { class Event; + class Avatar; + class SyncRoomData; class RoomMemberEvent; class Connection; class User; @@ -41,10 +43,17 @@ namespace QMatrixClient class SetRoomStateWithKeyJob; class RedactEventJob; + /** The data structure used to expose file transfer information to views + * + * This is specifically tuned to work with QML exposing all traits as + * Q_PROPERTY values. + */ class FileTransferInfo { Q_GADGET + Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) Q_PROPERTY(bool active READ active CONSTANT) + Q_PROPERTY(bool started READ started CONSTANT) Q_PROPERTY(bool completed READ completed CONSTANT) Q_PROPERTY(bool failed READ failed CONSTANT) Q_PROPERTY(int progress MEMBER progress CONSTANT) @@ -52,16 +61,17 @@ namespace QMatrixClient Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) public: - enum Status { None, Started, Completed, Failed }; + enum Status { None, Started, Completed, Failed, Cancelled }; Status status = None; + bool isUpload = false; int progress = 0; int total = -1; QUrl localDir { }; QUrl localPath { }; - bool active() const - { return status == Started || status == Completed; } + bool started() const { return status == Started; } bool completed() const { return status == Completed; } + bool active() const { return started() || completed(); } bool failed() const { return status == Failed; } }; @@ -71,10 +81,14 @@ namespace QMatrixClient Q_PROPERTY(Connection* connection READ connection CONSTANT) Q_PROPERTY(User* localUser READ localUser CONSTANT) Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded) + Q_PROPERTY(bool isUnstable READ isUnstable NOTIFY stabilityUpdated) + Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) + Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) Q_PROPERTY(QString name READ name NOTIFY namesChanged) Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) - Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged) + Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) @@ -83,6 +97,9 @@ namespace QMatrixClient Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) + Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) + Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) + Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged) Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged) Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) @@ -95,12 +112,34 @@ namespace QMatrixClient Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged) Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged) + Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged) + public: using Timeline = std::deque<TimelineItem>; using PendingEvents = std::vector<PendingEventItem>; using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; + enum Change : uint { + NoChange = 0x0, + NameChange = 0x1, + CanonicalAliasChange = 0x2, + TopicChange = 0x4, + UnreadNotifsChange = 0x8, + AvatarChange = 0x10, + JoinStateChange = 0x20, + TagsChange = 0x40, + MembersChange = 0x80, + /* = 0x100, */ + AccountDataChange = 0x200, + SummaryChange = 0x400, + ReadMarkerChange = 0x800, + OtherChange = 0x8000, + AnyChange = 0xFFFF + }; + Q_DECLARE_FLAGS(Changes, Change) + Q_FLAG(Changes) + Room(Connection* connection, QString id, JoinState initialJoinState); ~Room() override; @@ -109,6 +148,10 @@ namespace QMatrixClient Connection* connection() const; User* localUser() const; const QString& id() const; + QString version() const; + bool isUnstable() const; + QString predecessorId() const; + QString successorId() const; QString name() const; QStringList aliases() const; QString canonicalAlias() const; @@ -116,15 +159,22 @@ namespace QMatrixClient QString topic() const; QString avatarMediaId() const; QUrl avatarUrl() const; + const Avatar& avatarObject() const; Q_INVOKABLE JoinState joinState() const; Q_INVOKABLE QList<User*> usersTyping() const; QList<User*> membersLeft() const; Q_INVOKABLE QList<User*> users() const; QStringList memberNames() const; + [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] int memberCount() const; int timelineSize() const; bool usesEncryption() const; + int joinedCount() const; + int invitedCount() const; + int totalMemberCount() const; + + GetRoomEventsJob* eventsHistoryJob() const; /** * Returns a square room avatar with the given size and requests it @@ -177,9 +227,16 @@ namespace QMatrixClient const Timeline& messageEvents() const; const PendingEvents& pendingEvents() const; /** - * A convenience method returning the read marker to - * the before-oldest message + * A convenience method returning the read marker to the position + * before the "oldest" event; same as messageEvents().crend() */ + rev_iter_t historyEdge() const; + /** + * A convenience method returning the iterator beyond the latest + * arrived event; same as messageEvents().cend() + */ + Timeline::const_iterator syncEdge() const; + /// \deprecated Use historyEdge instead rev_iter_t timelineEdge() const; Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; @@ -187,8 +244,17 @@ namespace QMatrixClient rev_iter_t findInTimeline(TimelineItem::index_t index) const; rev_iter_t findInTimeline(const QString& evtId) const; + PendingEvents::iterator findPendingEvent(const QString & txnId); + PendingEvents::const_iterator findPendingEvent(const QString & txnId) const; bool displayed() const; + /// Mark the room as currently displayed to the user + /** + * Marking the room displayed causes the room to obtain the full + * list of members if it's been lazy-loaded before; in the future + * it may do more things bound to "screen time" of the room, e.g. + * measure that "screen time". + */ void setDisplayed(bool displayed = true); QString firstDisplayedEventId() const; rev_iter_t firstDisplayedMarker() const; @@ -288,11 +354,32 @@ namespace QMatrixClient /// Get the list of users this room is a direct chat with QList<User*> directChatUsers() const; - Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); - Q_INVOKABLE QUrl urlToDownload(const QString& eventId); - Q_INVOKABLE QString fileNameToDownload(const QString& eventId); + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; + Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; + + /// Get a file name for downloading for a given event id + /*! + * The event MUST be RoomMessageEvent and have content + * for downloading. \sa RoomMessageEvent::hasContent + */ + Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; + + /// Get information on file upload/download + /*! + * \param id uploads are identified by the corresponding event's + * transactionId (because uploads are done before + * the event is even sent), while downloads are using + * the normal event id for identifier. + */ Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; + /// Get the URL to the actual file source in a unified way + /*! + * For uploads it will return a URL to a local file; for downloads + * the URL will be taken from the corresponding room event. + */ + Q_INVOKABLE QUrl fileSource(const QString& id) const; + /** Pretty-prints plain text into HTML * As of now, it's exactly the same as QMatrixClient::prettyPrint(); * in the future, it will also linkify room aliases, mxids etc. @@ -314,11 +401,17 @@ namespace QMatrixClient Q_INVOKABLE bool supportsCalls() const; public slots: + /** Check whether the room should be upgraded */ + void checkVersion(); + QString postMessage(const QString& plainText, MessageEventType type); QString postPlainText(const QString& plainText); QString postHtmlMessage(const QString& plainText, - const QString& html, MessageEventType type); + const QString& html, + MessageEventType type = MessageEventType::Text); QString postHtmlText(const QString& plainText, const QString& html); + QString postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile = false); /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one @@ -332,8 +425,12 @@ namespace QMatrixClient void discardMessage(const QString& txnId); void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); + void setAliases(const QStringList& aliases); void setTopic(const QString& newTopic); + /// You shouldn't normally call this method; it's here for debugging + void refreshDisplayName(); + void getPreviousContent(int limit = 10); void inviteToRoom(const QString& memberId); @@ -356,19 +453,63 @@ namespace QMatrixClient /// Mark all messages in the room as read void markAllMessagesAsRead(); + /// Whether the current user is allowed to upgrade the room + bool canSwitchVersions() const; + + /// Switch the room's version (aka upgrade) + void switchVersion(QString newVersion); + signals: + /// Initial set of state events has been loaded + /** + * The initial set is what comes from the initial sync for the room. + * This includes all basic things like RoomCreateEvent, + * RoomNameEvent, a (lazy-loaded, not full) set of RoomMemberEvents + * etc. This is a per-room reflection of Connection::loadedRoomState + * \sa Connection::loadedRoomState + */ + void baseStateLoaded(); + void eventsHistoryJobChanged(); void aboutToAddHistoricalMessages(RoomEventsRange events); void aboutToAddNewMessages(RoomEventsRange events); void addedMessages(int fromIndex, int toIndex); - void pendingEventAboutToAdd(); + /// The event is about to be appended to the list of pending events + void pendingEventAboutToAdd(RoomEvent* event); + /// An event has been appended to the list of pending events void pendingEventAdded(); + /// The remote echo has arrived with the sync and will be merged + /// with its local counterpart + /** NB: Requires a sync loop to be emitted */ void pendingEventAboutToMerge(RoomEvent* serverEvent, int pendingEventIndex); + /// The remote and local copies of the event have been merged + /** NB: Requires a sync loop to be emitted */ void pendingEventMerged(); + /// An event will be removed from the list of pending events void pendingEventAboutToDiscard(int pendingEventIndex); + /// An event has just been removed from the list of pending events void pendingEventDiscarded(); + /// The status of a pending event has changed + /** \sa PendingEventItem::deliveryStatus */ void pendingEventChanged(int pendingEventIndex); + /// The server accepted the message + /** This is emitted when an event sending request has successfully + * completed. This does not mean that the event is already in the + * local timeline, only that the server has accepted it. + * \param txnId transaction id assigned by the client during sending + * \param eventId event id assigned by the server upon acceptance + * \sa postEvent, postPlainText, postMessage, postHtmlMessage + * \sa pendingEventMerged, aboutToAddNewMessages + */ + void messageSent(QString txnId, QString eventId); + /** A common signal for various kinds of changes in the room + * Aside from all changes in the room state + * @param changes a set of flags describing what changes occured + * upon the last sync + * \sa StateChange + */ + void changed(Changes changes); /** * \brief The room name, the canonical alias or other aliases changed * @@ -383,7 +524,17 @@ namespace QMatrixClient void userRemoved(User* user); void memberAboutToRename(User* user, QString newName); void memberRenamed(User* user); + /// The list of members has changed + /** Emitted no more than once per sync, this is a good signal to + * for cases when some action should be done upon any change in + * the member list. If you need per-item granularity you should use + * userAdded, userRemoved and memberAboutToRename / memberRenamed + * instead. + */ void memberListChanged(); + /// The previously lazy-loaded members list is now loaded entirely + /// \sa setDisplayed + void allMembersLoaded(); void encryption(); void joinStateChanged(JoinState oldState, JoinState newState); @@ -415,30 +566,40 @@ namespace QMatrixClient void fileTransferCancelled(QString id); void callEvent(Room* room, const RoomEvent* event); - /// The room is about to be deleted - void beforeDestruction(Room*); - public: // Used by Connection - not a part of the client API - QJsonObject toJson() const; - void updateData(SyncRoomData&& data ); + /// The room's version stability may have changed + void stabilityUpdated(QString recommendedDefault, + QStringList stableVersions); + /// This room has been upgraded and won't receive updates anymore + void upgraded(QString serverMessage, Room* successor); + /// An attempted room upgrade has failed + void upgradeFailed(QString errorMessage); - // Clients should use Connection::joinRoom() and Room::leaveRoom() - // to change the room state - void setJoinState( JoinState state ); + /// The room is about to be deleted + void beforeDestruction(Room*); protected: /// Returns true if any of room names/aliases has changed - virtual bool processStateEvent(const RoomEvent& e); - virtual void processEphemeralEvent(EventPtr&& event); - virtual void processAccountDataEvent(EventPtr&& event); + virtual Changes processStateEvent(const RoomEvent& e); + virtual Changes processEphemeralEvent(EventPtr&& event); + virtual Changes processAccountDataEvent(EventPtr&& event); virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { } virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { } virtual void onRedaction(const RoomEvent& /*prevEvent*/, const RoomEvent& /*after*/) { } + virtual QJsonObject toJson() const; + virtual void updateData(SyncRoomData&& data, bool fromCache = false); private: + friend class Connection; + class Private; Private* d; + + // This is called from Connection, reflecting a state change that + // arrived from the server. Clients should use + // Connection::joinRoom() and Room::leaveRoom() to change the state. + void setJoinState(JoinState state); }; class MemberSorter @@ -461,3 +622,4 @@ namespace QMatrixClient }; } // namespace QMatrixClient Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo) +Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::Room::Changes) diff --git a/lib/settings.cpp b/lib/settings.cpp index 852e19cb..124d7042 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -84,18 +84,21 @@ void SettingsGroup::remove(const QString& key) Settings::remove(fullKey); } -QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId) -QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName) +QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId) +QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName) QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) +static const auto HomeserverKey = QStringLiteral("homeserver"); +static const auto AccessTokenKey = QStringLiteral("access_token"); + QUrl AccountSettings::homeserver() const { - return QUrl::fromUserInput(value("homeserver").toString()); + return QUrl::fromUserInput(value(HomeserverKey).toString()); } void AccountSettings::setHomeserver(const QUrl& url) { - setValue("homeserver", url.toString()); + setValue(HomeserverKey, url.toString()); } QString AccountSettings::userId() const @@ -105,19 +108,19 @@ QString AccountSettings::userId() const QString AccountSettings::accessToken() const { - return value("access_token").toString(); + return value(AccessTokenKey).toString(); } void AccountSettings::setAccessToken(const QString& accessToken) { qCWarning(MAIN) << "Saving access_token to QSettings is insecure." " Developers, please save access_token separately."; - setValue("access_token", accessToken); + setValue(AccessTokenKey, accessToken); } void AccountSettings::clearAccessToken() { - legacySettings.remove("access_token"); - legacySettings.remove("device_id"); // Force the server to re-issue it - remove("access_token"); + legacySettings.remove(AccessTokenKey); + legacySettings.remove(QStringLiteral("device_id")); // Force the server to re-issue it + remove(AccessTokenKey); } diff --git a/lib/settings.h b/lib/settings.h index 0b3ecaff..759bda35 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -119,7 +119,7 @@ type classname::propname() const \ \ void classname::setter(type newValue) \ { \ - setValue(QStringLiteral(qsettingname), newValue); \ + setValue(QStringLiteral(qsettingname), std::move(newValue)); \ } \ class AccountSettings: public SettingsGroup diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp new file mode 100644 index 00000000..21517884 --- /dev/null +++ b/lib/syncdata.cpp @@ -0,0 +1,228 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "syncdata.h" + +#include "events/eventloader.h" + +#include <QtCore/QFile> +#include <QtCore/QFileInfo> + +using namespace QMatrixClient; + +const QString SyncRoomData::UnreadCountKey = + QStringLiteral("x-qmatrixclient.unread_count"); + +bool RoomSummary::isEmpty() const +{ + return joinedMemberCount.omitted() && invitedMemberCount.omitted() && + heroes.omitted(); +} + +bool RoomSummary::merge(const RoomSummary& other) +{ + // Using bitwise OR to prevent computation shortcut. + return + joinedMemberCount.merge(other.joinedMemberCount) | + invitedMemberCount.merge(other.invitedMemberCount) | + heroes.merge(other.heroes); +} + +QDebug QMatrixClient::operator<<(QDebug dbg, const RoomSummary& rs) +{ + QDebugStateSaver _(dbg); + QStringList sl; + if (!rs.joinedMemberCount.omitted()) + sl << QStringLiteral("joined: %1").arg(rs.joinedMemberCount.value()); + if (!rs.invitedMemberCount.omitted()) + sl << QStringLiteral("invited: %1").arg(rs.invitedMemberCount.value()); + if (!rs.heroes.omitted()) + sl << QStringLiteral("heroes: [%1]").arg(rs.heroes.value().join(',')); + dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); + return dbg; +} + +void JsonObjectConverter<RoomSummary>::dumpTo(QJsonObject& jo, + const RoomSummary& rs) +{ + addParam<IfNotEmpty>(jo, QStringLiteral("m.joined_member_count"), + rs.joinedMemberCount); + addParam<IfNotEmpty>(jo, QStringLiteral("m.invited_member_count"), + rs.invitedMemberCount); + addParam<IfNotEmpty>(jo, QStringLiteral("m.heroes"), rs.heroes); +} + +void JsonObjectConverter<RoomSummary>::fillFrom(const QJsonObject& jo, + RoomSummary& rs) +{ + fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount); + fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount); + fromJson(jo["m.heroes"_ls], rs.heroes); +} + +template <typename EventsArrayT, typename StrT> +inline EventsArrayT load(const QJsonObject& batches, StrT keyName) +{ + return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); +} + +SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, + const QJsonObject& room_) + : roomId(roomId_) + , joinState(joinState_) + , summary(fromJson<RoomSummary>(room_["summary"_ls])) + , state(load<StateEvents>(room_, joinState == JoinState::Invite + ? "invite_state"_ls : "state"_ls)) +{ + switch (joinState) { + case JoinState::Join: + ephemeral = load<Events>(room_, "ephemeral"_ls); + FALLTHROUGH; + case JoinState::Leave: + { + accountData = load<Events>(room_, "account_data"_ls); + timeline = load<RoomEvents>(room_, "timeline"_ls); + const auto timelineJson = room_.value("timeline"_ls).toObject(); + timelineLimited = timelineJson.value("limited"_ls).toBool(); + timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); + + break; + } + default: /* nothing on top of state */; + } + + const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); + unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); + highlightCount = unreadJson.value("highlight_count"_ls).toInt(); + notificationCount = unreadJson.value("notification_count"_ls).toInt(); + if (highlightCount > 0 || notificationCount > 0) + qCDebug(SYNCJOB) << "Room" << roomId_ + << "has highlights:" << highlightCount + << "and notifications:" << notificationCount; +} + +SyncData::SyncData(const QString& cacheFileName) +{ + QFileInfo cacheFileInfo { cacheFileName }; + auto json = loadJson(cacheFileName); + auto requiredVersion = std::get<0>(cacheVersion()); + auto actualVersion = json.value("cache_version"_ls).toObject() + .value("major"_ls).toInt(); + if (actualVersion == requiredVersion) + parseJson(json, cacheFileInfo.absolutePath() + '/'); + else + qCWarning(MAIN) + << "Major version of the cache file is" << actualVersion << "but" + << requiredVersion << "is required; discarding the cache"; +} + +SyncDataList&& SyncData::takeRoomData() +{ + return move(roomData); +} + +QString SyncData::fileNameForRoom(QString roomId) +{ + roomId.replace(':', '_'); + return roomId + ".json"; +} + +Events&& SyncData::takePresenceData() +{ + return std::move(presenceData); +} + +Events&& SyncData::takeAccountData() +{ + return std::move(accountData); +} + +Events&& SyncData::takeToDeviceEvents() +{ + return std::move(toDeviceEvents); +} + +QJsonObject SyncData::loadJson(const QString& fileName) +{ + QFile roomFile { fileName }; + if (!roomFile.exists()) + { + qCWarning(MAIN) << "No state cache file" << fileName; + return {}; + } + if(!roomFile.open(QIODevice::ReadOnly)) + { + qCWarning(MAIN) << "Failed to open state cache file" + << roomFile.fileName(); + return {}; + } + auto data = roomFile.readAll(); + + const auto json = + (data.startsWith('{') ? QJsonDocument::fromJson(data) + : QJsonDocument::fromBinaryData(data)).object(); + if (json.isEmpty()) + { + qCWarning(MAIN) << "State cache in" << fileName + << "is broken or empty, discarding"; + } + return json; +} + +void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) +{ + QElapsedTimer et; et.start(); + + nextBatch_ = json.value("next_batch"_ls).toString(); + presenceData = load<Events>(json, "presence"_ls); + accountData = load<Events>(json, "account_data"_ls); + toDeviceEvents = load<Events>(json, "to_device"_ls); + + auto rooms = json.value("rooms"_ls).toObject(); + JoinStates::Int ii = 1; // ii is used to make a JoinState value + auto totalRooms = 0; + auto totalEvents = 0; + for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) + { + const auto rs = rooms.value(JoinStateStrings[i]).toObject(); + // We have a Qt container on the right and an STL one on the left + roomData.reserve(static_cast<size_t>(rs.size())); + for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) + { + auto roomJson = roomIt->isObject() + ? roomIt->toObject() + : loadJson(baseDir + fileNameForRoom(roomIt.key())); + if (roomJson.isEmpty()) + { + unresolvedRoomIds.push_back(roomIt.key()); + continue; + } + roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson); + const auto& r = roomData.back(); + totalEvents += r.state.size() + r.ephemeral.size() + + r.accountData.size() + r.timeline.size(); + } + totalRooms += rs.size(); + } + if (!unresolvedRoomIds.empty()) + qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(','); + if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" + << totalRooms << "room(s)," + << totalEvents << "event(s) in" << et; +} diff --git a/lib/syncdata.h b/lib/syncdata.h new file mode 100644 index 00000000..8694626e --- /dev/null +++ b/lib/syncdata.h @@ -0,0 +1,116 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "joinstate.h" +#include "events/stateevent.h" + +namespace QMatrixClient { + /// Room summary, as defined in MSC688 + /** + * Every member of this structure is an Omittable; as per the MSC, only + * changed values are sent from the server so if nothing is in the payload + * the respective member will be omitted. In particular, `heroes.omitted()` + * means that nothing has come from the server; heroes.value().isEmpty() + * means a peculiar case of a room with the only member - the current user. + */ + struct RoomSummary + { + Omittable<int> joinedMemberCount; + Omittable<int> invitedMemberCount; + Omittable<QStringList> heroes; //< mxids of users to take part in the room name + + bool isEmpty() const; + /// Merge the contents of another RoomSummary object into this one + /// \return true, if the current object has changed; false otherwise + bool merge(const RoomSummary& other); + + friend QDebug operator<<(QDebug dbg, const RoomSummary& rs); + }; + + template <> + struct JsonObjectConverter<RoomSummary> + { + static void dumpTo(QJsonObject& jo, const RoomSummary& rs); + static void fillFrom(const QJsonObject& jo, RoomSummary& rs); + }; + + class SyncRoomData + { + public: + QString roomId; + JoinState joinState; + RoomSummary summary; + StateEvents state; + RoomEvents timeline; + Events ephemeral; + Events accountData; + + bool timelineLimited; + QString timelinePrevBatch; + int unreadCount; + int highlightCount; + int notificationCount; + + SyncRoomData(const QString& roomId, JoinState joinState_, + const QJsonObject& room_); + SyncRoomData(SyncRoomData&&) = default; + SyncRoomData& operator=(SyncRoomData&&) = default; + + static const QString UnreadCountKey; + }; + + // QVector cannot work with non-copiable objects, std::vector can. + using SyncDataList = std::vector<SyncRoomData>; + + class SyncData + { + public: + SyncData() = default; + explicit SyncData(const QString& cacheFileName); + /** Parse sync response into room events + * \param json response from /sync or a room state cache + * \return the list of rooms with missing cache files; always + * empty when parsing response from /sync + */ + void parseJson(const QJsonObject& json, const QString& baseDir = {}); + + Events&& takePresenceData(); + Events&& takeAccountData(); + Events&& takeToDeviceEvents(); + SyncDataList&& takeRoomData(); + + QString nextBatch() const { return nextBatch_; } + + QStringList unresolvedRooms() const { return unresolvedRoomIds; } + + static std::pair<int, int> cacheVersion() { return { 10, 0 }; } + static QString fileNameForRoom(QString roomId); + + private: + QString nextBatch_; + Events presenceData; + Events accountData; + Events toDeviceEvents; + SyncDataList roomData; + QStringList unresolvedRoomIds; + + static QJsonObject loadJson(const QString& fileName); + }; +} // namespace QMatrixClient diff --git a/lib/user.cpp b/lib/user.cpp index bfd23ae2..17db5760 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -59,7 +59,7 @@ class User::Private QMultiHash<QString, const Room*> otherNames; Avatar mostUsedAvatar { makeAvatar({}) }; std::vector<Avatar> otherAvatars; - auto otherAvatar(QUrl url) + auto otherAvatar(const QUrl& url) { return std::find_if(otherAvatars.begin(), otherAvatars.end(), [&url] (const auto& av) { return av.url() == url; }); @@ -69,7 +69,7 @@ class User::Private mutable int totalRooms = 0; QString nameForRoom(const Room* r, const QString& hint = {}) const; - void setNameForRoom(const Room* r, QString newName, QString oldName); + void setNameForRoom(const Room* r, QString newName, const QString& oldName); QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; void setAvatarForRoom(const Room* r, const QUrl& newUrl, const QUrl& oldUrl); @@ -82,7 +82,8 @@ class User::Private QString User::Private::nameForRoom(const Room* r, const QString& hint) const { // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedName || otherNames.contains(hint, r)) + if (!hint.isNull() + && (hint == mostUsedName || otherNames.contains(hint, r))) return hint; return otherNames.key(r, mostUsedName); } @@ -90,7 +91,7 @@ QString User::Private::nameForRoom(const Room* r, const QString& hint) const static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; void User::Private::setNameForRoom(const Room* r, QString newName, - QString oldName) + const QString& oldName) { Q_ASSERT(oldName != newName); Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); @@ -117,7 +118,8 @@ void User::Private::setNameForRoom(const Room* r, QString newName, et.start(); } - for (auto* r1: connection->roomMap()) + const auto& roomMap = connection->roomMap(); + for (auto* r1: roomMap) if (nameForRoom(r1) == mostUsedName) otherNames.insert(mostUsedName, r1); @@ -177,7 +179,8 @@ void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, auto nextMostUsedIt = otherAvatar(newUrl); Q_ASSERT(nextMostUsedIt != otherAvatars.end()); std::swap(mostUsedAvatar, *nextMostUsedIt); - for (const auto* r1: connection->roomMap()) + const auto& roomMap = connection->roomMap(); + for (const auto* r1: roomMap) if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) avatarsToRooms.insert(nextMostUsedIt->url(), r1); @@ -264,8 +267,9 @@ void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, void User::rename(const QString& newName) { - auto job = connection()->callApi<SetDisplayNameJob>(id(), newName); - connect(job, &BaseJob::success, this, [=] { updateName(newName); }); + const auto actualNewName = sanitized(newName); + connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName), + &BaseJob::success, this, [=] { updateName(actualNewName); }); } void User::rename(const QString& newName, const Room* r) @@ -279,10 +283,11 @@ void User::rename(const QString& newName, const Room* r) } Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, "Attempt to rename a user that's not a room member"); + const auto actualNewName = sanitized(newName); MemberEventContent evtC; - evtC.displayName = newName; - auto job = r->setMemberState(id(), RoomMemberEvent(move(evtC))); - connect(job, &BaseJob::success, this, [=] { updateName(newName, r); }); + evtC.displayName = actualNewName; + connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))), + &BaseJob::success, this, [=] { updateName(actualNewName, r); }); } bool User::setAvatar(const QString& fileName) @@ -312,6 +317,11 @@ void User::unmarkIgnore() connection()->removeFromIgnoredUsers(this); } +bool User::isIgnored() const +{ + return connection()->isIgnored(this); +} + void User::Private::setAvatarOnServer(QString contentUri, User* q) { auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri); @@ -372,19 +382,18 @@ QUrl User::avatarUrl(const Room* room) const return avatarObject(room).url(); } -void User::processEvent(const RoomMemberEvent& event, const Room* room) +void User::processEvent(const RoomMemberEvent& event, const Room* room, + bool firstMention) { Q_ASSERT(room); + + if (firstMention) + ++d->totalRooms; + if (event.membership() != MembershipType::Invite && event.membership() != MembershipType::Join) return; - auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave && - (event.membership() == MembershipType::Join || - event.membership() == MembershipType::Invite); - if (aboutToEnter) - ++d->totalRooms; - auto newName = event.displayName(); // `bridged` value uses the same notification signal as the name; // it is assumed that first setting of the bridge occurs together with @@ -392,7 +401,7 @@ void User::processEvent(const RoomMemberEvent& event, const Room* room) // exceptionally rare (the only reasonable case being that the bridge // changes the naming convention). For the same reason room-specific // bridge tags are not supported at all. - QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$"); + QRegularExpression reSuffix(QStringLiteral(" \\((IRC|Gitter|Telegram)\\)$")); auto match = reSuffix.match(newName); if (match.hasMatch()) { @@ -105,7 +105,11 @@ namespace QMatrixClient QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; - void processEvent(const RoomMemberEvent& event, const Room* r); + /// This method is for internal use and should not be called + /// from client code + // FIXME: Move it away to private in lib 0.6 + void processEvent(const RoomMemberEvent& event, const Room* r, + bool firstMention); public slots: /** Set a new name in the global user profile */ @@ -125,6 +129,8 @@ namespace QMatrixClient void ignore(); /** Remove the user from the ignore list */ void unmarkIgnore(); + /** Check whether the user is in ignore list */ + bool isIgnored() const; signals: void nameAboutToChange(QString newName, QString oldName, diff --git a/lib/util.cpp b/lib/util.cpp index 1773fcfe..17674b84 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -19,6 +19,9 @@ #include "util.h" #include <QtCore/QRegularExpression> +#include <QtCore/QStandardPaths> +#include <QtCore/QDir> +#include <QtCore/QStringBuilder> static const auto RegExpOptions = QRegularExpression::CaseInsensitiveOption @@ -35,29 +38,128 @@ static void linkifyUrls(QString& htmlEscapedText) // comma or dot // Note: outer parentheses are a part of C++ raw string delimiters, not of // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). + // Note2: the next-outer parentheses are \N in the replacement. static const QRegularExpression FullUrlRegExp(QStringLiteral( - R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" + R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" ), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] static const QRegularExpression EmailAddressRegExp(QStringLiteral( - R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))" + R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))" + ), RegExpOptions); + // An interim liberal implementation of + // https://matrix.org/docs/spec/appendices.html#identifier-grammar + static const QRegularExpression MxIdRegExp(QStringLiteral( + R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:[-.a-z0-9]+))" ), RegExpOptions); - // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,& + // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," htmlEscapedText.replace(EmailAddressRegExp, QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)")); htmlEscapedText.replace(FullUrlRegExp, QStringLiteral(R"(<a href="\1">\1</a>)")); + htmlEscapedText.replace(MxIdRegExp, + QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)")); +} + +QString QMatrixClient::sanitized(const QString& plainText) +{ + auto text = plainText; + text.remove(QChar(0x202e)); + text.remove(QChar(0x202d)); + return text; } QString QMatrixClient::prettyPrint(const QString& plainText) { auto pt = QStringLiteral("<span style='white-space:pre-wrap'>") + - plainText.toHtmlEscaped() + QStringLiteral("</span>"); + plainText.toHtmlEscaped() + QStringLiteral("</span>"); pt.replace('\n', QStringLiteral("<br/>")); linkifyUrls(pt); return pt; } + +QString QMatrixClient::cacheLocation(const QString& dirName) +{ + const QString cachePath = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + % '/' % dirName % '/'; + QDir dir; + if (!dir.exists(cachePath)) + dir.mkpath(cachePath); + return cachePath; +} + +// Tests for function_traits<> + +#ifdef Q_CC_CLANG +#pragma clang diagnostic push +#pragma ide diagnostic ignored "OCSimplifyInspection" +#endif +using namespace QMatrixClient; + +int f(); +static_assert(std::is_same<fn_return_t<decltype(f)>, int>::value, + "Test fn_return_t<>"); + +void f1(int); +static_assert(function_traits<decltype(f1)>::arg_number == 1, + "Test fn_arg_number"); + +void f2(int, QString); +static_assert(std::is_same<fn_arg_t<decltype(f2), 1>, QString>::value, + "Test fn_arg_t<>"); + +struct S { int mf(); }; +static_assert(is_callable_v<decltype(&S::mf)>, "Test member function"); +static_assert(returns<int, decltype(&S::mf)>(), "Test returns<> with member function"); + +struct Fo { int operator()(); }; +static_assert(is_callable_v<Fo>, "Test is_callable<> with function object"); +static_assert(function_traits<Fo>::arg_number == 0, "Test function object"); +static_assert(std::is_same<fn_return_t<Fo>, int>::value, + "Test return type of function object"); + +struct Fo1 { void operator()(int); }; +static_assert(function_traits<Fo1>::arg_number == 1, "Test function object 1"); +static_assert(is_callable_v<Fo1>, "Test is_callable<> with function object 1"); +static_assert(std::is_same<fn_arg_t<Fo1>, int>(), + "Test fn_arg_t defaulting to first argument"); + +#if (!defined(_MSC_VER) || _MSC_VER >= 1910) +static auto l = [] { return 1; }; +static_assert(is_callable_v<decltype(l)>, "Test is_callable_v<> with lambda"); +static_assert(std::is_same<fn_return_t<decltype(l)>, int>::value, + "Test fn_return_t<> with lambda"); +#endif + +template <typename T> +struct fn_object +{ + static int smf(double) { return 0; } +}; +template <> +struct fn_object<QString> +{ + void operator()(QString); +}; +static_assert(is_callable_v<fn_object<QString>>, "Test function object"); +static_assert(returns<void, fn_object<QString>>(), + "Test returns<> with function object"); +static_assert(!is_callable_v<fn_object<int>>, "Test non-function object"); +// FIXME: These two don't work +//static_assert(is_callable_v<decltype(&fn_object<int>::smf)>, +// "Test static member function"); +//static_assert(returns<int, decltype(&fn_object<int>::smf)>(), +// "Test returns<> with static member function"); + +template <typename T> +QString ft(T&&) { return {}; } +static_assert(std::is_same<fn_arg_t<decltype(ft<QString>)>, QString&&>(), + "Test function templates"); + +#ifdef Q_CC_CLANG +#pragma clang diagnostic pop +#endif @@ -18,8 +18,9 @@ #pragma once -#include <QtCore/QPointer> -#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0)) +#include <QtCore/QLatin1String> + +#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) #include <QtCore/QMetaEnum> #include <QtCore/QDebug> #endif @@ -27,10 +28,12 @@ #include <functional> #include <memory> -#if __cplusplus >= 201703L +#if __has_cpp_attribute(fallthrough) #define FALLTHROUGH [[fallthrough]] #elif __has_cpp_attribute(clang::fallthrough) #define FALLTHROUGH [[clang::fallthrough]] +#elif __has_cpp_attribute(gnu::fallthrough) +#define FALLTHROUGH [[gnu::fallthrough]] #else #define FALLTHROUGH // -fallthrough #endif @@ -49,6 +52,14 @@ template <typename T> static void qAsConst(const T &&) Q_DECL_EQ_DELETE; #endif +// MSVC 2015 and older GCC's don't handle initialisation from initializer lists +// right in the absense of a constructor; MSVC 2015, notably, fails with +// "error C2440: 'return': cannot convert from 'initializer list' to '<type>'" +#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ + (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) +# define BROKEN_INITIALIZER_LISTS +#endif + namespace QMatrixClient { // The below enables pretty-printing of enums in logs @@ -86,74 +97,150 @@ namespace QMatrixClient static_assert(!std::is_reference<T>::value, "You cannot make an Omittable<> with a reference type"); public: + using value_type = std::decay_t<T>; + explicit Omittable() : Omittable(none) { } - Omittable(NoneTag) : _value(std::decay_t<T>()), _omitted(true) { } - Omittable(const std::decay_t<T>& val) : _value(val) { } - Omittable(std::decay_t<T>&& val) : _value(std::move(val)) { } - Omittable<T>& operator=(const std::decay_t<T>& val) + Omittable(NoneTag) : _value(value_type()), _omitted(true) { } + Omittable(const value_type& val) : _value(val) { } + Omittable(value_type&& val) : _value(std::move(val)) { } + Omittable<T>& operator=(const value_type& val) { _value = val; _omitted = false; return *this; } - Omittable<T>& operator=(std::decay_t<T>&& val) + Omittable<T>& operator=(value_type&& val) { + // For some reason GCC complains about -Wmaybe-uninitialized + // in the context of using Omittable<bool> with converters.h; + // though the logic looks very much benign (GCC bug???) _value = std::move(val); _omitted = false; return *this; } + bool operator==(const value_type& rhs) const + { + return !omitted() && value() == rhs; + } + friend bool operator==(const value_type& lhs, + const Omittable<value_type>& rhs) + { + return rhs == lhs; + } + bool operator!=(const value_type& rhs) const + { + return !operator==(rhs); + } + friend bool operator!=(const value_type& lhs, + const Omittable<value_type>& rhs) + { + return !(rhs == lhs); + } + bool omitted() const { return _omitted; } - const std::decay_t<T>& value() const { Q_ASSERT(!_omitted); return _value; } - std::decay_t<T>& value() { Q_ASSERT(!_omitted); return _value; } - std::decay_t<T>&& release() { _omitted = true; return std::move(_value); } + const value_type& value() const + { + Q_ASSERT(!_omitted); + return _value; + } + value_type& editValue() + { + _omitted = false; + return _value; + } + /// Merge the value from another Omittable + /// \return true if \p other is not omitted and the value of + /// the current Omittable was different (or omitted); + /// in other words, if the current Omittable has changed; + /// false otherwise + template <typename T1> + auto merge(const Omittable<T1>& other) + -> std::enable_if_t<std::is_convertible<T1, T>::value, bool> + { + if (other.omitted() || + (!_omitted && _value == other.value())) + return false; + _omitted = false; + _value = other.value(); + return true; + } + value_type&& release() { _omitted = true; return std::move(_value); } - operator bool() const { return !omitted(); } - const std::decay<T>* operator->() const { return &value(); } - std::decay_t<T>* operator->() { return &value(); } - const std::decay_t<T>& operator*() const { return value(); } - std::decay_t<T>& operator*() { return value(); } + const value_type* operator->() const & { return &value(); } + value_type* operator->() & { return &editValue(); } + const value_type& operator*() const & { return value(); } + value_type& operator*() & { return editValue(); } private: T _value; bool _omitted = false; }; + namespace _impl { + template <typename AlwaysVoid, typename> struct fn_traits; + } + /** Determine traits of an arbitrary function/lambda/functor - * This only works with arity of 1 (1-argument) for now but is extendable - * to other cases. Also, doesn't work with generic lambdas and function - * objects that have operator() overloaded + * Doesn't work with generic lambdas and function objects that have + * operator() overloaded. * \sa https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 */ template <typename T> - struct function_traits : public function_traits<decltype(&T::operator())> - { }; // A generic function object that has (non-overloaded) operator() + struct function_traits : public _impl::fn_traits<void, T> {}; // Specialisation for a function - template <typename ReturnT, typename ArgT> - struct function_traits<ReturnT(ArgT)> + template <typename ReturnT, typename... ArgTs> + struct function_traits<ReturnT(ArgTs...)> { + static constexpr auto is_callable = true; using return_type = ReturnT; - using arg_type = ArgT; + using arg_types = std::tuple<ArgTs...>; + using function_type = std::function<ReturnT(ArgTs...)>; + static constexpr auto arg_number = std::tuple_size<arg_types>::value; }; - // Specialisation for a member function - template <typename ReturnT, typename ClassT, typename ArgT> - struct function_traits<ReturnT(ClassT::*)(ArgT)> - : function_traits<ReturnT(ArgT)> - { }; + namespace _impl { + template <typename AlwaysVoid, typename T> + struct fn_traits + { + static constexpr auto is_callable = false; + }; + + template <typename T> + struct fn_traits<decltype(void(&T::operator())), T> + : public fn_traits<void, decltype(&T::operator())> + { }; // A generic function object that has (non-overloaded) operator() - // Specialisation for a const member function - template <typename ReturnT, typename ClassT, typename ArgT> - struct function_traits<ReturnT(ClassT::*)(ArgT) const> - : function_traits<ReturnT(ArgT)> - { }; + // Specialisation for a member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT(ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ArgTs...)> + { }; + + // Specialisation for a const member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT(ClassT::*)(ArgTs...) const> + : function_traits<ReturnT(ArgTs...)> + { }; + } // namespace _impl template <typename FnT> using fn_return_t = typename function_traits<FnT>::return_type; - template <typename FnT> - using fn_arg_t = typename function_traits<FnT>::arg_type; + template <typename FnT, int ArgN = 0> + using fn_arg_t = + std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; + + template <typename R, typename FnT> + constexpr bool returns() + { + return std::is_same<fn_return_t<FnT>, R>::value; + } + + // Poor-man's is_invokable + template <typename T> + constexpr auto is_callable_v = function_traits<T>::is_callable; inline auto operator"" _ls(const char* s, std::size_t size) { @@ -209,36 +296,20 @@ namespace QMatrixClient return std::make_pair(last, sLast); } - /** A guard pointer that disconnects an interested object upon destruction - * It's almost QPointer<> except that you have to initialise it with one - * more additional parameter - a pointer to a QObject that will be - * disconnected from signals of the underlying pointer upon the guard's - * destruction. + /** Sanitize the text before showing in HTML + * This does toHtmlEscaped() and removes Unicode BiDi marks. */ - template <typename T> - class ConnectionsGuard : public QPointer<T> - { - public: - ConnectionsGuard(T* publisher, QObject* subscriber) - : QPointer<T>(publisher), subscriber(subscriber) - { } - ~ConnectionsGuard() - { - if (*this) - (*this)->disconnect(subscriber); - } - ConnectionsGuard(ConnectionsGuard&&) = default; - ConnectionsGuard& operator=(ConnectionsGuard&&) = default; - Q_DISABLE_COPY(ConnectionsGuard) - using QPointer<T>::operator=; + QString sanitized(const QString& plainText); - private: - QObject* subscriber; - }; - - /** Pretty-prints plain text into HTML + /** Pretty-print plain text into HTML * This includes HTML escaping of <,>,",& and URLs linkification. */ QString prettyPrint(const QString& plainText); + + /** Return a path to cache directory after making sure that it exists + * The returned path has a trailing slash, clients don't need to append it. + * \param dir path to cache directory relative to the standard cache path + */ + QString cacheLocation(const QString& dirName); } // namespace QMatrixClient diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index cb90a9fd..be568bd2 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -1,4 +1,4 @@ -QT += network +QT += network multimedia CONFIG += c++14 warn_on rtti_off create_prl object_parallel_to_source win32-msvc* { @@ -17,13 +17,17 @@ HEADERS += \ $$SRCPATH/room.h \ $$SRCPATH/user.h \ $$SRCPATH/avatar.h \ + $$SRCPATH/syncdata.h \ $$SRCPATH/util.h \ + $$SRCPATH/qt_connection_util.h \ $$SRCPATH/events/event.h \ $$SRCPATH/events/roomevent.h \ $$SRCPATH/events/stateevent.h \ $$SRCPATH/events/eventcontent.h \ $$SRCPATH/events/roommessageevent.h \ $$SRCPATH/events/simplestateevents.h \ + $$SRCPATH/events/roomcreateevent.h \ + $$SRCPATH/events/roomtombstoneevent.h \ $$SRCPATH/events/roommemberevent.h \ $$SRCPATH/events/roomavatarevent.h \ $$SRCPATH/events/typingevent.h \ @@ -60,11 +64,14 @@ SOURCES += \ $$SRCPATH/room.cpp \ $$SRCPATH/user.cpp \ $$SRCPATH/avatar.cpp \ + $$SRCPATH/syncdata.cpp \ $$SRCPATH/util.cpp \ $$SRCPATH/events/event.cpp \ $$SRCPATH/events/roomevent.cpp \ $$SRCPATH/events/stateevent.cpp \ $$SRCPATH/events/eventcontent.cpp \ + $$SRCPATH/events/roomcreateevent.cpp \ + $$SRCPATH/events/roomtombstoneevent.cpp \ $$SRCPATH/events/roommessageevent.cpp \ $$SRCPATH/events/roommemberevent.cpp \ $$SRCPATH/events/typingevent.cpp \ |