diff options
author | Alexey Rusakov <Kitsune-Ral@users.sf.net> | 2022-02-13 19:25:24 +0100 |
---|---|---|
committer | Alexey Rusakov <Kitsune-Ral@users.sf.net> | 2022-02-13 19:25:24 +0100 |
commit | 044ae4a029b710571420f830a497647f7a54698a (patch) | |
tree | 14d50ba0eb1a5cfac995f13cd6059aed74e506b2 | |
parent | b0aef4af9cbf00755c7b70c71d77f0bf7ce0d200 (diff) | |
parent | 10ac7c13cdcd62b62af6e89cb726376cfbc53302 (diff) | |
download | libquotient-044ae4a029b710571420f830a497647f7a54698a.tar.gz libquotient-044ae4a029b710571420f830a497647f7a54698a.zip |
Merge branch 'dev'
The result is FTBFS as yet; next commits will fix that, along with a few
other things.
147 files changed, 2043 insertions, 1463 deletions
diff --git a/.clang-format b/.clang-format index 6e13223e..8375204a 100644 --- a/.clang-format +++ b/.clang-format @@ -104,8 +104,8 @@ PenaltyBreakComment: 45 #PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 200 #PenaltyBreakTemplateDeclaration: 10 -PenaltyExcessCharacter: 20 -PenaltyReturnTypeOnItsOwnLine: 60 +PenaltyExcessCharacter: 40 +PenaltyReturnTypeOnItsOwnLine: 150 #PointerAlignment: Left #ReflowComments: true #SortIncludes: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0c391be..1d902bd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,28 +21,36 @@ jobs: matrix: os: [ubuntu-20.04, macos-10.15] compiler: [ GCC, Clang ] - platform: [ '' ] - qt-version: [ '5.12.10' ] - qt-arch: [ '' ] + qt-version: [ '5.12.12' ] # Not using binary values here, to make the job captions more readable e2ee: [ '' ] update-api: [ '', 'update-api' ] + sonar: [ '' ] + platform: [ '' ] + qt-arch: [ '' ] exclude: - os: macos-10.15 compiler: GCC include: + - os: ubuntu-latest + compiler: GCC + qt-version: '5.12.12' + sonar: 'sonar' - os: windows-2019 compiler: MSVC platform: x64 - qt-version: '5.12.10' + qt-version: '5.12.12' qt-arch: win64_msvc2017_64 - os: windows-2019 compiler: MSVC platform: x64 - qt-version: '5.12.10' + qt-version: '5.12.12' qt-arch: win64_msvc2017_64 update-api: update-api + env: + SONAR_SERVER_URL: 'https://sonarcloud.io' + steps: - uses: actions/checkout@v2 with: @@ -71,7 +79,7 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get -qq install ninja-build valgrind - echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=quotest/.valgrind.supp" >>$GITHUB_ENV + echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=$GITHUB_WORKSPACE/quotest/.valgrind.supp" >>$GITHUB_ENV - name: Setup build environment run: | @@ -96,9 +104,31 @@ jobs: VERSION="$(git describe --all --contains)-ci${{ github.run_number }}-$(git rev-parse --short HEAD)" fi echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/${{ matrix.compiler }}" >>$GITHUB_ENV - echo "CMAKE_ARGS=-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=false \ - -DCMAKE_INSTALL_PREFIX=~/.local -DCMAKE_PREFIX_PATH=~/.local" >>$GITHUB_ENV + + # Build libQuotient as a shared library across platforms but also + # check the static configuration somewhere + CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBUILD_SHARED_LIBS=${{ !matrix.sonar && runner.os == 'Linux' }} \ + -DCMAKE_INSTALL_PREFIX=~/.local \ + -DCMAKE_PREFIX_PATH=~/.local \ + -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON" + + if [ -n "${{ matrix.sonar }}" ]; then + mkdir -p $HOME/.sonar + CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CXX_FLAGS=--coverage" + echo "COV=gcov$CXX_VERSION_POSTFIX" >>$GITHUB_ENV + fi + echo "CMAKE_ARGS=$CMAKE_ARGS" >>$GITHUB_ENV + + if [[ '${{ runner.os }}' != 'Windows' ]]; then + BIN_DIR=/bin + echo "LIB_PATH=$HOME/.local/lib" >>$GITHUB_ENV + fi + echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV + echo "~/.local$BIN_DIR" >>$GITHUB_PATH + cmake -E make_directory ${{ runner.workspace }}/build + echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV - name: Setup MSVC environment uses: ilammy/msvc-dev-cmd@v1 @@ -106,6 +136,20 @@ jobs: with: arch: ${{ matrix.platform }} + - name: Download and set up Sonar Cloud tools + if: matrix.sonar != '' + env: + SONAR_SCANNER_VERSION: 4.6.2.2472 + run: | + pushd $HOME/.sonar + curl -sSL --remote-name-all \ + $SONAR_SERVER_URL/static/cpp/build-wrapper-linux-x86.zip \ + https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip + unzip -o build-wrapper*.zip + echo "BUILD_WRAPPER=$HOME/.sonar/build-wrapper-linux-x86/build-wrapper-linux* --out-dir $BUILD_PATH/sonar" >>$GITHUB_ENV + unzip -o sonar-scanner-cli*.zip + popd + - name: Install OpenSSL if: ${{ contains(matrix.os, 'ubuntu') && matrix.e2ee }} run: | @@ -114,11 +158,11 @@ jobs: - name: Build and install olm if: matrix.e2ee + working-directory: ${{ runner.workspace }} run: | - cd .. git clone https://gitlab.matrix.org/matrix-org/olm.git - cmake -S olm -B olm/build $CMAKE_ARGS - cmake --build olm/build --target install + cmake -S olm -B build/olm $CMAKE_ARGS + cmake --build build/olm --target install echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV - name: Build and install QtKeychain @@ -130,14 +174,14 @@ jobs: - name: Pull CS API and build GTAD if: matrix.update-api + working-directory: ${{ runner.workspace }} run: | - cd .. git clone https://github.com/matrix-org/matrix-doc.git git clone --recursive https://github.com/KitsuneRal/gtad.git - cmake -S gtad -B gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF - cmake --build gtad - echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=$GITHUB_WORKSPACE/../matrix-doc \ - -DGTAD_PATH=$GITHUB_WORKSPACE/../gtad/gtad" \ + cmake -S gtad -B build/gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF + cmake --build build/gtad + echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_DOC_PATH=${{ runner.workspace }}/matrix-doc \ + -DGTAD_PATH=${{ runner.workspace }}/build/gtad/gtad" \ >>$GITHUB_ENV echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with API files regeneration" >>$GITHUB_ENV @@ -153,33 +197,51 @@ jobs: - name: Configure libQuotient run: | - if [[ '${{ runner.os }}' == 'Windows' ]]; then - BIN_DIR=. - else - BIN_DIR=bin - fi - echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV - cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} + cmake -S $GITHUB_WORKSPACE -B $BUILD_PATH $CMAKE_ARGS \ + -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} -DQuotient_INSTALL_TESTS=ON - name: Regenerate API code if: matrix.update-api - run: cmake --build build --target update-api + run: cmake --build ../build/libQuotient --target update-api - name: Build and install libQuotient run: | - cmake --build build --target install - ls ~/.local/$BIN_DIR/quotest + $BUILD_WRAPPER cmake --build $BUILD_PATH --target all + cmake --build $BUILD_PATH --target install + ls ~/.local$BIN_DIR/quotest - name: Run tests env: TEST_USER: ${{ secrets.TEST_USER }} TEST_PWD: ${{ secrets.TEST_PWD }} - QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true' + QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true;quotient.events.debug=true' QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' run: | - [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + ctest --test-dir $BUILD_PATH --output-on-failure + [[ -z "$TEST_USER" ]] || \ + LD_LIBRARY_PATH=$LIB_PATH \ + $VALGRIND quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually - + - name: Perform CodeQL analysis if: env.CODEQL_ANALYSIS uses: github/codeql-action/analyze@v1 + + - name: Run sonar-scanner + if: matrix.sonar != '' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + mkdir .coverage && pushd .coverage + find $BUILD_PATH -name '*.gcda' -print0 \ + | xargs -0 $COV -s $GITHUB_WORKSPACE -pr + # Coverage of the test source code is not tracked, as it is always 100% + # (if not, some tests failed and broke the build at an earlier stage) + rm -f quotest* autotests* + popd + $HOME/.sonar/sonar-scanner*/bin/sonar-scanner \ + -Dsonar.host.url="$SONAR_SERVER_URL" \ + -Dsonar.cfamily.build-wrapper-output="$BUILD_PATH/sonar" \ + -Dsonar.cfamily.threads=2 \ + -Dsonar.cfamily.gcov.reportsPath=.coverage @@ -15,4 +15,4 @@ extraction: # - cmake --build build # - popd configure: - command: "cmake . -GNinja" # -DOlm_DIR=olm/build" + command: "CXX=clang++-9 cmake . -GNinja" # -DOlm_DIR=olm/build" diff --git a/CMakeLists.txt b/CMakeLists.txt index 69ac7e20..15726240 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,7 @@ if (MSVC) /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) else() foreach (FLAG Wall Wpedantic Wextra Werror=return-type Wno-unused-parameter - Wno-gnu-zero-variadic-macro-arguments fvisibility-inlines-hidden) + Wno-gnu-zero-variadic-macro-arguments) CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) if ( COMPILER_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-?${FLAG}($| )") @@ -115,66 +115,75 @@ endif() # Set up source files list(APPEND lib_SRCS lib/quotient_common.h - lib/networkaccessmanager.cpp - lib/connectiondata.cpp - lib/connection.cpp - lib/ssosession.cpp - lib/logging.cpp - lib/room.cpp - lib/user.cpp - lib/avatar.cpp - lib/uri.cpp - lib/uriresolver.cpp - lib/eventstats.cpp - lib/syncdata.cpp - lib/settings.cpp - lib/networksettings.cpp - lib/converters.cpp - lib/util.cpp - lib/eventitem.cpp - lib/accountregistry.cpp - lib/mxcreply.cpp - lib/events/event.cpp - 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/roompowerlevelsevent.cpp - lib/events/typingevent.cpp - lib/events/receiptevent.cpp - lib/events/reactionevent.cpp - lib/events/callanswerevent.cpp - lib/events/callcandidatesevent.cpp - lib/events/callhangupevent.cpp - lib/events/callinviteevent.cpp - lib/events/directchatevent.cpp - lib/events/encryptionevent.cpp - lib/events/encryptedevent.cpp - lib/events/roomkeyevent.cpp - lib/events/stickerevent.cpp - lib/events/keyverificationevent.cpp - lib/events/encryptedfile.cpp - lib/jobs/requestdata.cpp - lib/jobs/basejob.cpp - lib/jobs/syncjob.cpp - lib/jobs/mediathumbnailjob.cpp - lib/jobs/downloadfilejob.cpp + lib/quotient_export.h + lib/function_traits.h lib/function_traits.cpp + lib/omittable.h + lib/networkaccessmanager.h lib/networkaccessmanager.cpp + lib/connectiondata.h lib/connectiondata.cpp + lib/connection.h lib/connection.cpp + lib/ssosession.h lib/ssosession.cpp + lib/logging.h lib/logging.cpp + lib/room.h lib/room.cpp + lib/roomstateview.h lib/roomstateview.cpp + lib/user.h lib/user.cpp + lib/avatar.h lib/avatar.cpp + lib/uri.h lib/uri.cpp + lib/uriresolver.h lib/uriresolver.cpp + lib/eventstats.h lib/eventstats.cpp + lib/syncdata.h lib/syncdata.cpp + lib/settings.h lib/settings.cpp + lib/networksettings.h lib/networksettings.cpp + lib/converters.h lib/converters.cpp + lib/util.h lib/util.cpp + lib/eventitem.h lib/eventitem.cpp + lib/accountregistry.h lib/accountregistry.cpp + lib/mxcreply.h lib/mxcreply.cpp + lib/events/event.h lib/events/event.cpp + lib/events/eventloader.h + lib/events/roomevent.h lib/events/roomevent.cpp + lib/events/stateevent.h lib/events/stateevent.cpp + lib/events/simplestateevents.h + lib/events/eventcontent.h lib/events/eventcontent.cpp + lib/events/eventrelation.h lib/events/eventrelation.cpp + lib/events/roomcreateevent.h lib/events/roomcreateevent.cpp + lib/events/roomtombstoneevent.h lib/events/roomtombstoneevent.cpp + lib/events/roommessageevent.h lib/events/roommessageevent.cpp + lib/events/roommemberevent.h lib/events/roommemberevent.cpp + lib/events/roomavatarevent.h + lib/events/roompowerlevelsevent.h lib/events/roompowerlevelsevent.cpp + lib/events/typingevent.h lib/events/typingevent.cpp + lib/events/accountdataevents.h + lib/events/receiptevent.h lib/events/receiptevent.cpp + lib/events/reactionevent.h + lib/events/callinviteevent.h lib/events/callinviteevent.cpp + lib/events/callcandidatesevent.h + lib/events/callanswerevent.h lib/events/callanswerevent.cpp + lib/events/callhangupevent.h + lib/events/directchatevent.h lib/events/directchatevent.cpp + lib/events/encryptionevent.h lib/events/encryptionevent.cpp + lib/events/encryptedevent.h lib/events/encryptedevent.cpp + lib/events/roomkeyevent.h lib/events/roomkeyevent.cpp + lib/events/stickerevent.h lib/events/stickerevent.cpp + lib/events/keyverificationevent.h lib/events/keyverificationevent.cpp + lib/events/encryptedfile.h lib/events/encryptedfile.cpp + lib/jobs/requestdata.h lib/jobs/requestdata.cpp + lib/jobs/basejob.h lib/jobs/basejob.cpp + lib/jobs/syncjob.h lib/jobs/syncjob.cpp + lib/jobs/mediathumbnailjob.h lib/jobs/mediathumbnailjob.cpp + lib/jobs/downloadfilejob.h lib/jobs/downloadfilejob.cpp ) if (${PROJECT_NAME}_ENABLE_E2EE) list(APPEND lib_SRCS - lib/database.cpp - lib/e2ee/qolmaccount.cpp - lib/e2ee/qolmsession.cpp - lib/e2ee/qolminboundsession.cpp - lib/e2ee/qolmoutboundsession.cpp - lib/e2ee/qolmutils.cpp - lib/e2ee/qolmutility.cpp - lib/e2ee/qolmerrors.cpp - lib/e2ee/qolmsession.cpp - lib/e2ee/qolmmessage.cpp + lib/database.h lib/database.cpp + lib/e2ee/qolmaccount.h lib/e2ee/qolmaccount.cpp + lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp + lib/e2ee/qolminboundsession.h lib/e2ee/qolminboundsession.cpp + lib/e2ee/qolmoutboundsession.h lib/e2ee/qolmoutboundsession.cpp + lib/e2ee/qolmutils.h lib/e2ee/qolmutils.cpp + lib/e2ee/qolmutility.h lib/e2ee/qolmutility.cpp + lib/e2ee/qolmerrors.h lib/e2ee/qolmerrors.cpp + lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp + lib/e2ee/qolmmessage.h lib/e2ee/qolmmessage.cpp ) endif() @@ -186,7 +195,6 @@ set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) set(API_GENERATION_ENABLED 0) -set(API_FORMATTING_ENABLED 0) if (GTAD_PATH AND MATRIX_DOC_PATH) # REALPATH resolves ~ (home directory) while PROGRAM doesn't get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) @@ -213,28 +221,17 @@ if (API_GENERATION_ENABLED) set(CLANG_FORMAT clang-format) endif() get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM PROGRAM_ARGS CLANG_FORMAT_ARGS) - if (ABS_CLANG_FORMAT) - set(API_FORMATTING_ENABLED 1) - message( STATUS "clang-format is at ${ABS_CLANG_FORMAT}") - else () - message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") + if (NOT ABS_CLANG_FORMAT) + message( WARNING "${CLANG_FORMAT} is NOT FOUND; API files won't be formatted") endif () - # We use globbing with CONFIGURE_DEPENDS to produce two file lists: - # one of all API files for clang-format and another of just .cpp - # files to supply for library source files. Since we expect these - # file lists to only change due to GTAD invocation, we only use - # CONFIGURE_DEPENDS when pre-requisites to update API are met. - # Read comments next to each file(GLOB_RECURSE) for caveats. - set(add_CONFIGURE_DEPENDS "CONFIGURE_DEPENDS") - set(FULL_CSAPI_SRC_DIR ${ABS_API_DEF_PATH}/client-server) file(GLOB_RECURSE API_DEFS RELATIVE ${PROJECT_SOURCE_DIR} ${FULL_CSAPI_SRC_DIR}/*.yaml ${ABS_API_DEF_PATH}/${ASAPI_DEF_DIR}/*.yaml ${ABS_API_DEF_PATH}/${ISAPI_DEF_DIR}/*.yaml ) - add_custom_target(generate-unformatted-api + add_custom_target(update-api ${ABS_GTAD_PATH} --config ../gtad/gtad.yaml --out ${CSAPI_DIR} ${FULL_CSAPI_SRC_DIR} old_sync.yaml- room_initial_sync.yaml- # deprecated @@ -249,46 +246,28 @@ if (API_GENERATION_ENABLED) ${API_DEFS} VERBATIM ) - add_custom_target(update-api DEPENDS generate-unformatted-api) - if (EXISTS ${ABS_CLANG_FORMAT}) - set(CLANG_FORMAT_ARGS -i -sort-includes ${CLANG_FORMAT_ARGS}) - # FIXME: the list of files should be produced after GTAD has run. - # For now it's produced at CMake invocation. If file() hasn't found - # any files at that moment, clang-format will be called with an empty - # list of files and choke. Taking outfiles.txt from GTAD could be - # an option but this, too, must be done during the build stage, and - # there's no crossplatform way to use the contents of a file as - # input for a build target command. - file(GLOB_RECURSE api_ALL_SRCS ${add_CONFIGURE_DEPENDS} - ${FULL_CSAPI_DIR}/*.* - lib/${ASAPI_DEF_DIR}/*.* - lib/${ISAPI_DEF_DIR}/*.*) - if (api_ALL_SRCS) - add_custom_target(format-api - ${ABS_CLANG_FORMAT} ${CLANG_FORMAT_ARGS} ${api_ALL_SRCS} - DEPENDS generate-unformatted-api - VERBATIM) - add_dependencies(update-api format-api) - endif() - endif() endif() add_feature_info(EnableApiCodeGeneration "${API_GENERATION_ENABLED}" "build target update-api") -if (API_GENERATION_ENABLED) - add_feature_info(EnableApiFormatting "${API_FORMATTING_ENABLED}" - "formatting of generated API files with clang-format") -endif() - -# Make no mistake: CMake cannot run gtad first and then populate the list of -# resulting api_SRCS files. In other words, placing the below statement after -# the block above will not lead to CMake magically reconfiguring the build -# after building the update-api target. CONFIGURE_DEPENDS somewhat helps, -# at least forcing the build system to reevaluate the glob before building -# the next target. Otherwise, you have to watch out if gtad has created -# new files and if it has, re-run cmake. -file(GLOB_RECURSE api_SRCS ${add_CONFIGURE_DEPENDS} ${FULL_CSAPI_DIR}/*.cpp) -add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS}) +# Produce the list of all Matrix API files for building the library. When this +# list changes (normally after calling GTAD), CONFIGURE_DEPENDS will force +# the build system to call CMake again. Checking for the glob change slows down +# each build (even if the target does not involve API generation). It would be +# ideal if GTAD could compare the initial (saved somewhere) and the generated +# file list itself and write down to some .cmake file if those are different, +# which would trigger the reconfiguration specifically before the next build. +# For now CONFIGURE_DEPENDS is the best approximation of that. +file(GLOB_RECURSE api_ALL_SRCS CONFIGURE_DEPENDS + ${FULL_CSAPI_DIR}/*.* lib/${ASAPI_DEF_DIR}/*.* lib/${ISAPI_DEF_DIR}/*.*) + +add_library(${PROJECT_NAME} ${lib_SRCS} ${api_ALL_SRCS}) +# Set BUILDING_SHARED_QUOTIENT if building as a shared library +target_compile_definitions(${PROJECT_NAME} PRIVATE + $<$<STREQUAL:$<TARGET_PROPERTY:${PROJECT_NAME},TYPE>,SHARED_LIBRARY>:BUILDING_SHARED_QUOTIENT>) +# Set QUOTIENT_STATIC in a static library setting +target_compile_definitions(${PROJECT_NAME} PUBLIC + $<$<STREQUAL:$<TARGET_PROPERTY:${PROJECT_NAME},TYPE>,STATIC_LIBRARY>:QUOTIENT_STATIC>) target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_TO_ASCII) target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} @@ -300,6 +279,8 @@ endif() set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20 CXX_EXTENSIONS OFF + VISIBILITY_INLINES_HIDDEN ON + CXX_VISIBILITY_PRESET hidden VERSION "${PROJECT_VERSION}" SOVERSION ${API_VERSION} INTERFACE_${PROJECT_NAME}_MAJOR_VERSION ${API_VERSION} @@ -351,8 +332,8 @@ endif() # Configure installation install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets + LIBRARY RUNTIME ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} INCLUDES DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} ) install(DIRECTORY lib/ DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 0354172b..671d6c08 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -12,6 +12,7 @@ function(QUOTIENT_ADD_TEST) endfunction() quotient_add_test(NAME callcandidateseventtest) +quotient_add_test(NAME utiltests) if(${PROJECT_NAME}_ENABLE_E2EE) quotient_add_test(NAME testolmaccount) quotient_add_test(NAME testgroupsession) diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp index 1a69cb66..0d5a543b 100644 --- a/autotests/callcandidateseventtest.cpp +++ b/autotests/callcandidateseventtest.cpp @@ -53,5 +53,5 @@ void TestCallCandidatesEvent::fromJson() QStringLiteral("candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0")); } -QTEST_MAIN(TestCallCandidatesEvent) +QTEST_APPLESS_MAIN(TestCallCandidatesEvent) #include "callcandidateseventtest.moc" diff --git a/autotests/utiltests.cpp b/autotests/utiltests.cpp new file mode 100644 index 00000000..e3ec63d0 --- /dev/null +++ b/autotests/utiltests.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "omittable.h" + +#include <QtTest/QtTest> + +// compile-time Omittable<> tests +using namespace Quotient; + +Omittable<int> testFn(bool) { return 0; } +bool testFn2(int) { return false; } +static_assert( + std::is_same_v<decltype(std::declval<Omittable<bool>>().then(testFn)), + Omittable<int>>); +static_assert( + std::is_same_v< + decltype(std::declval<Omittable<bool>>().then_or(testFn, 0)), int>); +static_assert( + std::is_same_v<decltype(std::declval<Omittable<bool>>().then(testFn)), + Omittable<int>>); +static_assert(std::is_same_v<decltype(std::declval<Omittable<int>>() + .then(testFn2) + .then(testFn)), + Omittable<int>>); +static_assert(std::is_same_v<decltype(std::declval<Omittable<bool>>() + .then(testFn) + .then_or(testFn2, false)), + bool>); + +constexpr auto visitTestFn(int, bool) { return false; } +static_assert( + std::is_same_v<Omittable<bool>, decltype(lift(testFn2, Omittable<int>()))>); +static_assert(std::is_same_v<Omittable<bool>, + decltype(lift(visitTestFn, Omittable<int>(), + Omittable<bool>()))>); + +class TestUtils : public QObject { + Q_OBJECT +private Q_SLOTS: + // TODO +}; + +QTEST_APPLESS_MAIN(TestUtils) +#include "utiltests.moc" diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml index 943ac013..03c23886 100644 --- a/gtad/gtad.yaml +++ b/gtad/gtad.yaml @@ -45,7 +45,7 @@ analyzer: types: - +set: &UseOmittable useOmittable: - omittedValue: 'none' # Quotient::none in lib/util.h + omittedValue: 'none' # Quotient::none in lib/omittable.h +on: - integer: - int64: qint64 diff --git a/gtad/operation.h.mustache b/gtad/operation.h.mustache index f91dc66c..063f0bbd 100644 --- a/gtad/operation.h.mustache +++ b/gtad/operation.h.mustache @@ -16,7 +16,7 @@ namespace Quotient { /*!{{>docCommentSummary}}{{#description}} * {{_}}{{/description}} */ -class {{camelCaseOperationId}}Job : public BaseJob { +class QUOTIENT_API {{camelCaseOperationId}}Job : public BaseJob { public: {{#models}} // Inner data structures diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index a292ed45..616b54b4 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -8,90 +8,59 @@ using namespace Quotient; -void AccountRegistry::add(Connection* c) +void AccountRegistry::add(Connection* a) { - if (m_accounts.contains(c)) + if (contains(a)) return; - beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size()); - m_accounts += c; + beginInsertRows(QModelIndex(), size(), size()); + push_back(a); endInsertRows(); } -void AccountRegistry::drop(Connection* c) +void AccountRegistry::drop(Connection* a) { - beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c)); - m_accounts.removeOne(c); + const auto idx = indexOf(a); + beginRemoveRows(QModelIndex(), idx, idx); + remove(idx); endRemoveRows(); - Q_ASSERT(!m_accounts.contains(c)); + Q_ASSERT(!contains(a)); } bool AccountRegistry::isLoggedIn(const QString &userId) const { - return std::any_of(m_accounts.cbegin(), m_accounts.cend(), - [&userId](Connection* a) { return a->userId() == userId; }); + return std::any_of(cbegin(), cend(), [&userId](const Connection* a) { + return a->userId() == userId; + }); } -bool AccountRegistry::contains(Connection *c) const +QVariant AccountRegistry::data(const QModelIndex& index, int role) const { - return m_accounts.contains(c); -} - -AccountRegistry::AccountRegistry() = default; - -QVariant AccountRegistry::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) { - return {}; - } - - if (index.row() >= m_accounts.count()) { + if (!index.isValid() || index.row() >= count()) return {}; - } - auto account = m_accounts[index.row()]; - - if (role == ConnectionRole) { - return QVariant::fromValue(account); - } + if (role == AccountRole) + return QVariant::fromValue(at(index.row())); return {}; } -int AccountRegistry::rowCount(const QModelIndex &parent) const +int AccountRegistry::rowCount(const QModelIndex& parent) const { - if (parent.isValid()) { - return 0; - } - - return m_accounts.count(); + return parent.isValid() ? 0 : count(); } QHash<int, QByteArray> AccountRegistry::roleNames() const { - return {{ConnectionRole, "connection"}}; + return { { AccountRole, "connection" } }; } -bool AccountRegistry::isEmpty() const -{ - return m_accounts.isEmpty(); -} -int AccountRegistry::count() const -{ - return m_accounts.count(); -} - -const QVector<Connection*> AccountRegistry::accounts() const -{ - return m_accounts; -} Connection* AccountRegistry::get(const QString& userId) { - for (const auto &connection : m_accounts) { - if(connection->userId() == userId) { + for (const auto &connection : *this) { + if (connection->userId() == userId) return connection; - } } return nullptr; } diff --git a/lib/accountregistry.h b/lib/accountregistry.h index 5efda459..2f6dffdf 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -4,42 +4,57 @@ #pragma once -#include <QtCore/QObject> -#include <QtCore/QList> +#include "quotient_export.h" + #include <QtCore/QAbstractListModel> namespace Quotient { class Connection; -class AccountRegistry : public QAbstractListModel { +class QUOTIENT_API AccountRegistry : public QAbstractListModel, + private QVector<Connection*> { Q_OBJECT public: + using const_iterator = QVector::const_iterator; + using const_reference = QVector::const_reference; + enum EventRoles { - ConnectionRole = Qt::UserRole + 1, + AccountRole = Qt::UserRole + 1, + ConnectionRole = AccountRole }; - static AccountRegistry &instance() { - static AccountRegistry _instance; - return _instance; - } + [[deprecated("Use Accounts variable instead")]] // + static AccountRegistry& instance(); + + // Expose most of QVector's const-API but only provide add() and drop() + // for changing it. In theory other changing operations could be supported + // too; but then boilerplate begin/end*() calls has to be tucked into each + // and this class gives no guarantees on the order of entries, so why care. - const QVector<Connection*> accounts() const; + const QVector<Connection*>& accounts() const { return *this; } void add(Connection* a); void drop(Connection* a); + const_iterator begin() const { return QVector::begin(); } + const_iterator end() const { return QVector::end(); } + const_reference front() const { return QVector::front(); } + const_reference back() const { return QVector::back(); } bool isLoggedIn(const QString& userId) const; - bool isEmpty() const; - int count() const; - bool contains(Connection*) const; Connection* get(const QString& userId); - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + using QVector::isEmpty, QVector::empty; + using QVector::size, QVector::count, QVector::capacity; + using QVector::cbegin, QVector::cend, QVector::contains; + // QAbstractItemModel interface implementation + + [[nodiscard]] QVariant data(const QModelIndex& index, + int role) const override; + [[nodiscard]] int rowCount( + const QModelIndex& parent = QModelIndex()) const override; [[nodiscard]] QHash<int, QByteArray> roleNames() const override; +}; -private: - AccountRegistry(); +inline QUOTIENT_API AccountRegistry Accounts {}; - QVector<Connection *> m_accounts; -}; +inline AccountRegistry& AccountRegistry::instance() { return Accounts; } } diff --git a/lib/avatar.cpp b/lib/avatar.cpp index 77648562..9304a3de 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -47,15 +47,11 @@ public: mutable std::vector<get_callback_t> callbacks; }; -Avatar::Avatar() : d(std::make_unique<Private>()) {} +Avatar::Avatar() + : d(makeImpl<Private>()) +{} -Avatar::Avatar(QUrl url) : d(std::make_unique<Private>(std::move(url))) {} - -Avatar::Avatar(Avatar&&) = default; - -Avatar::~Avatar() = default; - -Avatar& Avatar::operator=(Avatar&&) = default; +Avatar::Avatar(QUrl url) : d(makeImpl<Private>(std::move(url))) {} QImage Avatar::get(Connection* connection, int dimension, get_callback_t callback) const diff --git a/lib/avatar.h b/lib/avatar.h index d4634aea..c94dc369 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -3,22 +3,20 @@ #pragma once +#include "util.h" + #include <QtCore/QUrl> #include <QtGui/QIcon> #include <functional> -#include <memory> namespace Quotient { class Connection; -class Avatar { +class QUOTIENT_API Avatar { public: explicit Avatar(); explicit Avatar(QUrl url); - Avatar(Avatar&&); - ~Avatar(); - Avatar& operator=(Avatar&&); using get_callback_t = std::function<void()>; using upload_callback_t = std::function<void(QUrl)>; @@ -39,6 +37,6 @@ public: private: class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } // namespace Quotient diff --git a/lib/connection.cpp b/lib/connection.cpp index a8de4030..14188ace 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -348,7 +348,8 @@ public: }; Connection::Connection(const QUrl& server, QObject* parent) - : QObject(parent), d(new Private(std::make_unique<ConnectionData>(server))) + : QObject(parent) + , d(makeImpl<Private>(std::make_unique<ConnectionData>(server))) { #ifdef Quotient_E2EE_ENABLED //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount); @@ -362,7 +363,7 @@ Connection::~Connection() { qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); - AccountRegistry::instance().drop(this); + Accounts.drop(this); } void Connection::resolveServer(const QString& mxid) @@ -541,7 +542,7 @@ void Connection::Private::completeSetup(const QString& mxId) qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() << "by user" << data->userId() << "from device" << data->deviceId(); - AccountRegistry::instance().add(q); + Accounts.add(q); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED @@ -825,7 +826,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents) // After running this loop, the account data events not saved in // accountData (see the end of the loop body) are auto-cleaned away for (auto&& eventPtr: accountDataEvents) { - visit(*eventPtr, + switchOnType(*eventPtr, [this](const DirectChatEvent& dce) { // https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events const auto& usersToDCs = dce.usersToDirectChats(); @@ -925,7 +926,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) return; } - visit(*decryptedEvent, + switchOnType(*decryptedEvent, [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); @@ -1713,8 +1714,8 @@ room_factory_t Connection::roomFactory() { return _roomFactory; } user_factory_t Connection::userFactory() { return _userFactory; } -room_factory_t Connection::_roomFactory = defaultRoomFactory<>(); -user_factory_t Connection::_userFactory = defaultUserFactory<>(); +room_factory_t Connection::_roomFactory = defaultRoomFactory<>; +user_factory_t Connection::_userFactory = defaultUserFactory<>; QByteArray Connection::generateTxnId() const { diff --git a/lib/connection.h b/lib/connection.h index 28ea6ff3..165d8d68 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -8,6 +8,7 @@ #include "ssosession.h" #include "qt_connection_util.h" #include "quotient_common.h" +#include "util.h" #include "csapi/login.h" #include "csapi/create_room.h" @@ -48,6 +49,7 @@ class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; class Database; +struct EncryptedFile; class QOlmAccount; class QOlmInboundGroupSession; @@ -55,11 +57,11 @@ class QOlmInboundGroupSession; using LoginFlow = GetLoginFlowsJob::LoginFlow; /// Predefined login flows -struct LoginFlows { - static inline const LoginFlow Password { "m.login.password" }; - static inline const LoginFlow SSO { "m.login.sso" }; - static inline const LoginFlow Token { "m.login.token" }; -}; +namespace LoginFlows { + inline const LoginFlow Password { "m.login.password" }; + inline const LoginFlow SSO { "m.login.sso" }; + inline const LoginFlow Token { "m.login.token" }; +} // To simplify comparisons of LoginFlows @@ -85,11 +87,9 @@ using user_factory_t = std::function<User*(Connection*, const QString&)>; * \sa Connection::setRoomFactory, Connection::setRoomType */ template <typename T = Room> -static inline room_factory_t defaultRoomFactory() +auto defaultRoomFactory(Connection* c, const QString& id, JoinState js) { - return [](Connection* c, const QString& id, JoinState js) { - return new T(c, id, js); - }; + return new T(c, id, js); } /** The default factory to create user objects @@ -98,9 +98,9 @@ static inline room_factory_t defaultRoomFactory() * \sa Connection::setUserFactory, Connection::setUserType */ template <typename T = User> -static inline user_factory_t defaultUserFactory() +auto defaultUserFactory(Connection* c, const QString& id) { - return [](Connection* c, const QString& id) { return new T(id, c); }; + return new T(id, c); } // Room ids, rather than room pointers, are used in the direct chat @@ -111,7 +111,7 @@ using DirectChatsMap = QMultiHash<const User*, QString>; using DirectChatUsersMap = QMultiHash<QString, User*>; using IgnoredUsersList = IgnoredUsersEvent::content_type; -class Connection : public QObject { +class QUOTIENT_API Connection : public QObject { Q_OBJECT Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) @@ -128,6 +128,7 @@ class Connection : public QObject { Q_PROPERTY(bool supportsPasswordAuth READ supportsPasswordAuth NOTIFY loginFlowsChanged STORED false) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) + Q_PROPERTY(bool canChangePassword READ canChangePassword NOTIFY capabilitiesLoaded) public: using UsersToDevicesToEvents = @@ -451,6 +452,17 @@ public: std::forward<JobArgTs>(jobArgs)...); } + //! \brief Start a local HTTP server and generate a single sign-on URL + //! + //! This call does the preparatory steps to carry out single sign-on + //! sequence + //! \sa https://matrix.org/docs/guides/sso-for-client-developers + //! \return A proxy object holding two URLs: one for SSO on the chosen + //! homeserver and another for the local callback address. Normally + //! you won't need the callback URL unless you proxy the response + //! with a custom UI. You do not need to delete the SsoSession + //! object; the Connection that issued it will dispose of it once + //! the login sequence completes (with any outcome). Q_INVOKABLE SsoSession* prepareForSso(const QString& initialDeviceName, const QString& deviceId = {}); @@ -475,14 +487,14 @@ public: template <typename T> static void setRoomType() { - setRoomFactory(defaultRoomFactory<T>()); + setRoomFactory(defaultRoomFactory<T>); } /// Set the user factory to default with the overriden user type template <typename T> static void setUserType() { - setUserFactory(defaultUserFactory<T>()); + setUserFactory(defaultUserFactory<T>); } /// Saves the olm account data to disk. Usually doesn't need to be called manually. @@ -864,7 +876,7 @@ protected Q_SLOTS: private: class Private; - QScopedPointer<Private> d; + ImplPtr<Private> d; static room_factory_t _roomFactory; static user_factory_t _userFactory; diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index 87ad4577..aca218be 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -41,7 +41,7 @@ public: }; ConnectionData::ConnectionData(QUrl baseUrl) - : d(std::make_unique<Private>(std::move(baseUrl))) + : d(makeImpl<Private>(std::move(baseUrl))) { // Each lambda invocation below takes no more than one job from the // queues (first foreground, then background) and resumes it; then diff --git a/lib/connectiondata.h b/lib/connectiondata.h index e16a2dac..75fc332f 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -4,9 +4,10 @@ #pragma once +#include "util.h" + #include <QtCore/QUrl> -#include <memory> #include <chrono> class QNetworkAccessManager; @@ -42,6 +43,6 @@ public: private: class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } // namespace Quotient diff --git a/lib/converters.h b/lib/converters.h index 90349019..515c96fd 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -3,6 +3,7 @@ #pragma once +#include "omittable.h" #include "util.h" #include <QtCore/QDate> @@ -13,6 +14,7 @@ #include <QtCore/QUrlQuery> #include <QtCore/QVector> +#include <type_traits> #include <vector> class QVariant; @@ -20,15 +22,34 @@ class QVariant; namespace Quotient { template <typename T> struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); } - static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } + // To be implemented in specialisations + static void dumpTo(QJsonObject&, const T&) = delete; + static void fillFrom(const QJsonObject&, T&) = delete; }; +namespace _impl { + template <typename T, typename = void> + struct JsonExporter { + static QJsonObject dump(const T& data) + { + QJsonObject jo; + JsonObjectConverter<T>::dumpTo(jo, data); + return jo; + } + }; + + template <typename T> + struct JsonExporter< + T, std::enable_if_t<std::is_invocable_v<decltype(&T::toJson), T>>> { + static auto dump(const T& data) { return data.toJson(); } + }; +} + //! \brief The switchboard for extra conversion algorithms behind from/toJson //! //! This template is mainly intended for partial conversion specialisations -//! since from/toJson are functions and cannot be partially specialised; -//! another case for JsonConverter is to insulate types that can be constructed +//! since from/toJson are functions and cannot be partially specialised. +//! Another case for JsonConverter is to insulate types that can be constructed //! from basic types - namely, QVariant and QUrl can be directly constructed //! from QString and having an overload or specialisation for those leads to //! ambiguity between these and QJsonValue. For trivial (converting @@ -40,18 +61,25 @@ struct JsonObjectConverter { //! that they are not supported and it's not feasible to support those by means //! of overloading toJson() and specialising fromJson(). template <typename T> -struct JsonConverter { - static QJsonObject dump(const T& pod) - { - QJsonObject jo; - JsonObjectConverter<T>::dumpTo(jo, pod); - return jo; - } +struct JsonConverter : _impl::JsonExporter<T> { + // Unfortunately, if constexpr doesn't work with dump() and T::toJson + // because trying to check invocability of T::toJson hits a hard + // (non-SFINAE) compilation error if the member is not there. Hence a bit + // more verbose SFINAE construct in _impl::JsonExporter. + static T doLoad(const QJsonObject& jo) { - T pod; - JsonObjectConverter<T>::fillFrom(jo, pod); - return pod; + // 'else' below are required to suppress code generation for unused + // branches - 'return' is not enough + if constexpr (std::is_same_v<T, QJsonObject>) + return jo; + else if constexpr (std::is_constructible_v<T, QJsonObject>) + return T(jo); + else { + 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()); } @@ -183,7 +211,7 @@ struct JsonConverter<QUrl> { }; template <> -struct JsonConverter<QVariant> { +struct QUOTIENT_API JsonConverter<QVariant> { static QJsonValue dump(const QVariant& v); static QVariant load(const QJsonValue& jv); }; @@ -281,12 +309,14 @@ template <typename T> struct JsonObjectConverter<QHash<QString, T>> : public HashMapFromJson<QHash<QString, T>> {}; -QJsonObject toJson(const QVariantHash& vh); +QJsonObject QUOTIENT_API toJson(const QVariantHash& vh); template <> -QVariantHash fromJson(const QJsonValue& jv); +QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv); // Conditional insertion into a QJsonObject +constexpr bool IfNotEmpty = false; + namespace _impl { template <typename ValT> inline void addTo(QJsonObject& o, const QString& k, ValT&& v) @@ -332,7 +362,7 @@ namespace _impl { // This one is for types that have isEmpty() when Force is false template <typename ValT> - struct AddNode<ValT, false, decltype(std::declval<ValT>().isEmpty())> { + struct AddNode<ValT, IfNotEmpty, decltype(std::declval<ValT>().isEmpty())> { template <typename ContT, typename ForwardedT> static void impl(ContT& container, const QString& key, ForwardedT&& value) @@ -342,9 +372,9 @@ namespace _impl { } }; - // This one unfolds Omittable<> (also only when Force is false) + // This one unfolds Omittable<> (also only when IfNotEmpty is requested) template <typename ValT> - struct AddNode<Omittable<ValT>, false> { + struct AddNode<Omittable<ValT>, IfNotEmpty> { template <typename ContT, typename OmittableT> static void impl(ContT& container, const QString& key, const OmittableT& value) @@ -355,8 +385,6 @@ namespace _impl { }; } // namespace _impl -static constexpr bool IfNotEmpty = false; - /*! Add a key-value pair to QJsonObject or QUrlQuery * * Adds a key-value pair(s) specified by \p key and \p value to diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index 0c760e80..5140d340 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -14,7 +14,7 @@ namespace Quotient { * that set the account_data. The config will be synced to clients in the * top-level `account_data`. */ -class SetAccountDataJob : public BaseJob { +class QUOTIENT_API SetAccountDataJob : public BaseJob { public: /*! \brief Set some account_data for the user. * @@ -38,7 +38,7 @@ public: * Get some account_data for the client. This config is only visible to the user * that set the account_data. */ -class GetAccountDataJob : public BaseJob { +class QUOTIENT_API GetAccountDataJob : public BaseJob { public: /*! \brief Get some account_data for the user. * @@ -67,7 +67,7 @@ public: * visible to the user that set the account_data. The config will be synced to * clients in the per-room `account_data`. */ -class SetAccountDataPerRoomJob : public BaseJob { +class QUOTIENT_API SetAccountDataPerRoomJob : public BaseJob { public: /*! \brief Set some account_data for the user. * @@ -95,7 +95,7 @@ public: * 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 { +class QUOTIENT_API GetAccountDataPerRoomJob : public BaseJob { public: /*! \brief Get some account_data for the user. * diff --git a/lib/csapi/admin.h b/lib/csapi/admin.h index 570bf24a..c53ddd7e 100644 --- a/lib/csapi/admin.h +++ b/lib/csapi/admin.h @@ -16,7 +16,7 @@ namespace Quotient { * up, or by a server admin. Server-local administrator privileges are not * specified in this document. */ -class GetWhoIsJob : public BaseJob { +class QUOTIENT_API GetWhoIsJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index e436971d..e636b12a 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -24,7 +24,7 @@ namespace Quotient { * Identifiers in this list may be used by the homeserver as, for example, * identifiers that it will accept to reset the user's account password. */ -class GetAccount3PIDsJob : public BaseJob { +class QUOTIENT_API GetAccount3PIDsJob : public BaseJob { public: // Inner data structures @@ -102,7 +102,7 @@ struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> { * This results in this endpoint being an equivalent to `/3pid/bind` rather * than dual-purpose. */ -class Post3PIDsJob : public BaseJob { +class QUOTIENT_API Post3PIDsJob : public BaseJob { public: // Inner data structures @@ -154,7 +154,7 @@ struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> { * Homeservers should prevent the caller from adding a 3PID to their account if * it has already been added to another user's account on the homeserver. */ -class Add3PIDJob : public BaseJob { +class QUOTIENT_API Add3PIDJob : public BaseJob { public: /*! \brief Adds contact information to the user's account. * @@ -182,7 +182,7 @@ public: * * Homeservers should track successful binds so they can be unbound later. */ -class Bind3PIDJob : public BaseJob { +class QUOTIENT_API Bind3PIDJob : public BaseJob { public: /*! \brief Binds a 3PID to the user's account through an Identity Service. * @@ -211,7 +211,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class Delete3pidFromAccountJob : public BaseJob { +class QUOTIENT_API Delete3pidFromAccountJob : public BaseJob { public: /*! \brief Deletes a third party identifier from the user's account * @@ -254,7 +254,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class Unbind3pidFromAccountJob : public BaseJob { +class QUOTIENT_API Unbind3pidFromAccountJob : public BaseJob { public: /*! \brief Removes a user's third party identifier from an identity server. * @@ -300,7 +300,7 @@ public: * the email itself, either by sending a validation email itself or by using * a service it has control over. */ -class RequestTokenTo3PIDEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenTo3PIDEmailJob : public BaseJob { public: /*! \brief Begins the validation process for an email address for * association with the user's account. @@ -342,7 +342,7 @@ public: * the phone number itself, either by sending a validation message itself or by * using a service it has control over. */ -class RequestTokenTo3PIDMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenTo3PIDMSISDNJob : public BaseJob { public: /*! \brief Begins the validation process for a phone number for association * with the user's account. diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h index 56a69592..6b2801ca 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -21,7 +21,8 @@ namespace Quotient { * instead of a typical client's access_token. This API cannot be invoked by * users who are not identified as application services. */ -class UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob { +class QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob + : public BaseJob { public: /*! \brief Updates a room's visibility in the application service's room * directory. diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h index 7a9697d3..e4c60ce3 100644 --- a/lib/csapi/banning.h +++ b/lib/csapi/banning.h @@ -18,7 +18,7 @@ namespace Quotient { * The caller must have the required power level in order to perform this * operation. */ -class BanJob : public BaseJob { +class QUOTIENT_API BanJob : public BaseJob { public: /*! \brief Ban a user in the room. * @@ -46,7 +46,7 @@ public: * The caller must have the required power level in order to perform this * operation. */ -class UnbanJob : public BaseJob { +class QUOTIENT_API UnbanJob : public BaseJob { public: /*! \brief Unban a user from the room. * diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h index da50c8c1..81b47cd4 100644 --- a/lib/csapi/capabilities.h +++ b/lib/csapi/capabilities.h @@ -13,7 +13,7 @@ namespace Quotient { * Gets information about the server's supported feature set * and other relevant capabilities. */ -class GetCapabilitiesJob : public BaseJob { +class QUOTIENT_API GetCapabilitiesJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index 28409f5c..511db985 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -14,7 +14,7 @@ namespace Quotient { /*! \brief Upload some content to the content repository. * */ -class UploadContentJob : public BaseJob { +class QUOTIENT_API UploadContentJob : public BaseJob { public: /*! \brief Upload some content to the content repository. * @@ -40,7 +40,7 @@ public: /*! \brief Download content from the content repository. * */ -class GetContentJob : public BaseJob { +class QUOTIENT_API GetContentJob : public BaseJob { public: /*! \brief Download content from the content repository. * @@ -87,7 +87,7 @@ public: * the previous endpoint) but replace the target file name with the one * provided by the caller. */ -class GetContentOverrideNameJob : public BaseJob { +class QUOTIENT_API GetContentOverrideNameJob : public BaseJob { public: /*! \brief Download content from the content repository overriding the file * name @@ -142,7 +142,7 @@ public: * See the [Thumbnails](/client-server-api/#thumbnails) section for more * information. */ -class GetContentThumbnailJob : public BaseJob { +class QUOTIENT_API GetContentThumbnailJob : public BaseJob { public: /*! \brief Download a thumbnail of content from the content repository * @@ -204,7 +204,7 @@ public: * do not want to share with the homeserver, and this can mean that the URLs * being shared should also not be shared with the homeserver. */ -class GetUrlPreviewJob : public BaseJob { +class QUOTIENT_API GetUrlPreviewJob : public BaseJob { public: /*! \brief Get information about a URL for a client * @@ -252,7 +252,7 @@ public: * content repository APIs, for example, proxies may enforce a lower upload size * limit than is advertised by the server on this endpoint. */ -class GetConfigJob : public BaseJob { +class QUOTIENT_API GetConfigJob : public BaseJob { public: /// Get the configuration for the content repository. explicit GetConfigJob(); diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index 81dfbffc..7d566057 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -53,7 +53,7 @@ namespace Quotient { * requesting user as the creator, alongside other keys provided in the * `creation_content`. */ -class CreateRoomJob : public BaseJob { +class QUOTIENT_API CreateRoomJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h index 2ab65e06..617b61d1 100644 --- a/lib/csapi/cross_signing.h +++ b/lib/csapi/cross_signing.h @@ -17,7 +17,7 @@ namespace Quotient { * This API endpoint uses the [User-Interactive Authentication * API](/client-server-api/#user-interactive-authentication-api). */ -class UploadCrossSigningKeysJob : public BaseJob { +class QUOTIENT_API UploadCrossSigningKeysJob : public BaseJob { public: /*! \brief Upload cross-signing keys. * @@ -47,7 +47,7 @@ public: * Publishes cross-signing signatures for the user. The request body is a * map from user ID to key ID to signed JSON object. */ -class UploadCrossSigningSignaturesJob : public BaseJob { +class QUOTIENT_API UploadCrossSigningSignaturesJob : public BaseJob { public: /*! \brief Upload cross-signing signatures. * diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index 7fb69873..430d2132 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -15,7 +15,7 @@ namespace Quotient { * * Gets information about all devices for the current user. */ -class GetDevicesJob : public BaseJob { +class QUOTIENT_API GetDevicesJob : public BaseJob { public: /// List registered devices for the current user explicit GetDevicesJob(); @@ -40,7 +40,7 @@ public: * * Gets information on a single device, by device id. */ -class GetDeviceJob : public BaseJob { +class QUOTIENT_API GetDeviceJob : public BaseJob { public: /*! \brief Get a single device * @@ -66,7 +66,7 @@ public: * * Updates the metadata on the given device. */ -class UpdateDeviceJob : public BaseJob { +class QUOTIENT_API UpdateDeviceJob : public BaseJob { public: /*! \brief Update a device * @@ -88,7 +88,7 @@ public: * * Deletes the given device, and invalidates any access token associated with it. */ -class DeleteDeviceJob : public BaseJob { +class QUOTIENT_API DeleteDeviceJob : public BaseJob { public: /*! \brief Delete a device * @@ -111,7 +111,7 @@ public: * Deletes the given devices, and invalidates any access token associated with * them. */ -class DeleteDevicesJob : public BaseJob { +class QUOTIENT_API DeleteDevicesJob : public BaseJob { public: /*! \brief Bulk deletion of devices * diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h index 93a31595..0bd13a76 100644 --- a/lib/csapi/directory.h +++ b/lib/csapi/directory.h @@ -11,7 +11,7 @@ namespace Quotient { /*! \brief Create a new mapping from room alias to room ID. * */ -class SetRoomAliasJob : public BaseJob { +class QUOTIENT_API SetRoomAliasJob : public BaseJob { public: /*! \brief Create a new mapping from room alias to room ID. * @@ -32,7 +32,7 @@ public: * domain part of the alias does not correspond to the server's own * domain. */ -class GetRoomIdByAliasJob : public BaseJob { +class QUOTIENT_API GetRoomIdByAliasJob : public BaseJob { public: /*! \brief Get the room ID corresponding to this room alias. * @@ -76,7 +76,7 @@ public: * return a successful response even if the user does not have permission to * update the `m.room.canonical_alias` event. */ -class DeleteRoomAliasJob : public BaseJob { +class QUOTIENT_API DeleteRoomAliasJob : public BaseJob { public: /*! \brief Remove a mapping of room alias to room ID. * @@ -112,7 +112,7 @@ public: * as they are not curated, unlike those listed in the `m.room.canonical_alias` * state event. */ -class GetLocalAliasesJob : public BaseJob { +class QUOTIENT_API GetLocalAliasesJob : public BaseJob { public: /*! \brief Get a list of local aliases on a given room. * diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h index 4e50edf3..662b976b 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -19,7 +19,7 @@ namespace Quotient { * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) * for more information. */ -class GetEventContextJob : public BaseJob { +class QUOTIENT_API GetEventContextJob : public BaseJob { public: /*! \brief Get events and state around the specified event. * diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h index 01bec36b..9518a461 100644 --- a/lib/csapi/filter.h +++ b/lib/csapi/filter.h @@ -16,7 +16,7 @@ namespace Quotient { * Returns a filter ID that may be used in future requests to * restrict which events are returned to the client. */ -class DefineFilterJob : public BaseJob { +class QUOTIENT_API DefineFilterJob : public BaseJob { public: /*! \brief Upload a new filter. * @@ -41,7 +41,7 @@ public: /*! \brief Download a filter * */ -class GetFilterJob : public BaseJob { +class QUOTIENT_API GetFilterJob : public BaseJob { public: /*! \brief Download a filter * diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h index eb13cc95..21e6cb74 100644 --- a/lib/csapi/inviting.h +++ b/lib/csapi/inviting.h @@ -26,7 +26,7 @@ namespace Quotient { * If the user was invited to the room, the homeserver will append a * `m.room.member` event to the room. */ -class InviteUserJob : public BaseJob { +class QUOTIENT_API InviteUserJob : public BaseJob { public: /*! \brief Invite a user to participate in a particular room. * diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index d0199b11..f64152f7 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -25,7 +25,7 @@ namespace Quotient { * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. */ -class JoinRoomByIdJob : public BaseJob { +class QUOTIENT_API JoinRoomByIdJob : public BaseJob { public: /*! \brief Start the requesting user participating in a particular room. * @@ -67,7 +67,7 @@ public: * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. */ -class JoinRoomJob : public BaseJob { +class QUOTIENT_API JoinRoomJob : public BaseJob { public: /*! \brief Start the requesting user participating in a particular room. * diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index 7db09e8d..ce1ca9ed 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -15,7 +15,7 @@ namespace Quotient { * * Publishes end-to-end encryption keys for the device. */ -class UploadKeysJob : public BaseJob { +class QUOTIENT_API UploadKeysJob : public BaseJob { public: /*! \brief Upload end-to-end encryption keys. * @@ -48,7 +48,7 @@ public: * * Returns the current devices and identity keys for the given users. */ -class QueryKeysJob : public BaseJob { +class QUOTIENT_API QueryKeysJob : public BaseJob { public: // Inner data structures @@ -172,7 +172,7 @@ struct JsonObjectConverter<QueryKeysJob::DeviceInformation> { * * Claims one-time keys for use in pre-key messages. */ -class ClaimKeysJob : public BaseJob { +class QUOTIENT_API ClaimKeysJob : public BaseJob { public: /*! \brief Claim one-time encryption keys. * @@ -226,7 +226,7 @@ public: * * added new device identity keys or removed an existing device with * identity keys, between `from` and `to`. */ -class GetKeysChangesJob : public BaseJob { +class QUOTIENT_API GetKeysChangesJob : public BaseJob { public: /*! \brief Query users with recent device key updates. * diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 11018368..6ac106e2 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -20,7 +20,7 @@ namespace Quotient { * directly adjust the target member's state by making a request to * `/rooms/<room id>/state/m.room.member/<user id>`. */ -class KickJob : public BaseJob { +class QUOTIENT_API KickJob : public BaseJob { public: /*! \brief Kick a user from the room. * diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h index 1108cb64..e3645b59 100644 --- a/lib/csapi/knocking.h +++ b/lib/csapi/knocking.h @@ -27,7 +27,7 @@ namespace Quotient { * The knock will appear as an entry in the response of the * [`/sync`](/client-server-api/#get_matrixclientr0sync) API. */ -class KnockRoomJob : public BaseJob { +class QUOTIENT_API KnockRoomJob : public BaseJob { public: /*! \brief Knock on a room, requesting permission to join. * diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h index 2e402d16..19cac3f0 100644 --- a/lib/csapi/leaving.h +++ b/lib/csapi/leaving.h @@ -22,7 +22,7 @@ namespace Quotient { * The user will still be allowed to retrieve history from the room which * they were previously allowed to see. */ -class LeaveRoomJob : public BaseJob { +class QUOTIENT_API LeaveRoomJob : public BaseJob { public: /*! \brief Stop the requesting user participating in a particular room. * @@ -48,7 +48,7 @@ public: * If the user is currently joined to the room, they must leave the room * before calling this API. */ -class ForgetRoomJob : public BaseJob { +class QUOTIENT_API ForgetRoomJob : public BaseJob { public: /*! \brief Stop the requesting user remembering about a particular room. * diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h index 59a24a49..aea68afd 100644 --- a/lib/csapi/list_joined_rooms.h +++ b/lib/csapi/list_joined_rooms.h @@ -12,7 +12,7 @@ namespace Quotient { * * This API returns a list of the user's current rooms. */ -class GetJoinedRoomsJob : public BaseJob { +class QUOTIENT_API GetJoinedRoomsJob : public BaseJob { public: /// Lists the user's current rooms. explicit GetJoinedRoomsJob(); diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 963c8b56..e1f03db7 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -14,7 +14,7 @@ namespace Quotient { * * Gets the visibility of a given room on the server's public room directory. */ -class GetRoomVisibilityOnDirectoryJob : public BaseJob { +class QUOTIENT_API GetRoomVisibilityOnDirectoryJob : public BaseJob { public: /*! \brief Gets the visibility of a room in the directory * @@ -48,7 +48,7 @@ public: * here, for instance that room visibility can only be changed by * the room creator or a server administrator. */ -class SetRoomVisibilityOnDirectoryJob : public BaseJob { +class QUOTIENT_API SetRoomVisibilityOnDirectoryJob : public BaseJob { public: /*! \brief Sets the visibility of a room in the room directory * @@ -70,7 +70,7 @@ public: * This API returns paginated responses. The rooms are ordered by the number * of joined members, with the largest rooms first. */ -class GetPublicRoomsJob : public BaseJob { +class QUOTIENT_API GetPublicRoomsJob : public BaseJob { public: /*! \brief Lists the public rooms on the server. * @@ -133,7 +133,7 @@ public: * This API returns paginated responses. The rooms are ordered by the number * of joined members, with the largest rooms first. */ -class QueryPublicRoomsJob : public BaseJob { +class QUOTIENT_API QueryPublicRoomsJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/login.h b/lib/csapi/login.h index b35db1eb..ce6951eb 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -16,7 +16,7 @@ namespace Quotient { * Gets the homeserver's supported login types to authenticate users. Clients * should pick one of these and supply it as the `type` when logging in. */ -class GetLoginFlowsJob : public BaseJob { +class QUOTIENT_API GetLoginFlowsJob : public BaseJob { public: // Inner data structures @@ -73,7 +73,7 @@ struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> { * [Relationship between access tokens and * devices](/client-server-api/#relationship-between-access-tokens-and-devices). */ -class LoginJob : public BaseJob { +class QUOTIENT_API LoginJob : public BaseJob { public: /*! \brief Authenticates the user. * diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h index 2e4c2692..3f1ac7fa 100644 --- a/lib/csapi/logout.h +++ b/lib/csapi/logout.h @@ -15,7 +15,7 @@ namespace Quotient { * [Device keys](/client-server-api/#device-keys) for the device are deleted * alongside the device. */ -class LogoutJob : public BaseJob { +class QUOTIENT_API LogoutJob : public BaseJob { public: /// Invalidates a user access token explicit LogoutJob(); @@ -44,7 +44,7 @@ public: * used in the request, and therefore the attacker is unable to take over the * account in this way. */ -class LogoutAllJob : public BaseJob { +class QUOTIENT_API LogoutAllJob : public BaseJob { public: /// Invalidates all access tokens for a user explicit LogoutAllJob(); diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 363e4d99..8c18f104 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -18,7 +18,7 @@ namespace Quotient { * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) * for more information. */ -class GetRoomEventsJob : public BaseJob { +class QUOTIENT_API GetRoomEventsJob : public BaseJob { public: /*! \brief Get a list of events for this room * diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index 0c38fe6b..23211758 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -14,7 +14,7 @@ namespace Quotient { * This API is used to paginate through the list of events that the * user has been, or would have been notified about. */ -class GetNotificationsJob : public BaseJob { +class QUOTIENT_API GetNotificationsJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index 0be39c8c..773b6011 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -21,7 +21,7 @@ namespace Quotient { * be used to request another OpenID access token or call `/sync`, for * example. */ -class RequestOpenIdTokenJob : public BaseJob { +class QUOTIENT_API RequestOpenIdTokenJob : public BaseJob { public: /*! \brief Get an OpenID token object to verify the requester's identity. * diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index 885ff340..14cb6f0b 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -22,7 +22,7 @@ namespace Quotient { * API will also be deprecated at some point, but its replacement is not * yet known. */ -class PeekEventsJob : public BaseJob { +class QUOTIENT_API PeekEventsJob : public BaseJob { public: /*! \brief Listen on the event stream. * diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index 4ab50e25..52445205 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -15,7 +15,7 @@ namespace Quotient { * not need to specify the `last_active_ago` field. You cannot set the * presence state of another user. */ -class SetPresenceJob : public BaseJob { +class QUOTIENT_API SetPresenceJob : public BaseJob { public: /*! \brief Update this user's presence state. * @@ -36,7 +36,7 @@ public: * * Get the given user's presence state. */ -class GetPresenceJob : public BaseJob { +class QUOTIENT_API GetPresenceJob : public BaseJob { public: /*! \brief Get this user's presence state. * diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 7f9c9e95..b00c944b 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -13,7 +13,7 @@ namespace Quotient { * This API sets the given user's display name. You must have permission to * set this user's display name, e.g. you need to have their `access_token`. */ -class SetDisplayNameJob : public BaseJob { +class QUOTIENT_API SetDisplayNameJob : public BaseJob { public: /*! \brief Set the user's display name. * @@ -33,7 +33,7 @@ public: * own displayname or to query the name of other users; either locally or * on remote homeservers. */ -class GetDisplayNameJob : public BaseJob { +class QUOTIENT_API GetDisplayNameJob : public BaseJob { public: /*! \brief Get the user's display name. * @@ -63,7 +63,7 @@ public: * This API sets the given user's avatar URL. You must have permission to * set this user's avatar URL, e.g. you need to have their `access_token`. */ -class SetAvatarUrlJob : public BaseJob { +class QUOTIENT_API SetAvatarUrlJob : public BaseJob { public: /*! \brief Set the user's avatar URL. * @@ -82,7 +82,7 @@ public: * own avatar URL or to query the URL of other users; either locally or * on remote homeservers. */ -class GetAvatarUrlJob : public BaseJob { +class QUOTIENT_API GetAvatarUrlJob : public BaseJob { public: /*! \brief Get the user's avatar URL. * @@ -111,7 +111,7 @@ public: * locally or on remote homeservers. This API may return keys which are not * limited to `displayname` or `avatar_url`. */ -class GetUserProfileJob : public BaseJob { +class QUOTIENT_API GetUserProfileJob : public BaseJob { public: /*! \brief Get this user's profile information. * diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index 622b0df6..d859ffc4 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -12,7 +12,7 @@ namespace Quotient { * * Gets all currently active pushers for the authenticated user. */ -class GetPushersJob : public BaseJob { +class QUOTIENT_API GetPushersJob : public BaseJob { public: // Inner data structures @@ -108,7 +108,7 @@ struct JsonObjectConverter<GetPushersJob::Pusher> { * [pushers](/client-server-api/#push-notifications) for this user ID. The * behaviour of this endpoint varies depending on the values in the JSON body. */ -class PostPusherJob : public BaseJob { +class QUOTIENT_API PostPusherJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h index a5eb48f0..d6c57efd 100644 --- a/lib/csapi/pushrules.h +++ b/lib/csapi/pushrules.h @@ -19,7 +19,7 @@ namespace Quotient { * `/pushrules/global/`. This will return a subset of this data under the * specified key e.g. the `global` key. */ -class GetPushRulesJob : public BaseJob { +class QUOTIENT_API GetPushRulesJob : public BaseJob { public: /// Retrieve all push rulesets. explicit GetPushRulesJob(); @@ -44,7 +44,7 @@ public: * * Retrieve a single specified push rule. */ -class GetPushRuleJob : public BaseJob { +class QUOTIENT_API GetPushRuleJob : public BaseJob { public: /*! \brief Retrieve a push rule. * @@ -79,7 +79,7 @@ public: * * This endpoint removes the push rule defined in the path. */ -class DeletePushRuleJob : public BaseJob { +class QUOTIENT_API DeletePushRuleJob : public BaseJob { public: /*! \brief Delete a push rule. * @@ -112,7 +112,7 @@ public: * * When creating push rules, they MUST be enabled by default. */ -class SetPushRuleJob : public BaseJob { +class QUOTIENT_API SetPushRuleJob : public BaseJob { public: /*! \brief Add or change a push rule. * @@ -160,7 +160,7 @@ public: * * This endpoint gets whether the specified push rule is enabled. */ -class IsPushRuleEnabledJob : public BaseJob { +class QUOTIENT_API IsPushRuleEnabledJob : public BaseJob { public: /*! \brief Get whether a push rule is enabled * @@ -195,7 +195,7 @@ public: * * This endpoint allows clients to enable or disable the specified push rule. */ -class SetPushRuleEnabledJob : public BaseJob { +class QUOTIENT_API SetPushRuleEnabledJob : public BaseJob { public: /*! \brief Enable or disable a push rule. * @@ -219,7 +219,7 @@ public: * * This endpoint get the actions for the specified push rule. */ -class GetPushRuleActionsJob : public BaseJob { +class QUOTIENT_API GetPushRuleActionsJob : public BaseJob { public: /*! \brief The actions for a push rule * @@ -258,7 +258,7 @@ public: * This endpoint allows clients to change the actions of a push rule. * This can be used to change the actions of builtin rules. */ -class SetPushRuleActionsJob : public BaseJob { +class QUOTIENT_API SetPushRuleActionsJob : public BaseJob { public: /*! \brief Set the actions for a push rule. * diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index 00a2aa0d..d13fa4fc 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -13,7 +13,7 @@ namespace Quotient { * Sets the position of the read marker for a given room, and optionally * the read receipt's location. */ -class SetReadMarkerJob : public BaseJob { +class QUOTIENT_API SetReadMarkerJob : public BaseJob { public: /*! \brief Set the position of the read marker for a room. * diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h index 7ac093cd..e29e7b29 100644 --- a/lib/csapi/receipts.h +++ b/lib/csapi/receipts.h @@ -13,7 +13,7 @@ namespace Quotient { * This API updates the marker for the given receipt type to the event ID * specified. */ -class PostReceiptJob : public BaseJob { +class QUOTIENT_API PostReceiptJob : public BaseJob { public: /*! \brief Send a receipt for the given event ID. * diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index f0db9f9f..29d9c5d5 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -22,7 +22,7 @@ namespace Quotient { * * Server administrators may redact events sent by users on their server. */ -class RedactEventJob : public BaseJob { +class QUOTIENT_API RedactEventJob : public BaseJob { public: /*! \brief Strips all non-integrity-critical information out of an event. * diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index c1614f20..10375971 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -59,7 +59,7 @@ namespace Quotient { * Any user ID returned by this API must conform to the grammar given in the * [Matrix specification](/appendices/#user-identifiers). */ -class RegisterJob : public BaseJob { +class QUOTIENT_API RegisterJob : public BaseJob { public: /*! \brief Register for an account on this homeserver. * @@ -143,7 +143,7 @@ public: * should validate the email itself, either by sending a validation email * itself or by using a service it has control over. */ -class RequestTokenToRegisterEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenToRegisterEmailJob : public BaseJob { public: /*! \brief Begins the validation process for an email to be used during * registration. @@ -175,7 +175,7 @@ public: * should validate the phone number itself, either by sending a validation * message itself or by using a service it has control over. */ -class RequestTokenToRegisterMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenToRegisterMSISDNJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given phone number for * the purpose of registering an account @@ -215,7 +215,7 @@ public: * access token provided in the request. Whether other access tokens for * the user are revoked depends on the request parameters. */ -class ChangePasswordJob : public BaseJob { +class QUOTIENT_API ChangePasswordJob : public BaseJob { public: /*! \brief Changes a user's password. * @@ -257,7 +257,7 @@ public: * The homeserver should validate the email itself, either by sending a * validation email itself or by using a service it has control over. */ -class RequestTokenToResetPasswordEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenToResetPasswordEmailJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given email address * for the purpose of resetting a user's password @@ -309,7 +309,7 @@ public: * The homeserver should validate the phone number itself, either by sending a * validation message itself or by using a service it has control over. */ -class RequestTokenToResetPasswordMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenToResetPasswordMSISDNJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given phone number for * the purpose of resetting a user's password. @@ -361,7 +361,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class DeactivateAccountJob : public BaseJob { +class QUOTIENT_API DeactivateAccountJob : public BaseJob { public: /*! \brief Deactivate a user's account. * @@ -411,7 +411,7 @@ public: * reserve the username. This can mean that the username becomes unavailable * between checking its availability and attempting to register it. */ -class CheckUsernameAvailabilityJob : public BaseJob { +class QUOTIENT_API CheckUsernameAvailabilityJob : public BaseJob { public: /*! \brief Checks to see if a username is available on the server. * diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h index e401c2e1..8c533c19 100644 --- a/lib/csapi/report_content.h +++ b/lib/csapi/report_content.h @@ -13,7 +13,7 @@ namespace Quotient { * Reports an event as inappropriate to the server, which may then notify * the appropriate people. */ -class ReportContentJob : public BaseJob { +class QUOTIENT_API ReportContentJob : public BaseJob { public: /*! \brief Reports an event as inappropriate. * diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index 96f5beca..fea3d59d 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -18,7 +18,7 @@ namespace Quotient { * fields in this object will vary depending on the type of event. See * [Room Events](/client-server-api/#room-events) for the m. event specification. */ -class SendMessageJob : public BaseJob { +class QUOTIENT_API SendMessageJob : public BaseJob { public: /*! \brief Send a message event to the given room. * diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h index f95af223..a00b0947 100644 --- a/lib/csapi/room_state.h +++ b/lib/csapi/room_state.h @@ -29,7 +29,7 @@ namespace Quotient { * state event is to be sent. Servers do not validate aliases which are * being removed or are already present in the state event. */ -class SetRoomStateWithKeyJob : public BaseJob { +class QUOTIENT_API SetRoomStateWithKeyJob : public BaseJob { public: /*! \brief Send a state event to the given room. * diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h index 58327587..0432f667 100644 --- a/lib/csapi/room_upgrades.h +++ b/lib/csapi/room_upgrades.h @@ -12,7 +12,7 @@ namespace Quotient { * * Upgrades the given room to a particular room version. */ -class UpgradeRoomJob : public BaseJob { +class QUOTIENT_API UpgradeRoomJob : public BaseJob { public: /*! \brief Upgrades a room to a new room version. * diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 2620582b..f0815109 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -15,7 +15,7 @@ namespace Quotient { * Get a single event based on `roomId/eventId`. You must have permission to * retrieve this event e.g. by being a member in the room for this event. */ -class GetOneRoomEventJob : public BaseJob { +class QUOTIENT_API GetOneRoomEventJob : public BaseJob { public: /*! \brief Get a single event by event ID. * @@ -48,7 +48,7 @@ public: * state of the room. If the user has left the room then the state is * taken from the state of the room when they left. */ -class GetRoomStateWithKeyJob : public BaseJob { +class QUOTIENT_API GetRoomStateWithKeyJob : public BaseJob { public: /*! \brief Get the state identified by the type and key. * @@ -80,7 +80,7 @@ public: * * Get the state events for the current state of a room. */ -class GetRoomStateJob : public BaseJob { +class QUOTIENT_API GetRoomStateJob : public BaseJob { public: /*! \brief Get all state events in the current state of a room. * @@ -106,7 +106,7 @@ public: * * Get the list of members for this room. */ -class GetMembersByRoomJob : public BaseJob { +class QUOTIENT_API GetMembersByRoomJob : public BaseJob { public: /*! \brief Get the m.room.member events for the room. * @@ -161,7 +161,7 @@ public: * respond than `/members` as it can be implemented more efficiently on the * server. */ -class GetJoinedMembersByRoomJob : public BaseJob { +class QUOTIENT_API GetJoinedMembersByRoomJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/search.h b/lib/csapi/search.h index 3d02752a..8683413d 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -15,7 +15,7 @@ namespace Quotient { * * Performs a full text search across different categories. */ -class SearchJob : public BaseJob { +class QUOTIENT_API SearchJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h index ade1eb7d..f4f81c1e 100644 --- a/lib/csapi/sso_login_redirect.h +++ b/lib/csapi/sso_login_redirect.h @@ -17,7 +17,7 @@ namespace Quotient { * or present a page which lets the user select an IdP to continue * with in the event multiple are supported by the server. */ -class RedirectToSSOJob : public BaseJob { +class QUOTIENT_API RedirectToSSOJob : public BaseJob { public: /*! \brief Redirect the user's browser to the SSO interface. * @@ -44,7 +44,7 @@ public: * The server MUST respond with an HTTP redirect to the SSO interface * for that IdP. */ -class RedirectToIdPJob : public BaseJob { +class QUOTIENT_API RedirectToIdPJob : public BaseJob { public: /*! \brief Redirect the user's browser to the SSO interface for an IdP. * diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h index a854531a..f4250674 100644 --- a/lib/csapi/tags.h +++ b/lib/csapi/tags.h @@ -12,7 +12,7 @@ namespace Quotient { * * List the tags set by a user on a room. */ -class GetRoomTagsJob : public BaseJob { +class QUOTIENT_API GetRoomTagsJob : public BaseJob { public: // Inner data structures @@ -68,7 +68,7 @@ struct JsonObjectConverter<GetRoomTagsJob::Tag> { * * Add a tag to the room. */ -class SetRoomTagJob : public BaseJob { +class QUOTIENT_API SetRoomTagJob : public BaseJob { public: /*! \brief Add a tag to a room. * @@ -98,7 +98,7 @@ public: * * Remove a tag from the room. */ -class DeleteRoomTagJob : public BaseJob { +class QUOTIENT_API DeleteRoomTagJob : public BaseJob { public: /*! \brief Remove a tag from the room. * diff --git a/lib/csapi/third_party_lookup.h b/lib/csapi/third_party_lookup.h index 969e767c..30c5346e 100644 --- a/lib/csapi/third_party_lookup.h +++ b/lib/csapi/third_party_lookup.h @@ -18,7 +18,7 @@ namespace Quotient { * homeserver. Includes both the available protocols and all fields * required for queries against each protocol. */ -class GetProtocolsJob : public BaseJob { +class QUOTIENT_API GetProtocolsJob : public BaseJob { public: /// Retrieve metadata about all protocols that a homeserver supports. explicit GetProtocolsJob(); @@ -45,7 +45,7 @@ public: * Fetches the metadata from the homeserver about a particular third party * protocol. */ -class GetProtocolMetadataJob : public BaseJob { +class QUOTIENT_API GetProtocolMetadataJob : public BaseJob { public: /*! \brief Retrieve metadata about a specific protocol that the homeserver * supports. @@ -82,7 +82,7 @@ public: * identifier. It should attempt to canonicalise the identifier as much * as reasonably possible given the network type. */ -class QueryLocationByProtocolJob : public BaseJob { +class QUOTIENT_API QueryLocationByProtocolJob : public BaseJob { public: /*! \brief Retrieve Matrix-side portals rooms leading to a third party * location. @@ -119,7 +119,7 @@ public: * Retrieve a Matrix User ID linked to a user on the third party service, given * a set of user parameters. */ -class QueryUserByProtocolJob : public BaseJob { +class QUOTIENT_API QueryUserByProtocolJob : public BaseJob { public: /*! \brief Retrieve the Matrix User ID of a corresponding third party user. * @@ -155,7 +155,7 @@ public: * Retrieve an array of third party network locations from a Matrix room * alias. */ -class QueryLocationByAliasJob : public BaseJob { +class QUOTIENT_API QueryLocationByAliasJob : public BaseJob { public: /*! \brief Reverse-lookup third party locations given a Matrix room alias. * @@ -184,7 +184,7 @@ public: * * Retrieve an array of third party users from a Matrix User ID. */ -class QueryUserByIDJob : public BaseJob { +class QUOTIENT_API QueryUserByIDJob : public BaseJob { public: /*! \brief Reverse-lookup third party users given a Matrix User ID. * diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h index a424678f..1edb969e 100644 --- a/lib/csapi/third_party_membership.h +++ b/lib/csapi/third_party_membership.h @@ -52,7 +52,7 @@ namespace Quotient { * If a token is requested from the identity server, the homeserver will * append a `m.room.third_party_invite` event to the room. */ -class InviteBy3PIDJob : public BaseJob { +class QUOTIENT_API InviteBy3PIDJob : public BaseJob { public: /*! \brief Invite a user to participate in a particular room. * diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h index 7a237195..5b6e0bfb 100644 --- a/lib/csapi/to_device.h +++ b/lib/csapi/to_device.h @@ -13,7 +13,7 @@ namespace Quotient { * This endpoint is used to send send-to-device events to a set of * client devices. */ -class SendToDeviceJob : public BaseJob { +class QUOTIENT_API SendToDeviceJob : public BaseJob { public: /*! \brief Send an event to a given set of devices. * diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h index 64a310d0..234e91b0 100644 --- a/lib/csapi/typing.h +++ b/lib/csapi/typing.h @@ -15,7 +15,7 @@ namespace Quotient { * Alternatively, if `typing` is `false`, it tells the server that the * user has stopped typing. */ -class SetTypingJob : public BaseJob { +class QUOTIENT_API SetTypingJob : public BaseJob { public: /*! \brief Informs the server that the user has started or stopped typing. * diff --git a/lib/csapi/users.h b/lib/csapi/users.h index ec186592..3c99758b 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -21,7 +21,7 @@ namespace Quotient { * names preferably using a collation determined based upon the * `Accept-Language` header provided in the request, if present. */ -class SearchUserDirectoryJob : public BaseJob { +class QUOTIENT_API SearchUserDirectoryJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 896e2ea9..4445dbd2 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -31,7 +31,7 @@ namespace Quotient { * upgrade appropriately. Additionally, clients should avoid using unstable * features in their stable releases. */ -class GetVersionsJob : public BaseJob { +class QUOTIENT_API GetVersionsJob : public BaseJob { public: /// Gets the versions of the specification supported by the server. explicit GetVersionsJob(); diff --git a/lib/csapi/voip.h b/lib/csapi/voip.h index 087ebbbd..38904f60 100644 --- a/lib/csapi/voip.h +++ b/lib/csapi/voip.h @@ -13,7 +13,7 @@ namespace Quotient { * This API provides credentials for the client to use when initiating * calls. */ -class GetTurnServerJob : public BaseJob { +class QUOTIENT_API GetTurnServerJob : public BaseJob { public: /// Obtain TURN server credentials. explicit GetTurnServerJob(); diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index c707d232..8615191c 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -21,7 +21,7 @@ namespace Quotient { * Note that this endpoint is not necessarily handled by the homeserver, * but by another webserver, to be used for discovering the homeserver URL. */ -class GetWellknownJob : public BaseJob { +class QUOTIENT_API GetWellknownJob : public BaseJob { public: /// Gets Matrix server discovery information about the domain. explicit GetWellknownJob(); diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index 319f82c5..fba099f6 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -19,7 +19,7 @@ namespace Quotient { * is registered by the appservice, and return it in the response * body. */ -class GetTokenOwnerJob : public BaseJob { +class QUOTIENT_API GetTokenOwnerJob : public BaseJob { public: /// Gets information about the owner of an access token. explicit GetTokenOwnerJob(); diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 4f1595bc..a2d65d8d 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -25,3 +25,7 @@ void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) } setStatus(EventStatus::FileUploaded); } + +// Not exactly sure why but this helps with the linker not finding +// Quotient::EventStatus::staticMetaObject when building Quaternion +#include "moc_eventitem.cpp" diff --git a/lib/eventitem.h b/lib/eventitem.h index 0ab1a01d..f04ef323 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -4,6 +4,7 @@ #pragma once #include "events/stateevent.h" +#include "quotient_common.h" #include <any> #include <utility> @@ -11,7 +12,7 @@ namespace Quotient { namespace EventStatus { - Q_NAMESPACE + QUO_NAMESPACE /** Special marks an event can assume * @@ -33,7 +34,7 @@ namespace EventStatus { Q_ENUM_NS(Code) } // namespace EventStatus -class EventItemBase { +class QUOTIENT_API EventItemBase { public: explicit EventItemBase(RoomEventPtr&& e) : evt(std::move(e)) { @@ -57,7 +58,7 @@ public: } /// Store arbitrary data with the event item - void setUserData(std::any userData) { data = userData; } + void setUserData(std::any userData) { data = std::move(userData); } /// Obtain custom data previously stored with the event item const std::any& userdata() const { return data; } std::any& userData() { return data; } @@ -74,7 +75,7 @@ private: std::any data; }; -class TimelineItem : public EventItemBase { +class QUOTIENT_API TimelineItem : public EventItemBase { public: // For compatibility with Qt containers, even though we use // a std:: container now for the room timeline @@ -103,7 +104,7 @@ inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const return evt->isCallEvent() ? weakPtrCast<const CallEventBase>(evt) : nullptr; } -class PendingEventItem : public EventItemBase { +class QUOTIENT_API PendingEventItem : public EventItemBase { public: using EventItemBase::EventItemBase; diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index e5101d20..12f1f00b 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -4,28 +4,24 @@ #pragma once #include "event.h" -#include "eventcontent.h" +#include "util.h" namespace Quotient { -constexpr const char* FavouriteTag = "m.favourite"; -constexpr const char* LowPriorityTag = "m.lowpriority"; -constexpr const char* ServerNoticeTag = "m.server_notice"; +constexpr auto FavouriteTag [[maybe_unused]] = "m.favourite"_ls; +constexpr auto LowPriorityTag [[maybe_unused]] = "m.lowpriority"_ls; +constexpr auto ServerNoticeTag [[maybe_unused]] = "m.server_notice"_ls; struct TagRecord { - using order_type = Omittable<float>; - - order_type order; - - TagRecord(order_type order = none) : order(std::move(order)) {} - - bool operator<(const TagRecord& other) const - { - // Per The Spec, rooms with no order should be after those with order, - // against optional<>::operator<() convention. - return order && (!other.order || *order < *other.order); - } + Omittable<float> order = none; }; +inline bool operator<(TagRecord lhs, TagRecord rhs) +{ + // Per The Spec, rooms with no order should be after those with order, + // against std::optional<>::operator<() convention. + return lhs.order && (!rhs.order || *lhs.order < *rhs.order); +} + template <> struct JsonObjectConverter<TagRecord> { static void fillFrom(const QJsonObject& jo, TagRecord& rec) @@ -42,7 +38,7 @@ struct JsonObjectConverter<TagRecord> { rec.order = none; } } - static void dumpTo(QJsonObject& jo, const TagRecord& rec) + static void dumpTo(QJsonObject& jo, TagRecord rec) { addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order); } @@ -51,7 +47,7 @@ struct JsonObjectConverter<TagRecord> { using TagsMap = QHash<QString, TagRecord>; #define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public Event { \ + class QUOTIENT_API _Name : public Event { \ public: \ using content_type = _ContentType; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 4c01c941..8ffe60f2 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -7,7 +7,7 @@ #include "roomevent.h" namespace Quotient { -class CallAnswerEvent : public CallEventBase { +class QUOTIENT_API CallAnswerEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent) @@ -26,6 +26,5 @@ public: return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString(); } }; - REGISTER_EVENT_TYPE(CallAnswerEvent) } // namespace Quotient diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp deleted file mode 100644 index b87c8e9b..00000000 --- a/lib/events/callcandidatesevent.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com> -// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "callcandidatesevent.h" - -/* -m.call.candidates -{ - "age": 242352, - "content": { - "call_id": "12345", - "candidates": [ - { - "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 -43670 typ host generation 0", "sdpMLineIndex": 0, "sdpMid": "audio" - } - ], - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.candidates" -} -*/ diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp deleted file mode 100644 index 43bc4db0..00000000 --- a/lib/events/callhangupevent.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com> - * SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com> - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ - -#include "callhangupevent.h" - -/* -m.call.hangup -{ - "age": 242352, - "content": { - "call_id": "12345", - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.hangup" -} -*/ - -using namespace Quotient; - -CallHangupEvent::CallHangupEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Hangup event"; -} - -CallHangupEvent::CallHangupEvent(const QString& callId) - : CallEventBase(typeId(), matrixTypeId(), callId, 0) -{} diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index 24382ac2..b0017c59 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -7,12 +7,16 @@ #include "roomevent.h" namespace Quotient { -class CallHangupEvent : public CallEventBase { +class QUOTIENT_API CallHangupEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) - explicit CallHangupEvent(const QJsonObject& obj); - explicit CallHangupEvent(const QString& callId); + explicit CallHangupEvent(const QJsonObject& obj) + : CallEventBase(typeId(), obj) + {} + explicit CallHangupEvent(const QString& callId) + : CallEventBase(typeId(), matrixTypeId(), callId, 0) + {} }; REGISTER_EVENT_TYPE(CallHangupEvent) diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index 80b7d651..47362b5c 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -7,7 +7,7 @@ #include "roomevent.h" namespace Quotient { -class CallInviteEvent : public CallEventBase { +class QUOTIENT_API CallInviteEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent) diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index e2143779..2018d3d6 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -6,7 +6,7 @@ #include "event.h" namespace Quotient { -class DirectChatEvent : public Event { +class QUOTIENT_API DirectChatEvent : public Event { public: DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent) diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 4cc3bf8e..c838bbd8 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -25,7 +25,7 @@ namespace Quotient { * in general. It's possible, because RoomEvent interface is similar to Event's * one and doesn't add new restrictions, just provides additional features. */ -class EncryptedEvent : public RoomEvent { +class QUOTIENT_API EncryptedEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent) diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index 14439fcc..124ced33 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -6,13 +6,18 @@ #include "eventcontent.h" #include "stateevent.h" +#include "quotient_common.h" namespace Quotient { -class EncryptionEventContent : public EventContent::Base { +class QUOTIENT_API EncryptionEventContent : public EventContent::Base { public: enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; - explicit EncryptionEventContent(EncryptionType et = Undefined); + QUO_IMPLICIT EncryptionEventContent(EncryptionType et); + [[deprecated("This constructor will require explicit EncryptionType soon")]] // + explicit EncryptionEventContent() + : EncryptionEventContent(Undefined) + {} explicit EncryptionEventContent(const QJsonObject& json); EncryptionType encryption; @@ -26,7 +31,7 @@ protected: using EncryptionType = EncryptionEventContent::EncryptionType; -class EncryptionEvent : public StateEvent<EncryptionEventContent> { +class QUOTIENT_API EncryptionEvent : public StateEvent<EncryptionEventContent> { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) @@ -34,15 +39,15 @@ public: using EncryptionType = EncryptionEventContent::EncryptionType; Q_ENUM(EncryptionType) - explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate - // default value + explicit EncryptionEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - EncryptionEvent(EncryptionEvent&&) = delete; - template <typename... ArgTs> - EncryptionEvent(ArgTs&&... contentArgs) - : StateEvent(typeId(), matrixTypeId(), QString(), - std::forward<ArgTs>(contentArgs)...) + [[deprecated("This constructor will require an explicit parameter soon")]] // +// explicit EncryptionEvent() +// : EncryptionEvent(QJsonObject()) +// {} + explicit EncryptionEvent(EncryptionEventContent&& content) + : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) {} EncryptionType encryption() const { return content().encryption; } @@ -51,6 +56,5 @@ public: int rotationPeriodMs() const { return content().rotationPeriodMs; } int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } }; - REGISTER_EVENT_TYPE(EncryptionEvent) } // namespace Quotient diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 96be717c..4c304a3c 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -9,22 +9,14 @@ using namespace Quotient; -event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) -{ - const auto id = get().eventTypes.size(); - get().eventTypes.push_back(matrixTypeId); - if (strncmp(matrixTypeId, "", 1) == 0) - qDebug(EVENTS) << "Initialized unknown event type with id" << id; - else - qDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" - << id; - return id; -} +QString EventTypeRegistry::getMatrixType(event_type_t typeId) { return typeId; } -QString EventTypeRegistry::getMatrixType(event_type_t typeId) +void _impl::EventFactoryBase::logAddingMethod(event_type_t TypeId, + size_t newSize) { - return typeId < get().eventTypes.size() ? get().eventTypes[typeId] - : QString(); + qDebug(EVENTS) << "Adding factory method for" << TypeId << "events;" + << newSize << "methods will be in the" << name + << "chain"; } Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) diff --git a/lib/events/event.h b/lib/events/event.h index 998a386c..113fa3fa 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -5,6 +5,7 @@ #include "converters.h" #include "logging.h" +#include "function_traits.h" namespace Quotient { // === event_ptr_tt<> and type casting facilities === @@ -28,24 +29,24 @@ inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) // === Standard Matrix key names and basicEventJson() === -static const auto TypeKey = QStringLiteral("type"); -static const auto BodyKey = QStringLiteral("body"); -static const auto ContentKey = QStringLiteral("content"); -static const auto EventIdKey = QStringLiteral("event_id"); -static const auto SenderKey = QStringLiteral("sender"); -static const auto RoomIdKey = QStringLiteral("room_id"); -static const auto UnsignedKey = QStringLiteral("unsigned"); -static const auto StateKeyKey = QStringLiteral("state_key"); -static const auto TypeKeyL = "type"_ls; -static const auto BodyKeyL = "body"_ls; -static const auto ContentKeyL = "content"_ls; -static const auto EventIdKeyL = "event_id"_ls; -static const auto SenderKeyL = "sender"_ls; -static const auto RoomIdKeyL = "room_id"_ls; -static const auto UnsignedKeyL = "unsigned"_ls; -static const auto RedactedCauseKeyL = "redacted_because"_ls; -static const auto PrevContentKeyL = "prev_content"_ls; -static const auto StateKeyKeyL = "state_key"_ls; +constexpr auto TypeKeyL = "type"_ls; +constexpr auto BodyKeyL = "body"_ls; +constexpr auto ContentKeyL = "content"_ls; +constexpr auto EventIdKeyL = "event_id"_ls; +constexpr auto SenderKeyL = "sender"_ls; +constexpr auto RoomIdKeyL = "room_id"_ls; +constexpr auto UnsignedKeyL = "unsigned"_ls; +constexpr auto RedactedCauseKeyL = "redacted_because"_ls; +constexpr auto PrevContentKeyL = "prev_content"_ls; +constexpr auto StateKeyKeyL = "state_key"_ls; +const QString TypeKey { TypeKeyL }; +const QString BodyKey { BodyKeyL }; +const QString ContentKey { ContentKeyL }; +const QString EventIdKey { EventIdKeyL }; +const QString SenderKey { SenderKeyL }; +const QString RoomIdKey { RoomIdKeyL }; +const QString UnsignedKey { UnsignedKeyL }; +const QString StateKeyKey { StateKeyKeyL }; /// Make a minimal correct Matrix event JSON inline QJsonObject basicEventJson(const QString& matrixType, @@ -54,149 +55,135 @@ inline QJsonObject basicEventJson(const QString& matrixType, return { { TypeKey, matrixType }, { ContentKey, content } }; } -// === Event types and event types registry === +// === Event types === -using event_type_t = size_t; +using event_type_t = QLatin1String; using event_mtype_t = const char*; -class EventTypeRegistry { +class QUOTIENT_API EventTypeRegistry { public: ~EventTypeRegistry() = default; - static event_type_t initializeTypeId(event_mtype_t matrixTypeId); - - template <typename EventT> - static inline event_type_t initializeTypeId() - { - return initializeTypeId(EventT::matrixTypeId()); - } - + [[deprecated("event_type_t is a string now, use it directly instead")]] static QString getMatrixType(event_type_t typeId); private: EventTypeRegistry() = default; Q_DISABLE_COPY_MOVE(EventTypeRegistry) - - static EventTypeRegistry& get() - { - static EventTypeRegistry etr; - return etr; - } - - std::vector<event_mtype_t> eventTypes; -}; - -template <> -inline event_type_t EventTypeRegistry::initializeTypeId<void>() -{ - return initializeTypeId(""); -} - -template <typename EventT> -struct EventTypeTraits { - static event_type_t id() - { - static const auto id = EventTypeRegistry::initializeTypeId<EventT>(); - return id; - } }; template <typename EventT> -inline event_type_t typeId() +constexpr event_type_t typeId() { - return EventTypeTraits<std::decay_t<EventT>>::id(); + return std::decay_t<EventT>::TypeId; } -inline event_type_t unknownEventTypeId() { return typeId<void>(); } +constexpr event_type_t UnknownEventTypeId = "?"_ls; +[[deprecated("Use UnknownEventTypeId")]] +constexpr event_type_t unknownEventTypeId() { return UnknownEventTypeId; } -// === EventFactory === +// === Event creation facilities === -/** Create an event of arbitrary type from its arguments */ +//! Create an event of arbitrary type from its arguments template <typename EventT, typename... ArgTs> inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args) { return std::make_unique<EventT>(std::forward<ArgTs>(args)...); } +namespace _impl { + class QUOTIENT_API EventFactoryBase { + public: + EventFactoryBase(const EventFactoryBase&) = delete; + + protected: // This class is only to inherit from + explicit EventFactoryBase(const char* name) + : name(name) + {} + void logAddingMethod(event_type_t TypeId, size_t newSize); + + private: + const char* const name; + }; +} // namespace _impl + +//! \brief A family of event factories to create events from CS API responses +//! +//! Each of these factories, as instantiated by event base types (Event, +//! RoomEvent etc.) is capable of producing an event object derived from +//! \p BaseEventT, using the JSON payload and the event type passed to its +//! make() method. Don't use these directly to make events; use loadEvent() +//! overloads as the frontend for these. Never instantiate new factories +//! outside of base event classes. +//! \sa loadEvent, setupFactory, Event::factory, RoomEvent::factory, +//! StateEventBase::factory template <typename BaseEventT> -class EventFactory { -public: - template <typename FnT> - static auto addMethod(FnT&& method) +class EventFactory : public _impl::EventFactoryBase { +private: + using method_t = event_ptr_tt<BaseEventT> (*)(const QJsonObject&, + const QString&); + std::vector<method_t> methods {}; + + template <class EventT> + static event_ptr_tt<BaseEventT> makeIfMatches(const QJsonObject& json, + const QString& matrixType) { - factories().emplace_back(std::forward<FnT>(method)); - return 0; + // If your matrix event type is not all ASCII, it's your problem + // (see https://github.com/matrix-org/matrix-doc/pull/2758) + return EventT::TypeId == matrixType ? makeEvent<EventT>(json) : nullptr; } - /** Chain two type factories - * Adds the factory class of EventT2 (EventT2::factory_t) to - * the list in factory class of EventT1 (EventT1::factory_t) so - * that when EventT1::factory_t::make() is invoked, types of - * EventT2 factory are looked through as well. This is used - * to include RoomEvent types into the more general Event factory, - * and state event types into the RoomEvent factory. - */ - template <typename EventT> - static auto chainFactory() +public: + explicit EventFactory(const char* fName) + : EventFactoryBase { fName } + {} + + //! \brief Add a method to create events of a given type + //! + //! Adds a standard factory method (makeIfMatches) for \p EventT so that + //! event objects of this type can be created dynamically by loadEvent. + //! The caller is responsible for ensuring this method is called only + //! once per type. + //! \sa loadEvent, Quotient::loadEvent + template <class EventT> + const auto& addMethod() { - return addMethod(&EventT::factory_t::make); + const auto m = &makeIfMatches<EventT>; + const auto it = std::find(methods.cbegin(), methods.cend(), m); + if (it != methods.cend()) + return *it; + logAddingMethod(EventT::TypeId, methods.size() + 1); + return methods.emplace_back(m); } - static event_ptr_tt<BaseEventT> make(const QJsonObject& json, - const QString& matrixType) + auto loadEvent(const QJsonObject& json, const QString& matrixType) { - for (const auto& f : factories()) + for (const auto& f : methods) if (auto e = f(json, matrixType)) return e; - return nullptr; - } - -private: - static auto& factories() - { - using inner_factory_tt = std::function<event_ptr_tt<BaseEventT>( - const QJsonObject&, const QString&)>; - static std::vector<inner_factory_tt> _factories {}; - return _factories; + return makeEvent<BaseEventT>(UnknownEventTypeId, json); } }; -/** Add a type to its default factory - * Adds a standard factory method (via makeEvent<>) for a given - * type to EventT::factory_t factory class so that it can be - * created dynamically from loadEvent<>(). - * - * \tparam EventT the type to enable dynamic creation of - * \return the registered type id - * \sa loadEvent, Event::type - */ -template <typename EventT> -inline auto setupFactory() -{ - qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); - return EventT::factory_t::addMethod([](const QJsonObject& json, - const QString& jsonMatrixType) { - return EventT::matrixTypeId() == jsonMatrixType ? makeEvent<EventT>(json) - : nullptr; - }); -} - -template <typename EventT> -inline auto registerEventType() +//! \brief Point of customisation to dynamically load events +//! +//! The default specialisation of this calls BaseEventT::factory.loadEvent() +//! and if that fails (i.e. returns nullptr) creates an unknown event of +//! BaseEventT. Other specialisations may reuse other factories, add validations +//! common to BaseEventT events, and so on. +template <class BaseEventT> +event_ptr_tt<BaseEventT> doLoadEvent(const QJsonObject& json, + const QString& matrixType) { - // 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 + return BaseEventT::factory.loadEvent(json, matrixType); } // === Event === -class Event { +class QUOTIENT_API Event { public: using Type = event_type_t; - using factory_t = EventFactory<Event>; + static inline EventFactory<Event> factory { "Event" }; explicit Event(Type type, const QJsonObject& json); explicit Event(Type type, event_mtype_t matrixType, @@ -246,7 +233,7 @@ public: return fromJson<T>(unsignedJson()[std::forward<KeyT>(key)]); } - friend QDebug operator<<(QDebug dbg, const Event& e) + friend QUOTIENT_API QDebug operator<<(QDebug dbg, const Event& e) { QDebugStateSaver _dss { dbg }; dbg.noquote().nospace() << e.matrixType() << '(' << e.type() << "): "; @@ -271,26 +258,27 @@ template <typename EventT> using EventsArray = std::vector<event_ptr_tt<EventT>>; using Events = EventsArray<Event>; -// === Macros used with event class definitions === +// === Facilities for event class definitions === // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). -#define DEFINE_EVENT_TYPEID(_Id, _Type) \ - static constexpr event_mtype_t matrixTypeId() { return _Id; } \ - static auto typeId() { return Quotient::typeId<_Type>(); } \ +#define DEFINE_EVENT_TYPEID(Id_, Type_) \ + static constexpr event_type_t TypeId = Id_##_ls; \ + [[deprecated("Use " #Type_ "::TypeId directly instead")]] \ + static constexpr event_mtype_t matrixTypeId() { return Id_; } \ + [[deprecated("Use " #Type_ "::TypeId directly instead")]] \ + static event_type_t typeId() { return TypeId; } \ // End of macro // This macro should be put after an event class definition (in .h or .cpp) // to enable its deserialisation from a /sync and other // polymorphic event arrays -#define REGISTER_EVENT_TYPE(_Type) \ - namespace { \ - [[maybe_unused]] static const auto _factoryAdded##_Type = \ - registerEventType<_Type>(); \ - } \ +#define REGISTER_EVENT_TYPE(Type_) \ + [[maybe_unused]] inline const auto& factoryMethodFor##Type_ = \ + Type_::factory.addMethod<Type_>(); \ // End of macro -// === is<>(), eventCast<>() and visit<>() === +// === is<>(), eventCast<>() and switchOnType<>() === template <class EventT> inline bool is(const Event& e) @@ -300,7 +288,7 @@ inline bool is(const Event& e) inline bool isUnknown(const Event& e) { - return e.type() == unknownEventTypeId(); + return e.type() == UnknownEventTypeId; } template <class EventT, typename BasePtrT> @@ -312,68 +300,75 @@ inline auto eventCast(const BasePtrT& eptr) : nullptr; } -// A single generic catch-all visitor +// A trivial generic catch-all "switch" template <class BaseEventT, typename FnT> -inline auto visit(const BaseEventT& event, FnT&& visitor) - -> decltype(visitor(event)) +inline auto switchOnType(const BaseEventT& event, FnT&& fn) + -> decltype(fn(event)) { - return visitor(event); + return fn(event); } namespace _impl { // Using bool instead of auto below because auto apparently upsets MSVC template <class BaseT, typename FnT> - inline constexpr bool needs_downcast = + constexpr bool needs_downcast = std::is_base_of_v<BaseT, std::decay_t<fn_arg_t<FnT>>> && !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>; } -// A single type-specific void visitor +// A trivial type-specific "switch" for a void function template <class BaseT, typename FnT> -inline auto visit(const BaseT& event, FnT&& visitor) +inline auto switchOnType(const BaseT& event, FnT&& fn) -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT> && std::is_void_v<fn_return_t<FnT>>> { using event_type = fn_arg_t<FnT>; if (is<std::decay_t<event_type>>(event)) - visitor(static_cast<event_type>(event)); + fn(static_cast<event_type>(event)); } -// A single type-specific non-void visitor with an optional default value -// non-voidness is guarded by defaultValue type +// A trivial type-specific "switch" for non-void functions with an optional +// default value; non-voidness is guarded by defaultValue type template <class BaseT, typename FnT> -inline auto visit(const BaseT& event, FnT&& visitor, - fn_return_t<FnT>&& defaultValue = {}) +inline auto switchOnType(const BaseT& event, FnT&& fn, + fn_return_t<FnT>&& defaultValue = {}) -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>> { using event_type = fn_arg_t<FnT>; if (is<std::decay_t<event_type>>(event)) - return visitor(static_cast<event_type>(event)); - return std::forward<fn_return_t<FnT>>(defaultValue); + return fn(static_cast<event_type>(event)); + return std::move(defaultValue); } -// A chain of 2 or more visitors +// A switch for a chain of 2 or more functions template <class BaseT, typename FnT1, typename FnT2, typename... FnTs> -inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> visit( - const BaseT& event, FnT1&& visitor1, FnT2&& visitor2, - FnTs&&... visitors) +inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> +switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns) { using event_type1 = fn_arg_t<FnT1>; 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)...); + return fn1(static_cast<event_type1&>(event)); + return switchOnType(event, std::forward<FnT2>(fn2), + std::forward<FnTs>(fns)...); +} + +template <class BaseT, typename... FnTs> +[[deprecated("The new name for visit() is switchOnType()")]] // +inline auto visit(const BaseT& event, FnTs&&... fns) +{ + return switchOnType(event, std::forward<FnTs>(fns)...); } -// A facility overload that calls void-returning visit() on each event + // A facility overload that calls void-returning switchOnType() on each event // over a range of event pointers +// TODO: replace with ranges::for_each once all standard libraries have it template <typename RangeT, typename... FnTs> -inline auto visitEach(RangeT&& events, FnTs&&... visitors) +inline auto visitEach(RangeT&& events, FnTs&&... fns) -> std::enable_if_t<std::is_void_v< - decltype(visit(**begin(events), std::forward<FnTs>(visitors)...))>> + decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>> { for (auto&& evtPtr: events) - visit(*evtPtr, std::forward<FnTs>(visitors)...); + switchOnType(*evtPtr, std::forward<FnTs>(fns)...); } } // namespace Quotient Q_DECLARE_METATYPE(Quotient::Event*) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index d4cb43ff..9d7edf20 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -4,7 +4,6 @@ #include "eventcontent.h" #include "converters.h" -#include "util.h" #include "logging.h" #include <QtCore/QMimeDatabase> diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index f609a603..de9a792b 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -6,14 +6,15 @@ // This file contains generic event content definitions, applicable to room // message events as well as other events (e.g., avatars). +#include "encryptedfile.h" +#include "quotient_export.h" + #include <QtCore/QJsonObject> #include <QtCore/QMimeType> #include <QtCore/QSize> #include <QtCore/QUrl> #include <QtCore/QMetaType> -#include "encryptedfile.h" - class QFileInfo; namespace Quotient { @@ -28,7 +29,7 @@ namespace EventContent { * assumed but not required that a content object can also be created * from plain data. */ - class Base { + class QUOTIENT_API Base { public: explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} virtual ~Base() = default; @@ -76,7 +77,7 @@ namespace EventContent { * * This class is not polymorphic. */ - class FileInfo { + class QUOTIENT_API FileInfo { public: FileInfo() = default; explicit FileInfo(const QFileInfo& fi); @@ -105,7 +106,7 @@ namespace EventContent { QJsonObject originalInfoJson; QMimeType mimeType; QUrl url; - qint64 payloadSize; + qint64 payloadSize = 0; QString originalName; Omittable<EncryptedFile> file = none; }; @@ -121,7 +122,7 @@ namespace EventContent { /** * A content info class for image content types: image, thumbnail, video */ - class ImageInfo : public FileInfo { + class QUOTIENT_API ImageInfo : public FileInfo { public: ImageInfo() = default; explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); @@ -146,12 +147,10 @@ namespace EventContent { * the JSON representation of event content; namely, * "info/thumbnail_url" and "info/thumbnail_info" fields are used. */ - class Thumbnail : public ImageInfo { + class QUOTIENT_API Thumbnail : public ImageInfo { public: - Thumbnail() = default; // Allow empty thumbnails - Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file = none); - Thumbnail(const ImageInfo& info) : ImageInfo(info) {} using ImageInfo::ImageInfo; + Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file = none); /** * Writes thumbnail information to "thumbnail_info" subobject @@ -160,7 +159,7 @@ namespace EventContent { void fillInfoJson(QJsonObject* infoJson) const; }; - class TypedBase : public Base { + class QUOTIENT_API TypedBase : public Base { public: virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } @@ -183,7 +182,7 @@ namespace EventContent { * \tparam InfoT base info class */ template <class InfoT> - class UrlBasedContent : public TypedBase, public InfoT { + class QUOTIENT_API UrlBasedContent : public TypedBase, public InfoT { public: using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) @@ -215,7 +214,7 @@ namespace EventContent { }; template <typename InfoT> - class UrlWithThumbnailContent : public UrlBasedContent<InfoT> { + class QUOTIENT_API UrlWithThumbnailContent : public UrlBasedContent<InfoT> { public: // NB: when using inherited constructors, thumbnail has to be // initialised separately diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 978668f2..fe624d70 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -6,16 +6,6 @@ #include "stateevent.h" namespace Quotient { -namespace _impl { - template <typename BaseEventT> - static inline auto loadEvent(const QJsonObject& json, - const QString& matrixType) - { - if (auto e = EventFactory<BaseEventT>::make(json, matrixType)) - return e; - return makeEvent<BaseEventT>(unknownEventTypeId(), json); - } -} // namespace _impl /*! Create an event with proper type from a JSON object * @@ -26,7 +16,7 @@ namespace _impl { template <typename BaseEventT> inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson) { - return _impl::loadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString()); + return doLoadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString()); } /*! Create an event from a type string and content JSON @@ -39,8 +29,8 @@ template <typename BaseEventT> inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType, const QJsonObject& content) { - return _impl::loadEvent<BaseEventT>(basicEventJson(matrixType, content), - matrixType); + return doLoadEvent<BaseEventT>(basicEventJson(matrixType, content), + matrixType); } /*! Create a state event from a type string, content JSON and state key @@ -53,7 +43,7 @@ inline StateEventPtr loadStateEvent(const QString& matrixType, const QJsonObject& content, const QString& stateKey = {}) { - return _impl::loadEvent<StateEventBase>( + return doLoadEvent<StateEventBase>( basicStateEventJson(matrixType, content, stateKey), matrixType); } diff --git a/lib/events/eventrelation.cpp b/lib/events/eventrelation.cpp new file mode 100644 index 00000000..04972f45 --- /dev/null +++ b/lib/events/eventrelation.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "eventrelation.h" + +#include "../logging.h" +#include "event.h" + +using namespace Quotient; + +void JsonObjectConverter<EventRelation>::dumpTo(QJsonObject& jo, + const EventRelation& pod) +{ + if (pod.type.isEmpty()) { + qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; + return; + } + jo.insert(RelTypeKey, pod.type); + jo.insert(EventIdKey, pod.eventId); + if (pod.type == EventRelation::AnnotationType) + jo.insert(QStringLiteral("key"), pod.key); +} + +void JsonObjectConverter<EventRelation>::fillFrom(const QJsonObject& jo, + EventRelation& pod) +{ + if (const auto replyJson = jo.value(EventRelation::ReplyType).toObject(); + !replyJson.isEmpty()) { + pod.type = EventRelation::ReplyType; + fromJson(replyJson[EventIdKeyL], pod.eventId); + } else { + // The experimental logic for generic relationships (MSC1849) + fromJson(jo[RelTypeKey], pod.type); + fromJson(jo[EventIdKeyL], pod.eventId); + if (pod.type == EventRelation::AnnotationType) + fromJson(jo["key"_ls], pod.key); + } +} diff --git a/lib/events/eventrelation.h b/lib/events/eventrelation.h new file mode 100644 index 00000000..e445ee42 --- /dev/null +++ b/lib/events/eventrelation.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +namespace Quotient { + +[[maybe_unused]] constexpr auto RelatesToKey = "m.relates_to"_ls; +constexpr auto RelTypeKey = "rel_type"_ls; + +struct QUOTIENT_API EventRelation { + using reltypeid_t = QLatin1String; + + QString type; + QString eventId; + QString key = {}; // Only used for m.annotation for now + + static constexpr auto ReplyType = "m.in_reply_to"_ls; + static constexpr auto AnnotationType = "m.annotation"_ls; + static constexpr auto ReplacementType = "m.replace"_ls; + + static EventRelation replyTo(QString eventId) + { + return { ReplyType, std::move(eventId) }; + } + static EventRelation annotate(QString eventId, QString key) + { + return { AnnotationType, std::move(eventId), std::move(key) }; + } + static EventRelation replace(QString eventId) + { + return { ReplacementType, std::move(eventId) }; + } + + [[deprecated("Use ReplyRelation variable instead")]] + static constexpr auto Reply() { return ReplyType; } + [[deprecated("Use AnnotationRelation variable instead")]] // + static constexpr auto Annotation() { return AnnotationType; } + [[deprecated("Use ReplacementRelation variable instead")]] // + static constexpr auto Replacement() { return ReplacementType; } +}; + +template <> +struct QUOTIENT_API JsonObjectConverter<EventRelation> { + static void dumpTo(QJsonObject& jo, const EventRelation& pod); + static void fillFrom(const QJsonObject& jo, EventRelation& pod); +}; + +} + diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp deleted file mode 100644 index b53fffd6..00000000 --- a/lib/events/reactionevent.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "reactionevent.h" - -using namespace Quotient; - -void JsonObjectConverter<EventRelation>::dumpTo( - QJsonObject& jo, const EventRelation& pod) -{ - if (pod.type.isEmpty()) { - qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; - return; - } - jo.insert(QStringLiteral("rel_type"), pod.type); - jo.insert(EventIdKey, pod.eventId); - if (pod.type == EventRelation::Annotation()) - jo.insert(QStringLiteral("key"), pod.key); -} - -void JsonObjectConverter<EventRelation>::fillFrom( - const QJsonObject& jo, EventRelation& pod) -{ - // The experimental logic for generic relationships (MSC1849) - fromJson(jo["rel_type"_ls], pod.type); - fromJson(jo[EventIdKeyL], pod.eventId); - if (pod.type == EventRelation::Annotation()) - fromJson(jo["key"_ls], pod.key); -} diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 5a2b98c4..b3cb3ca7 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -4,39 +4,11 @@ #pragma once #include "roomevent.h" +#include "eventrelation.h" namespace Quotient { -struct EventRelation { - using reltypeid_t = const char*; - static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } - static constexpr reltypeid_t Annotation() { return "m.annotation"; } - static constexpr reltypeid_t Replacement() { return "m.replace"; } - - QString type; - QString eventId; - QString key = {}; // Only used for m.annotation for now - - static EventRelation replyTo(QString eventId) - { - return { Reply(), std::move(eventId) }; - } - static EventRelation annotate(QString eventId, QString key) - { - return { Annotation(), std::move(eventId), std::move(key) }; - } - static EventRelation replace(QString eventId) - { - return { Replacement(), std::move(eventId) }; - } -}; -template <> -struct JsonObjectConverter<EventRelation> { - static void dumpTo(QJsonObject& jo, const EventRelation& pod); - static void fillFrom(const QJsonObject& jo, EventRelation& pod); -}; - -class ReactionEvent : public RoomEvent { +class QUOTIENT_API ReactionEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent) @@ -47,7 +19,7 @@ public: explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} EventRelation relation() const { - return contentPart<EventRelation>("m.relates_to"_ls); + return contentPart<EventRelation>(RelatesToKey); } }; REGISTER_EVENT_TYPE(ReactionEvent) diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index 72dbf2e3..7f06d99f 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -20,7 +20,6 @@ Example of a Receipt Event: #include "receiptevent.h" -#include "converters.h" #include "logging.h" using namespace Quotient; diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index 9683deef..5e077e47 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -19,7 +19,7 @@ struct ReceiptsForEvent { }; using EventsWithReceipts = QVector<ReceiptsForEvent>; -class ReceiptEvent : public Event { +class QUOTIENT_API ReceiptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) explicit ReceiptEvent(const EventsWithReceipts& ewrs); diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 8618ba31..c54b5801 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -7,7 +7,8 @@ #include "stateevent.h" namespace Quotient { -class RoomAvatarEvent : public StateEvent<EventContent::ImageContent> { +class QUOTIENT_API RoomAvatarEvent + : public StateEvent<EventContent::ImageContent> { // It's a bit of an overkill to use a full-fledged ImageContent // because in reality m.room.avatar usually only has a single URL, // without a thumbnail. But The Spec says there be thumbnails, and diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index b3ad287c..989030ac 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -7,11 +7,10 @@ #include "quotient_common.h" namespace Quotient { -class RoomCreateEvent : public StateEventBase { +class QUOTIENT_API RoomCreateEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) - explicit RoomCreateEvent() : StateEventBase(typeId(), matrixTypeId()) {} explicit RoomCreateEvent(const QJsonObject& obj) : StateEventBase(typeId(), obj) {} diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index eb5d0485..2f482871 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -3,15 +3,11 @@ #include "roomevent.h" -#include "converters.h" #include "logging.h" #include "redactionevent.h" using namespace Quotient; -[[maybe_unused]] static auto roomEventTypeInitialised = - Event::factory_t::chainFactory<RoomEvent>(); - RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) : Event(type, matrixType, contentJson) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 3d46bf9b..c4b0131a 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -11,9 +11,9 @@ namespace Quotient { class RedactionEvent; /** This class corresponds to m.room.* events */ -class RoomEvent : public Event { +class QUOTIENT_API RoomEvent : public Event { public: - using factory_t = EventFactory<RoomEvent>; + static inline EventFactory<RoomEvent> factory { "RoomEvent" }; // RedactionEvent is an incomplete type here so we cannot inline // constructors and destructors and we cannot use 'using'. @@ -80,7 +80,7 @@ using RoomEventPtr = event_ptr_tt<RoomEvent>; using RoomEvents = EventsArray<RoomEvent>; using RoomEventsRange = Range<RoomEvents>; -class CallEventBase : public RoomEvent { +class QUOTIENT_API CallEventBase : public RoomEvent { public: CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson = {}); diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index d021fbec..c4df7936 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -6,7 +6,7 @@ #include "event.h" namespace Quotient { -class RoomKeyEvent : public Event +class QUOTIENT_API RoomKeyEvent : public Event { public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index b0bc7bcb..b4770224 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -4,7 +4,6 @@ #include "roommemberevent.h" -#include "converters.h" #include "logging.h" #include <QtCore/QtAlgorithms> @@ -48,11 +47,9 @@ void MemberEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); if (membership != Membership::Invalid) - o->insert( - QStringLiteral("membership"), - MembershipStrings[qCountTrailingZeroBits( - std::underlying_type_t<Membership>(membership)) - + 1]); + o->insert(QStringLiteral("membership"), + MembershipStrings[qCountTrailingZeroBits( + std::underlying_type_t<Membership>(membership))]); if (displayName) o->insert(QStringLiteral("displayname"), *displayName); if (avatarUrl && avatarUrl->isValid()) diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index f3047159..ceb7826b 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -10,14 +10,12 @@ #include "quotient_common.h" namespace Quotient { -class MemberEventContent : public EventContent::Base { +class QUOTIENT_API MemberEventContent : public EventContent::Base { public: using MembershipType [[deprecated("Use Quotient::Membership instead")]] = Membership; - explicit MemberEventContent(Membership ms = Membership::Join) - : membership(ms) - {} + QUO_IMPLICIT MemberEventContent(Membership ms) : membership(ms) {} explicit MemberEventContent(const QJsonObject& json); Membership membership; @@ -33,7 +31,7 @@ protected: using MembershipType [[deprecated("Use Membership instead")]] = Membership; -class RoomMemberEvent : public StateEvent<MemberEventContent> { +class QUOTIENT_API RoomMemberEvent : public StateEvent<MemberEventContent> { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) @@ -43,22 +41,19 @@ public: explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - template <typename... ArgTs> - RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs) - : StateEvent(typeId(), matrixTypeId(), userId, - std::forward<ArgTs>(contentArgs)...) + RoomMemberEvent(const QString& userId, MemberEventContent&& content) + : StateEvent(typeId(), matrixTypeId(), userId, std::move(content)) {} - /// 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 - */ + //! \brief 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 have a specialisation of + //! EventFactory\<> and be constructible with unknownTypeId() instead of + //! its genuine id. Don't use directly. + //! \sa EventFactory, loadEvent, GetMembersByRoomJob RoomMemberEvent(Type type, const QJsonObject& fullJson) : StateEvent(type, fullJson) {} @@ -89,14 +84,12 @@ public: }; template <> -class EventFactory<RoomMemberEvent> { -public: - static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json, - const QString&) - { +inline event_ptr_tt<RoomMemberEvent> +doLoadEvent<RoomMemberEvent>(const QJsonObject& json, const QString& matrixType) +{ + if (matrixType == QLatin1String(RoomMemberEvent::matrixTypeId())) return makeEvent<RoomMemberEvent>(json); - } -}; - + return makeEvent<RoomMemberEvent>(unknownEventTypeId(), json); +} REGISTER_EVENT_TYPE(RoomMemberEvent) } // namespace Quotient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 2b7b4166..d63352cb 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -6,6 +6,7 @@ #include "roommessageevent.h" #include "logging.h" +#include "events/eventrelation.h" #include <QtCore/QFileInfo> #include <QtCore/QMimeDatabase> @@ -19,15 +20,14 @@ using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; -static const auto RelatesToKeyL = "m.relates_to"_ls; -static const auto MsgTypeKeyL = "msgtype"_ls; -static const auto FormattedBodyKeyL = "formatted_body"_ls; - -static const auto TextTypeKey = "m.text"; -static const auto EmoteTypeKey = "m.emote"; -static const auto NoticeTypeKey = "m.notice"; - -static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); +namespace { // Supporting internal definitions +constexpr auto RelatesToKey = "m.relates_to"_ls; +constexpr auto MsgTypeKey = "msgtype"_ls; +constexpr auto FormattedBodyKey = "formatted_body"_ls; +constexpr auto TextTypeKey = "m.text"_ls; +constexpr auto EmoteTypeKey = "m.emote"_ls; +constexpr auto NoticeTypeKey = "m.notice"_ls; +constexpr auto HtmlContentTypeId = "org.matrix.custom.html"_ls; template <typename ContentT> TypedBase* make(const QJsonObject& json) @@ -38,13 +38,13 @@ TypedBase* make(const QJsonObject& json) template <> TypedBase* make<TextContent>(const QJsonObject& json) { - return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL) + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) ? new TextContent(json) : nullptr; } struct MsgTypeDesc { - QString matrixType; + QLatin1String matrixType; MsgType enumType; TypedBase* (*maker)(const QJsonObject&); }; @@ -53,11 +53,11 @@ const std::vector<MsgTypeDesc> msgTypes = { { TextTypeKey, MsgType::Text, make<TextContent> }, { EmoteTypeKey, MsgType::Emote, 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> }, - { QStringLiteral("m.video"), MsgType::Video, make<VideoContent> }, - { QStringLiteral("m.audio"), MsgType::Audio, make<AudioContent> } + { "m.image"_ls, MsgType::Image, make<ImageContent> }, + { "m.file"_ls, MsgType::File, make<FileContent> }, + { "m.location"_ls, MsgType::Location, make<LocationContent> }, + { "m.video"_ls, MsgType::Video, make<VideoContent> }, + { "m.audio"_ls, MsgType::Audio, make<AudioContent> } }; QString msgTypeToJson(MsgType enumType) @@ -84,41 +84,44 @@ MsgType jsonToMsgType(const QString& matrixType) return MsgType::Unknown; } -inline bool isReplacement(const Omittable<RelatesTo>& rel) +inline bool isReplacement(const Omittable<EventRelation>& rel) { - return rel && rel->type == RelatesTo::ReplacementTypeId(); + return rel && rel->type == EventRelation::ReplacementType; } +} // anonymous namespace + QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { - auto json = content ? content->toJson() : QJsonObject(); - if (json.contains(RelatesToKeyL)) { + QJsonObject json; + if (content) { + // TODO: replace with content->fillJson(json) when it starts working + json = content->toJson(); if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && jsonMsgType != EmoteTypeKey) { - json.remove(RelatesToKeyL); - qCWarning(EVENTS) - << RelatesToKeyL << "cannot be used in" << jsonMsgType - << "messages; the relation has been stripped off"; - } else { - // After the above, we know for sure that the content is TextContent - // and that its RelatesTo structure is not omitted - auto* textContent = static_cast<const TextContent*>(content); - Q_ASSERT(textContent && textContent->relatesTo.has_value()); - if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { - auto newContentJson = json.take("m.new_content"_ls).toObject(); - newContentJson.insert(BodyKey, plainBody); - newContentJson.insert(MsgTypeKeyL, jsonMsgType); - json.insert(QStringLiteral("m.new_content"), newContentJson); - json[MsgTypeKeyL] = jsonMsgType; - json[BodyKeyL] = "* " + plainBody; - return json; + if (json.contains(RelatesToKey)) { + json.remove(RelatesToKey); + qCWarning(EVENTS) + << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; } + } else if (auto* textContent = static_cast<const TextContent*>(content); + textContent->relatesTo + && textContent->relatesTo->type + == EventRelation::ReplacementType) { + auto newContentJson = json.take("m.new_content"_ls).toObject(); + newContentJson.insert(BodyKey, plainBody); + newContentJson.insert(MsgTypeKey, jsonMsgType); + json.insert(QStringLiteral("m.new_content"), newContentJson); + json[MsgTypeKey] = jsonMsgType; + json[BodyKeyL] = "* " + plainBody; + return json; } } - json.insert(QStringLiteral("msgtype"), jsonMsgType); - json.insert(QStringLiteral("body"), plainBody); + json.insert(MsgTypeKey, jsonMsgType); + json.insert(BodyKey, plainBody); return json; } @@ -177,8 +180,8 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if (isRedacted()) return; const QJsonObject content = contentJson(); - if (content.contains(MsgTypeKeyL) && content.contains(BodyKeyL)) { - auto msgtype = content[MsgTypeKeyL].toString(); + if (content.contains(MsgTypeKey) && content.contains(BodyKeyL)) { + auto msgtype = content[MsgTypeKey].toString(); bool msgTypeFound = false; for (const auto& mt : msgTypes) if (mt.matrixType == msgtype) { @@ -204,7 +207,7 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QString RoomMessageEvent::rawMsgtype() const { - return contentPart<QString>(MsgTypeKeyL); + return contentPart<QString>(MsgTypeKey); } QString RoomMessageEvent::plainBody() const @@ -267,7 +270,7 @@ QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) } TextContent::TextContent(QString text, const QString& contentType, - Omittable<RelatesTo> relatesTo) + Omittable<EventRelation> relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)) , body(std::move(text)) , relatesTo(std::move(relatesTo)) @@ -276,26 +279,8 @@ TextContent::TextContent(QString text, const QString& contentType, mimeType = QMimeDatabase().mimeTypeForName("text/html"); } -namespace Quotient { -// Overload the default fromJson<> logic that defined in converters.h -// as we want -template <> -Omittable<RelatesTo> fromJson(const QJsonValue& jv) -{ - const auto jo = jv.toObject(); - if (jo.isEmpty()) - return none; - const auto replyJson = jo.value(RelatesTo::ReplyTypeId()).toObject(); - if (!replyJson.isEmpty()) - return replyTo(fromJson<QString>(replyJson[EventIdKeyL])); - - return RelatesTo { jo.value("rel_type"_ls).toString(), - jo.value(EventIdKeyL).toString() }; -} -} // namespace Quotient - TextContent::TextContent(const QJsonObject& json) - : relatesTo(fromJson<Omittable<RelatesTo>>(json[RelatesToKeyL])) + : relatesTo(fromJson<Omittable<EventRelation>>(json[RelatesToKey])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); @@ -308,7 +293,7 @@ TextContent::TextContent(const QJsonObject& json) // of sending HTML messages. if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = actualJson[FormattedBodyKeyL].toString(); + body = actualJson[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. @@ -320,7 +305,6 @@ TextContent::TextContent(const QJsonObject& json) void TextContent::fillJson(QJsonObject* json) const { static const auto FormatKey = QStringLiteral("format"); - static const auto FormattedBodyKey = QStringLiteral("formatted_body"); Q_ASSERT(json); if (mimeType.inherits("text/html")) { @@ -328,12 +312,15 @@ void TextContent::fillJson(QJsonObject* json) const json->insert(FormattedBodyKey, body); } if (relatesTo) { - json->insert(QStringLiteral("m.relates_to"), - relatesTo->type == RelatesTo::ReplyTypeId() ? - QJsonObject { { relatesTo->type, QJsonObject{ { EventIdKey, relatesTo->eventId } } } } : - QJsonObject { { "rel_type", relatesTo->type }, { EventIdKey, relatesTo->eventId } } - ); - if (relatesTo->type == RelatesTo::ReplacementTypeId()) { + json->insert( + QStringLiteral("m.relates_to"), + relatesTo->type == EventRelation::ReplyType + ? QJsonObject { { relatesTo->type, + QJsonObject { + { EventIdKey, relatesTo->eventId } } } } + : QJsonObject { { RelTypeKey, relatesTo->type }, + { EventIdKey, relatesTo->eventId } }); + if (relatesTo->type == EventRelation::ReplacementType) { QJsonObject newContentJson; if (mimeType.inherits("text/html")) { newContentJson.insert(FormatKey, HtmlContentTypeId); diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 56597ddc..03a51328 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -6,6 +6,7 @@ #pragma once #include "eventcontent.h" +#include "eventrelation.h" #include "roomevent.h" class QFileInfo; @@ -16,7 +17,7 @@ namespace MessageEventContent = EventContent; // Back-compatibility /** * The event class corresponding to m.room.message events */ -class RoomMessageEvent : public RoomEvent { +class QUOTIENT_API RoomMessageEvent : public RoomEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent) @@ -97,40 +98,42 @@ REGISTER_EVENT_TYPE(RoomMessageEvent) using MessageEventType = RoomMessageEvent::MsgType; namespace EventContent { - // Additional event content types - struct RelatesTo { - static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } - static constexpr const char* ReplacementTypeId() { return "m.replace"; } - QString type; // The only supported relation so far - QString eventId; + struct [[deprecated("Use Quotient::EventRelation instead")]] RelatesTo + : EventRelation { + static constexpr auto ReplyTypeId() { return ReplyType; } + static constexpr auto ReplacementTypeId() { return ReplacementType; } }; - inline RelatesTo replyTo(QString eventId) + [[deprecated("Use EventRelation::replyTo() instead")]] + inline auto replyTo(QString eventId) { - return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + return EventRelation::replyTo(std::move(eventId)); } - inline RelatesTo replacementOf(QString eventId) + [[deprecated("Use EventRelation::replace() instead")]] + inline auto replacementOf(QString eventId) { - return { RelatesTo::ReplacementTypeId(), std::move(eventId) }; + return EventRelation::replace(std::move(eventId)); } + // Additional event content types + /** * Rich text content for m.text, m.emote, m.notice * * Available fields: mimeType, body. The body can be either rich text * or plain text, depending on what mimeType specifies. */ - class TextContent : public TypedBase { + class QUOTIENT_API TextContent : public TypedBase { public: TextContent(QString text, const QString& contentType, - Omittable<RelatesTo> relatesTo = none); + Omittable<EventRelation> relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; - Omittable<RelatesTo> relatesTo; + Omittable<EventRelation> relatesTo; protected: void fillJson(QJsonObject* json) const override; @@ -149,7 +152,7 @@ namespace EventContent { * - thumbnail.mimeType * - thumbnail.imageSize */ - class LocationContent : public TypedBase { + class QUOTIENT_API LocationContent : public TypedBase { public: LocationContent(const QString& geoUri, const Thumbnail& thumbnail = {}); explicit LocationContent(const QJsonObject& json); @@ -168,7 +171,7 @@ namespace EventContent { * A base class for info types that include duration: audio and video */ template <typename ContentT> - class PlayableContent : public ContentT { + class QUOTIENT_API PlayableContent : public ContentT { public: using ContentT::ContentT; PlayableContent(const QJsonObject& json) diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index 0346fc0d..415cc814 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -7,7 +7,7 @@ #include "stateevent.h" namespace Quotient { -class PowerLevelsEventContent : public EventContent::Base { +class QUOTIENT_API PowerLevelsEventContent : public EventContent::Base { public: struct Notifications { int room; @@ -34,11 +34,14 @@ protected: void fillJson(QJsonObject* o) const override; }; -class RoomPowerLevelsEvent : public StateEvent<PowerLevelsEventContent> { - Q_GADGET +class QUOTIENT_API RoomPowerLevelsEvent + : public StateEvent<PowerLevelsEventContent> { public: DEFINE_EVENT_TYPEID("m.room.power_levels", RoomPowerLevelsEvent) + explicit RoomPowerLevelsEvent(PowerLevelsEventContent&& content) + : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) + {} explicit RoomPowerLevelsEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} @@ -61,9 +64,6 @@ public: int powerLevelForEvent(const QString& eventId) const; int powerLevelForState(const QString& eventId) const; int powerLevelForUser(const QString& userId) const; - -private: }; - REGISTER_EVENT_TYPE(RoomPowerLevelsEvent) } // namespace Quotient diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index 30e53738..15d26923 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -6,11 +6,10 @@ #include "stateevent.h" namespace Quotient { -class RoomTombstoneEvent : public StateEventBase { +class QUOTIENT_API RoomTombstoneEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) - explicit RoomTombstoneEvent() : StateEventBase(typeId(), matrixTypeId()) {} explicit RoomTombstoneEvent(const QJsonObject& obj) : StateEventBase(typeId(), obj) {} diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index cf1bfbba..9610574b 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -8,8 +8,7 @@ namespace Quotient { namespace EventContent { template <typename T> - class SimpleContent { - public: + struct SimpleContent { using value_type = T; // The constructor is templated to enable perfect forwarding @@ -25,20 +24,17 @@ namespace EventContent { return { { key, Quotient::toJson(value) } }; } - public: T value; - - protected: - QString key; + const QString key; }; } // namespace EventContent #define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ - class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> { \ + class QUOTIENT_API _Name \ + : public StateEvent<EventContent::SimpleContent<_ValueType>> { \ public: \ using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name() : _Name(value_type()) {} \ template <typename T> \ explicit _Name(T&& value) \ : StateEvent(typeId(), matrixTypeId(), QString(), \ @@ -55,6 +51,7 @@ namespace EventContent { DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) +DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) class [[deprecated( "m.room.aliases events are deprecated by the Matrix spec; use" @@ -65,10 +62,6 @@ public: explicit RoomAliasesEvent(const QJsonObject& obj) : StateEvent(typeId(), obj, QStringLiteral("aliases")) {} - RoomAliasesEvent(const QString& server, const QStringList& aliases) - : StateEvent(typeId(), matrixTypeId(), server, - QStringLiteral("aliases"), aliases) - {} QString server() const { return stateKey(); } QStringList aliases() const { return content().value; } }; diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index efe011a0..e53d47d4 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -5,20 +5,13 @@ using namespace Quotient; -// 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. -[[maybe_unused]] static auto stateEventTypeInitialised = - RoomEvent::factory_t::addMethod( - [](const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains(StateKeyKeyL)) - return nullptr; - - if (auto e = StateEventBase::factory_t::make(json, matrixType)) - return e; - - return makeEvent<StateEventBase>(unknownEventTypeId(), json); - }); +StateEventBase::StateEventBase(Type type, const QJsonObject& json) + : RoomEvent(json.contains(StateKeyKeyL) ? type : unknownEventTypeId(), json) +{ + if (Event::type() == unknownEventTypeId() && !json.contains(StateKeyKeyL)) + qWarning(EVENTS) << "Attempt to create a state event with no stateKey -" + "forcing the event type to unknown to avoid damage"; +} StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index b0aa9907..88da68f8 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -17,12 +17,11 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, { ContentKey, content } }; } -class StateEventBase : public RoomEvent { +class QUOTIENT_API StateEventBase : public RoomEvent { public: - using factory_t = EventFactory<StateEventBase>; + static inline EventFactory<StateEventBase> factory { "StateEvent" }; - StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) - {} + StateEventBase(Type type, const QJsonObject& json); StateEventBase(Type type, event_mtype_t matrixType, const QString& stateKey = {}, const QJsonObject& contentJson = {}); @@ -37,6 +36,22 @@ public: using StateEventPtr = event_ptr_tt<StateEventBase>; using StateEvents = EventsArray<StateEventBase>; +//! \brief Override RoomEvent factory with that from StateEventBase if JSON has +//! stateKey +//! +//! This means in particular that an event with a type known to RoomEvent but +//! having stateKey set (even to an empty value) will be treated as a state +//! event and most likely end up as unknown (consider, e.g., m.room.message +//! that has stateKey set). +template <> +inline RoomEventPtr doLoadEvent(const QJsonObject& json, + const QString& matrixType) +{ + if (json.contains(StateKeyKeyL)) + return StateEventBase::factory.loadEvent(json, matrixType); + return RoomEvent::factory.loadEvent(json, matrixType); +} + template <> inline bool is<StateEventBase>(const Event& e) { diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h index 93671086..0957dca3 100644 --- a/lib/events/stickerevent.h +++ b/lib/events/stickerevent.h @@ -11,7 +11,7 @@ namespace Quotient { /// Sticker messages are specialised image messages that are displayed without /// controls (e.g. no "download" link, or light-box view on click, as would be /// displayed for for m.image events). -class StickerEvent : public RoomEvent +class QUOTIENT_API StickerEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.sticker", StickerEvent) diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 7456100a..522f7e42 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -6,7 +6,7 @@ #include "event.h" namespace Quotient { -class TypingEvent : public Event { +class QUOTIENT_API TypingEvent : public Event { public: DEFINE_EVENT_TYPEID("m.typing", TypingEvent) diff --git a/lib/eventstats.h b/lib/eventstats.h index 77c661a7..a10c81fb 100644 --- a/lib/eventstats.h +++ b/lib/eventstats.h @@ -16,7 +16,7 @@ namespace Quotient { //! \note It's just a simple grouping of counters and is not automatically //! updated from the room as subsequent syncs arrive. //! \sa Room::unreadStats, Room::partiallyReadStats, Room::isEventNotable -struct EventStats { +struct QUOTIENT_API EventStats { Q_GADGET Q_PROPERTY(qsizetype notableCount MEMBER notableCount CONSTANT) Q_PROPERTY(qsizetype highlightCount MEMBER highlightCount CONSTANT) @@ -109,6 +109,6 @@ public: bool isValidFor(const Room* room, const marker_t& marker) const; }; -QDebug operator<<(QDebug dbg, const EventStats& es); +QUOTIENT_API QDebug operator<<(QDebug dbg, const EventStats& es); } diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp new file mode 100644 index 00000000..6542101a --- /dev/null +++ b/lib/function_traits.cpp @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "function_traits.h" + +// Tests for function_traits<> + +using namespace Quotient; + +int f_(); +static_assert(std::is_same_v<fn_return_t<decltype(f_)>, int>, + "Test fn_return_t<>"); + +void f1_(int, float); +static_assert(std::is_same_v<fn_arg_t<decltype(f1_), 1>, float>, + "Test fn_arg_t<>"); + +struct Fo { + int operator()(); + static constexpr auto l = [] { return 0.0f; }; + bool memFn(); + void constMemFn() const&; + double field; + const double field2; +}; +static_assert(std::is_same_v<fn_return_t<Fo>, int>, + "Test return type of function object"); +static_assert(std::is_same_v<fn_return_t<decltype(Fo::l)>, float>, + "Test return type of lambda"); +static_assert(std::is_same_v<fn_arg_t<decltype(&Fo::memFn)>, Fo>, + "Test first argument type of member function"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::memFn)>, bool>, + "Test return type of member function"); +static_assert(std::is_same_v<fn_arg_t<decltype(&Fo::constMemFn)>, const Fo&>, + "Test first argument type of const member function"); +static_assert(std::is_void_v<fn_return_t<decltype(&Fo::constMemFn)>>, + "Test return type of const member function"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::field)>, double&>, + "Test return type of a class member"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::field2)>, const double&>, + "Test return type of a const class member"); + +struct Fo1 { + void operator()(int); +}; +static_assert(std::is_same_v<fn_arg_t<Fo1>, int>, + "Test fn_arg_t defaulting to first argument"); + +template <typename T> +[[maybe_unused]] static void ft(const std::vector<T>&); +static_assert( + std::is_same<fn_arg_t<decltype(ft<double>)>, const std::vector<double>&>(), + "Test function templates"); diff --git a/lib/function_traits.h b/lib/function_traits.h new file mode 100644 index 00000000..83b8e425 --- /dev/null +++ b/lib/function_traits.h @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <functional> + +namespace Quotient { + +namespace _impl { + template <typename AlwaysVoid, typename> + struct fn_traits {}; +} + +/// Determine traits of an arbitrary function/lambda/functor +/*! + * 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 _impl::fn_traits<void, std::remove_reference_t<T>> {}; + +// Specialisation for a function +template <typename ReturnT, typename... ArgTs> +struct function_traits<ReturnT(ArgTs...)> { + using return_type = ReturnT; + using arg_types = std::tuple<ArgTs...>; + // See also the comment for wrap_in_function() in qt_connection_util.h + using function_type = std::function<ReturnT(ArgTs...)>; +}; + +namespace _impl { + template <typename AlwaysVoid, typename> + struct fn_object_traits; + + // Specialisation for a lambda function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for a const lambda function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...) const> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for function objects with (non-overloaded) operator() + // (this includes non-generic lambdas) + template <typename T> + struct fn_traits<decltype(void(&T::operator())), T> + : public fn_object_traits<void, decltype(&T::operator())> {}; + + // Specialisation for a member function in a non-functor class + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ClassT, 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(const ClassT&, ArgTs...)> {}; + + // Specialisation for a constref member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const&> + : function_traits<ReturnT(const ClassT&, ArgTs...)> {}; + + // Specialisation for a prvalue member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) &&> + : function_traits<ReturnT(ClassT&&, ArgTs...)> {}; + + // Specialisation for a pointer-to-member + template <typename ReturnT, typename ClassT> + struct fn_traits<void, ReturnT ClassT::*> + : function_traits<ReturnT&(ClassT)> {}; + + // Specialisation for a const pointer-to-member + template <typename ReturnT, typename ClassT> + struct fn_traits<void, const ReturnT ClassT::*> + : function_traits<const ReturnT&(ClassT)> {}; +} // namespace _impl + +template <typename FnT> +using fn_return_t = typename function_traits<FnT>::return_type; + +template <typename FnT, int ArgN = 0> +using fn_arg_t = + std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; + +} // namespace Quotient diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 971fea7b..b6858b5a 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -5,11 +5,9 @@ #include "basejob.h" #include "connectiondata.h" -#include "quotient_common.h" #include <QtCore/QRegularExpression> #include <QtCore/QTimer> -#include <QtCore/QStringBuilder> #include <QtCore/QMetaEnum> #include <QtCore/QPointer> #include <QtNetwork/QNetworkAccessManager> @@ -194,8 +192,8 @@ BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, const QUrlQuery& query, RequestData&& data, bool needsToken) - : d(new Private(verb, std::move(endpoint), query, std::move(data), - needsToken)) + : d(makeImpl<Private>(verb, std::move(endpoint), query, std::move(data), + needsToken)) { setObjectName(name); connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index ddf243ed..555c602b 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -5,9 +5,9 @@ #pragma once #include "requestdata.h" -#include "../logging.h" -#include "../converters.h" -#include "../quotient_common.h" +#include "logging.h" +#include "converters.h" // Common for csapi/ headers even though not used here +#include "quotient_common.h" // For DECL_DEPRECATED_ENUMERATOR #include <QtCore/QObject> #include <QtCore/QStringBuilder> @@ -20,7 +20,7 @@ class ConnectionData; enum class HttpVerb { Get, Put, Post, Delete }; -class BaseJob : public QObject { +class QUOTIENT_API BaseJob : public QObject { Q_OBJECT Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) @@ -28,7 +28,7 @@ class BaseJob : public QObject { static QByteArray encodeIfParam(const QString& paramPart); template <int N> - static inline auto encodeIfParam(const char (&constPart)[N]) + static auto encodeIfParam(const char (&constPart)[N]) { return constPart; } @@ -248,7 +248,7 @@ public: } public Q_SLOTS: - void initiate(ConnectionData* connData, bool inBackground); + void initiate(Quotient::ConnectionData* connData, bool inBackground); /** * Abandons the result of this job, arrived or unarrived. @@ -467,10 +467,10 @@ private: void finishJob(); class Private; - QScopedPointer<Private> d; + ImplPtr<Private> d; }; -inline bool isJobPending(BaseJob* job) +inline bool QUOTIENT_API isJobPending(BaseJob* job) { return job && job->error() == BaseJob::Pending; } diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index c5280770..634e5fb9 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -40,7 +40,8 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename) : GetContentJob(serverName, mediaId) - , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) + , d(localFilename.isEmpty() ? makeImpl<Private>() + : makeImpl<Private>(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); } @@ -51,7 +52,8 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, const EncryptedFile& file, const QString& localFilename) : GetContentJob(serverName, mediaId) - , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) + , d(localFilename.isEmpty() ? makeImpl<Private>() + : makeImpl<Private>(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); d->encryptedFile = file; diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index 90d80478..ffa3d055 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -7,7 +7,7 @@ #include "events/encryptedfile.h" namespace Quotient { -class DownloadFileJob : public GetContentJob { +class QUOTIENT_API DownloadFileJob : public GetContentJob { public: using GetContentJob::makeRequestUrl; static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); @@ -22,7 +22,7 @@ public: private: class Private; - QScopedPointer<Private> d; + ImplPtr<Private> d; void doPrepare() override; void onSentRequest(QNetworkReply* reply) override; diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h index 3183feb1..c9f6da35 100644 --- a/lib/jobs/mediathumbnailjob.h +++ b/lib/jobs/mediathumbnailjob.h @@ -8,7 +8,7 @@ #include <QtGui/QPixmap> namespace Quotient { -class MediaThumbnailJob : public GetContentThumbnailJob { +class QUOTIENT_API MediaThumbnailJob : public GetContentThumbnailJob { public: using GetContentThumbnailJob::makeRequestUrl; static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 4f05e5ff..41ad833a 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -3,6 +3,8 @@ #pragma once +#include "quotient_export.h" + #include <QtCore/QByteArray> #include <memory> @@ -19,7 +21,7 @@ namespace Quotient { * as well as JSON (and possibly other structures in the future) to * a QByteArray consumed by QNetworkAccessManager request methods. */ -class RequestData { +class QUOTIENT_API RequestData { public: RequestData(const QByteArray& a = {}); RequestData(const QJsonObject& jo); diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 9b1b46f0..f5c632bf 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -37,10 +37,12 @@ SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout, BaseJob::Status SyncJob::prepareResult() { d.parseJson(jsonData()); - if (d.unresolvedRooms().isEmpty()) + if (Q_LIKELY(d.unresolvedRooms().isEmpty())) return Success; - qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" + Q_ASSERT(d.unresolvedRooms().isEmpty()); + qCCritical(MAIN).noquote() << "Rooms missing after processing sync " + "response, possibly a bug in SyncData: " << d.unresolvedRooms().join(','); return IncorrectResponse; } diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp index c666cce3..1d40c5e1 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -26,7 +26,7 @@ public: }; MxcReply::MxcReply(QNetworkReply* reply) - : d(std::make_unique<Private>(reply)) + : d(makeImpl<Private>(reply)) { d->m_device = d->m_reply; reply->setParent(this); @@ -38,7 +38,7 @@ MxcReply::MxcReply(QNetworkReply* reply) } MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) - : d(std::make_unique<Private>(reply)) + : d(makeImpl<Private>(reply)) { reply->setParent(this); connect(d->m_reply, &QNetworkReply::finished, this, [this]() { @@ -77,6 +77,7 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) #endif MxcReply::MxcReply() + : d(ZeroImpl<Private>()) { static const auto BadRequestPhrase = tr("Bad Request"); QMetaObject::invokeMethod(this, [this]() { diff --git a/lib/mxcreply.h b/lib/mxcreply.h index efaf01c6..f6c4a34d 100644 --- a/lib/mxcreply.h +++ b/lib/mxcreply.h @@ -3,14 +3,16 @@ #pragma once +#include "util.h" + #include <QtNetwork/QNetworkReply> -#include <memory> namespace Quotient { class Room; -class MxcReply : public QNetworkReply +class QUOTIENT_API MxcReply : public QNetworkReply { + Q_OBJECT public: explicit MxcReply(); explicit MxcReply(QNetworkReply *reply); @@ -24,6 +26,6 @@ protected: private: class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index d0380cec..f4e7b1af 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -39,7 +39,7 @@ public: }; NetworkAccessManager::NetworkAccessManager(QObject* parent) - : QNetworkAccessManager(parent), d(std::make_unique<Private>(this)) + : QNetworkAccessManager(parent), d(makeImpl<Private>(this)) {} QList<QSslError> NetworkAccessManager::ignoredSslErrors() const @@ -90,8 +90,6 @@ NetworkAccessManager* NetworkAccessManager::instance() return storage.localData(); } -NetworkAccessManager::~NetworkAccessManager() = default; - QNetworkReply* NetworkAccessManager::createRequest( Operation op, const QNetworkRequest& request, QIODevice* outgoingData) { @@ -110,7 +108,7 @@ QNetworkReply* NetworkAccessManager::createRequest( // TODO: Make the best effort with a direct unauthenticated request // to the media server } else { - auto* const connection = AccountRegistry::instance().get(accountId); + auto* const connection = Accounts.get(accountId); if (!connection) { qCWarning(NETWORK) << "Connection" << accountId << "not found"; return new MxcReply(); diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index efa41994..01b0599d 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -3,18 +3,16 @@ #pragma once -#include <QtNetwork/QNetworkAccessManager> +#include "util.h" -#include <memory> +#include <QtNetwork/QNetworkAccessManager> namespace Quotient { -class Room; -class NetworkAccessManager : public QNetworkAccessManager { +class QUOTIENT_API NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: NetworkAccessManager(QObject* parent = nullptr); - ~NetworkAccessManager() override; QList<QSslError> ignoredSslErrors() const; void addIgnoredSslError(const QSslError& error); @@ -32,6 +30,6 @@ private: QIODevice* outgoingData = Q_NULLPTR) override; class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } // namespace Quotient diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index ce46ce5f..06b1fdf9 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -11,9 +11,9 @@ void NetworkSettings::setupApplicationProxy() const { proxyType(), proxyHostName(), proxyPort() }); } -QTNT_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, +QUO_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QTNT_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", +QUO_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName) -QTNT_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, +QUO_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) diff --git a/lib/networksettings.h b/lib/networksettings.h index df11a9c8..44247e59 100644 --- a/lib/networksettings.h +++ b/lib/networksettings.h @@ -10,11 +10,11 @@ Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) namespace Quotient { -class NetworkSettings : public SettingsGroup { +class QUOTIENT_API NetworkSettings : public SettingsGroup { Q_OBJECT - QTNT_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) - QTNT_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) - QTNT_DECLARE_SETTING(quint16, proxyPort, setProxyPort) + QUO_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) + QUO_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) + QUO_DECLARE_SETTING(quint16, proxyPort, setProxyPort) Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) public: template <typename... ArgTs> diff --git a/lib/omittable.h b/lib/omittable.h new file mode 100644 index 00000000..b5efecf5 --- /dev/null +++ b/lib/omittable.h @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <optional> +#include <functional> + +namespace Quotient { + +template <typename T> +class Omittable; + +constexpr auto none = std::nullopt; + +//! \brief Lift an operation into dereferenceable types (Omittables or pointers) +//! +//! This is a more generic version of Omittable::then() that extends to +//! an arbitrary number of arguments of any type that is dereferenceable (unary +//! operator*() can be applied to it) and (explicitly or implicitly) convertible +//! to bool. This allows to streamline checking for nullptr/none before applying +//! the operation on the underlying types. \p fn is only invoked if all \p args +//! are "truthy" (i.e. <tt>(... && bool(args)) == true</tt>). +//! \param fn A callable that should accept the types stored inside +//! Omittables/pointers passed in \p args +//! \return Always an Omittable: if \p fn returns another type, lift() wraps +//! it in an Omittable; if \p fn returns an Omittable, that return value +//! (or none) is returned as is. +template <typename FnT, typename... MaybeTs> +inline auto lift(FnT&& fn, MaybeTs&&... args) +{ + return (... && bool(args)) + ? Omittable(std::invoke(std::forward<FnT>(fn), *args...)) + : none; +} + +/** `std::optional` with tweaks + * + * The tweaks are: + * - streamlined assignment (operator=)/emplace()ment of values that can be + * used to implicitly construct the underlying type, including + * direct-list-initialisation, e.g.: + * \code + * struct S { int a; char b; } + * Omittable<S> o; + * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } + * \endcode + * - entirely deleted value(). The technical reason is that Xcode 10 doesn't + * have it; but besides that, value_or() or (after explicit checking) + * `operator*()`/`operator->()` are better alternatives within Quotient + * that doesn't practice throwing exceptions (as doesn't most of Qt). + * - disabled non-const lvalue operator*() and operator->(), as it's too easy + * to inadvertently cause a value change through them. + * - ensure() to provide a safe and explicit lvalue accessor instead of + * those above. Allows chained initialisation of nested Omittables: + * \code + * struct Inner { int member = 10; Omittable<int> innermost; }; + * struct Outer { int anotherMember = 10; Omittable<Inner> inner; }; + * Omittable<Outer> o; // = { 10, std::nullopt }; + * o.ensure().inner.ensure().innermost.emplace(42); + * \endcode + * - merge() - a soft version of operator= that only overwrites its first + * operand with the second one if the second one is not empty. + * - then() and then_or() to streamline read-only interrogation in a "monadic" + * interface. + */ +template <typename T> +class Omittable : public std::optional<T> { +public: + using base_type = std::optional<T>; + using value_type = std::decay_t<T>; + + using std::optional<T>::optional; + + // Overload emplace() and operator=() to allow passing braced-init-lists + // (the standard emplace() does direct-initialisation but + // not direct-list-initialisation). + using base_type::operator=; + Omittable& operator=(const value_type& v) + { + base_type::operator=(v); + return *this; + } + Omittable& operator=(value_type&& v) + { + base_type::operator=(std::move(v)); + return *this; + } + + using base_type::emplace; + T& emplace(const T& val) { return base_type::emplace(val); } + T& emplace(T&& val) { return base_type::emplace(std::move(val)); } + + // Use value_or() or check (with operator! or has_value) before accessing + // with operator-> or operator* + // The technical reason is that Xcode 10 has incomplete std::optional + // that has no value(); but using value() may also mean that you rely + // on the optional throwing an exception (which is not assumed practice + // throughout Quotient) or that you spend unnecessary CPU cycles on + // an extraneous has_value() check. + auto& value() = delete; + const auto& value() const = delete; + + template <typename U> + value_type& ensure(U&& defaultValue = value_type {}) + { + return this->has_value() ? this->operator*() + : this->emplace(std::forward<U>(defaultValue)); + } + value_type& ensure(const value_type& defaultValue) + { + return ensure<>(defaultValue); + } + value_type& ensure(value_type&& defaultValue) + { + return ensure<>(std::move(defaultValue)); + } + + //! 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 std::optional<T1>& other) + -> std::enable_if_t<std::is_convertible_v<T1, T>, bool> + { + if (!other || (this->has_value() && **this == *other)) + return false; + this->emplace(*other); + return true; + } + + // Hide non-const lvalue operator-> and operator* as these are + // a bit too surprising: value() & doesn't lazy-create an object; + // and it's too easy to inadvertently change the underlying value. + + const value_type* operator->() const& { return base_type::operator->(); } + value_type* operator->() && { return base_type::operator->(); } + const value_type& operator*() const& { return base_type::operator*(); } + value_type& operator*() && { return base_type::operator*(); } + + // The below is inspired by the proposed std::optional monadic operations + // (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0798r6.html). + + //! \brief Lift a callable into the Omittable + //! + //! 'Lifting', as used in functional programming, means here invoking + //! a callable (e.g., a function) on the contents of the Omittable if it has + //! any and wrapping the returned value (that may be of a different type T2) + //! into a new Omittable\<T2>. If the current Omittable is empty, + //! the invocation is skipped altogether and Omittable\<T2>{none} is + //! returned instead. + //! \note if \p fn already returns an Omittable (i.e., it is a 'functor', + //! in functional programming terms), then() will not wrap another + //! Omittable around but will just return what \p fn returns. The + //! same doesn't hold for the parameter: if \p fn accepts an Omittable + //! you have to wrap it in another Omittable before calling then(). + //! \return `none` if the current Omittable has `none`; + //! otherwise, the Omittable returned from a call to \p fn + //! \tparam FnT a callable with \p T (or <tt>const T&</tt>) + //! returning Omittable<T2>, T2 is any supported type + //! \sa then_or, transform + template <typename FnT> + auto then(FnT&& fn) const& + { + return lift(std::forward<FnT>(fn), *this); + } + + //! \brief Lift a callable into the rvalue Omittable + //! + //! This is an rvalue overload for then(). + template <typename FnT> + auto then(FnT&& fn) && + { + return lift(std::forward<FnT>(fn), *this); + } + + //! \brief Lift a callable into the const lvalue Omittable, with a fallback + //! + //! This effectively does the same what then() does, except that it returns + //! a value of type returned by the callable, or the provided fallback value + //! if the current Omittable is empty. This is a typesafe version to apply + //! an operation on an Omittable without having to deal with another + //! Omittable afterwards. + template <typename FnT, typename FallbackT> + auto then_or(FnT&& fn, FallbackT&& fallback) const& + { + return then(std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + + //! \brief Lift a callable into the rvalue Omittable, with a fallback + //! + //! This is an overload for functions that accept rvalue + template <typename FnT, typename FallbackT> + auto then_or(FnT&& fn, FallbackT&& fallback) && + { + return then(std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } +}; + +template <typename T> +Omittable(T&&) -> Omittable<T>; + +//! \brief Merge the value from an optional +//! This is an adaptation of Omittable::merge() to the case when the value +//! on the left hand side is not an Omittable. +//! \return true if \p rhs is not omitted and the \p lhs value was different, +//! in other words, if \p lhs has changed; +//! false otherwise +template <typename T1, typename T2> +inline auto merge(T1& lhs, const std::optional<T2>& rhs) + -> std::enable_if_t<std::is_assignable_v<T1&, const T2&>, bool> +{ + if (!rhs || lhs == *rhs) + return false; + lhs = *rhs; + return true; +} + +} // namespace Quotient diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 46294499..86593cc8 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -3,7 +3,7 @@ #pragma once -#include "util.h" +#include "function_traits.h" #include <QtCore/QPointer> diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 0e3e2a40..2b785a39 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -3,31 +3,69 @@ #pragma once +#include "quotient_export.h" + #include <qobjectdefs.h> #include <array> -// See https://bugreports.qt.io/browse/QTBUG-82295 - despite the comment that -// Q_FLAG[_NS] "should" be applied to the enum only, Qt doesn't allow to wrap -// a flag type into a QVariant then. The macros below define Q_FLAG[_NS] and on -// top of that add Q_ENUM[_NS]_IMPL which is a part of Q_ENUM() macro that -// enables the metatype data but goes under the moc radar to avoid double -// registration of the same data in the map defined in moc_*.cpp + +//! \brief Quotient replacement for the Q_FLAG/Q_DECLARE_FLAGS combination +//! +//! Although the comment in QTBUG-82295 says that Q_FLAG[_NS] "should" be +//! applied to the enum type only, Qt then doesn't allow to wrap the +//! corresponding flag type (defined with Q_DECLARE_FLAGS) into a QVariant. +//! This macro defines Q_FLAG and on top of that adds Q_ENUM_IMPL which is +//! a part of Q_ENUM() macro that enables the metatype data but goes under +//! the moc radar to avoid double registration of the same data in the map +//! defined in moc_*.cpp. +//! +//! Simply put, instead of using Q_FLAG/Q_DECLARE_FLAGS combo (and struggling +//! to figure out what you should pass to Q_FLAG if you want to make it +//! wrappable in a QVariant) use the macro below, and things will just work. +//! +//! \sa https://bugreports.qt.io/browse/QTBUG-82295 #define QUO_DECLARE_FLAGS(Flags, Enum) \ Q_DECLARE_FLAGS(Flags, Enum) \ Q_ENUM_IMPL(Enum) \ Q_FLAG(Flags) +//! \brief Quotient replacement for the Q_FLAG_NS/Q_DECLARE_FLAGS combination +//! +//! This is the equivalent of QUO_DECLARE_FLAGS for enums declared at the +//! namespace level (be sure to provide Q_NAMESPACE _in the same file_ +//! as the enum definition and this macro). +//! \sa QUO_DECLARE_FLAGS #define QUO_DECLARE_FLAGS_NS(Flags, Enum) \ Q_DECLARE_FLAGS(Flags, Enum) \ Q_ENUM_NS_IMPL(Enum) \ Q_FLAG_NS(Flags) +// Apple Clang hasn't caught up with explicit(bool) yet +#if __cpp_conditional_explicit >= 201806L +#define QUO_IMPLICIT explicit(false) +#else +#define QUO_IMPLICIT +#endif + #define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) +// The first line forward-declares the namespace static metaobject with +// QUOTIENT_API so that dynamically linked clients could serialise flag/enum +// values from the namespace; Qt before 5.14 doesn't help with that. The second +// line is needed for moc to do its job on the namespace. +#define QUO_NAMESPACE \ + extern QUOTIENT_API const QMetaObject staticMetaObject; \ + Q_NAMESPACE +#else +// Since Qt 5.14.0, it's all packed in a single macro +#define QUO_NAMESPACE Q_NAMESPACE_EXPORT(QUOTIENT_API) +#endif + namespace Quotient { -Q_NAMESPACE +QUO_NAMESPACE // std::array {} needs explicit template parameters on macOS because // Apple stdlib doesn't have deduction guides for std::array. C++20 has @@ -62,7 +100,7 @@ enum class Membership : unsigned int { }; QUO_DECLARE_FLAGS_NS(MembershipMask, Membership) -constexpr inline auto MembershipStrings = make_array( +constexpr auto MembershipStrings = make_array( // The order MUST be the same as the order in the original enum "join", "leave", "invite", "knock", "ban"); @@ -80,7 +118,7 @@ enum class JoinState : std::underlying_type_t<Membership> { }; QUO_DECLARE_FLAGS_NS(JoinStates, JoinState) -constexpr inline auto JoinStateStrings = make_array( +[[maybe_unused]] constexpr auto JoinStateStrings = make_array( MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ ); @@ -110,9 +148,7 @@ enum RoomType { }; Q_ENUM_NS(RoomType) -constexpr inline auto RoomTypeStrings = make_array( - "m.space" -); +[[maybe_unused]] constexpr auto RoomTypeStrings = make_array("m.space"); } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) diff --git a/lib/quotient_export.h b/lib/quotient_export.h new file mode 100644 index 00000000..5a6edb0e --- /dev/null +++ b/lib/quotient_export.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#ifdef QUOTIENT_STATIC +# define QUOTIENT_API +# define QUOTIENT_HIDDEN +#else +# ifndef QUOTIENT_API +# ifdef BUILDING_SHARED_QUOTIENT + /* We are building this library */ +# define QUOTIENT_API Q_DECL_EXPORT +# else + /* We are using this library */ +# define QUOTIENT_API Q_DECL_IMPORT +# endif +# endif + +# ifndef QUOTIENT_HIDDEN +# define QUOTIENT_HIDDEN Q_DECL_HIDDEN +# endif +#endif diff --git a/lib/room.cpp b/lib/room.cpp index 0fc7d23e..ea9915c3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -15,6 +15,7 @@ #include "syncdata.h" #include "user.h" #include "eventstats.h" +#include "roomstateview.h" // NB: since Qt 6, moc_room.cpp needs User fully defined #include "moc_room.cpp" @@ -104,7 +105,7 @@ public: static decltype(baseState) stubbedState; /// The state of the room at syncEdge() /// \sa syncEdge - QHash<StateEventKey, const StateEventBase*> currentState; + RoomStateView currentState; /// Servers with aliases for this room except the one of the local user /// \sa Room::remoteAliases QSet<QString> aliasServers; @@ -220,6 +221,7 @@ public: evtKey.second)); qCDebug(STATE) << "A new stub event created for key {" << evtKey.first << evtKey.second << "}"; + qCDebug(STATE) << "Stubbed state size:" << stubbedState.size(); } evt = stubbedState[evtKey].get(); Q_ASSERT(evt); @@ -229,34 +231,6 @@ public: return evt; } - QVector<const StateEventBase*> stateEventsOfType(const QString& evtType) const - { - auto vals = QVector<const StateEventBase*>(); - for (auto it = currentState.cbegin(); it != currentState.cend(); ++it) - if (it.key().first == evtType) - vals.append(it.value()); - - return vals; - } - - template <typename EventT> - const EventT* getCurrentState(const QString& stateKey = {}) const - { - const auto* evt = getCurrentState({ EventT::matrixTypeId(), stateKey }); - Q_ASSERT(evt->type() == EventT::typeId() - && evt->matrixType() == EventT::matrixTypeId()); - return static_cast<const EventT*>(evt); - } - -// template <typename EventT> -// const auto& getCurrentStateContent(const QString& stateKey = {}) const -// { -// if (const auto* evt = -// currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr)) -// return evt->content(); -// return EventT::content_type() -// } - template <typename EventArrayT> Changes updateStateFrom(EventArrayT&& events) { @@ -326,7 +300,9 @@ public: QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); - SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event) + SetRoomStateWithKeyJob* requestSetState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson) { // if (event.roomId().isEmpty()) // event.setRoomId(id); @@ -334,14 +310,8 @@ public: // event.setSender(connection->userId()); // TODO: Queue up state events sending (see #133). // TODO: Maybe addAsPending() as well, despite having no txnId - return connection->callApi<SetRoomStateWithKeyJob>( - id, event.matrixType(), event.stateKey(), event.contentJson()); - } - - template <typename EvT, typename... ArgTs> - auto requestSetState(ArgTs&&... args) - { - return requestSetState(EvT(std::forward<ArgTs>(args)...)); + return connection->callApi<SetRoomStateWithKeyJob>(id, evtType, stateKey, + contentJson); } /*! Apply redaction to the timeline @@ -397,8 +367,7 @@ public: const QString& eventId, QDateTime timestamp) { - const auto senderSessionPairKey = make_pair(senderKey, sessionId); - auto groupSessionIt = groupSessions.find(senderSessionPairKey); + auto groupSessionIt = groupSessions.find({ senderKey, sessionId }); if (groupSessionIt == groupSessions.end()) { // qCWarning(E2EE) << "Unable to decrypt event" << eventId // << "The sender's device has not sent us the keys for " @@ -472,8 +441,8 @@ 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; + const auto v = currentState().query(&RoomCreateEvent::version); + return v && !v->isEmpty() ? *v : QStringLiteral("1"); } bool Room::isUnstable() const @@ -484,7 +453,10 @@ bool Room::isUnstable() const QString Room::predecessorId() const { - return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId; + if (const auto* evt = currentState().get<RoomCreateEvent>()) + return evt->predecessor().roomId; + + return {}; } Room* Room::predecessor(JoinStates statesFilter) const @@ -499,7 +471,8 @@ Room* Room::predecessor(JoinStates statesFilter) const QString Room::successorId() const { - return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId(); + return currentState().queryOr(&RoomTombstoneEvent::successorRoomId, + QString()); } Room* Room::successor(JoinStates statesFilter) const @@ -526,30 +499,47 @@ bool Room::allHistoryLoaded() const QString Room::name() const { - return d->getCurrentState<RoomNameEvent>()->name(); + return currentState().queryOr(&RoomNameEvent::name, QString()); } QStringList Room::aliases() const { - const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>(); - auto result = evt->altAliases(); - if (!evt->alias().isEmpty()) - result << evt->alias(); - return result; + if (const auto* evt = currentState().get<RoomCanonicalAliasEvent>()) { + auto result = evt->altAliases(); + if (!evt->alias().isEmpty()) + result << evt->alias(); + return result; + } + return {}; } QStringList Room::altAliases() const { - return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases(); + return currentState().queryOr(&RoomCanonicalAliasEvent::altAliases, + QStringList()); } QString Room::canonicalAlias() const { - return d->getCurrentState<RoomCanonicalAliasEvent>()->alias(); + return currentState().queryOr(&RoomCanonicalAliasEvent::alias, QString()); } QString Room::displayName() const { return d->displayname; } +QStringList Room::pinnedEventIds() const { + return currentState().queryOr(&RoomPinnedEvent::pinnedEvents, QStringList()); +} + +QVector<const Quotient::RoomEvent*> Quotient::Room::pinnedEvents() const +{ + QVector<const RoomEvent*> pinnedEvents; + for (const auto& evtId : pinnedEventIds()) + if (const auto& it = findInTimeline(evtId); it != historyEdge()) + pinnedEvents.append(it->event()); + + return pinnedEvents; +} + QString Room::displayNameForHtml() const { return displayName().toHtmlEscaped(); @@ -559,7 +549,7 @@ void Room::refreshDisplayName() { d->updateDisplayname(); } QString Room::topic() const { - return d->getCurrentState<RoomTopicEvent>()->topic(); + return currentState().queryOr(&RoomTopicEvent::topic, QString()); } QString Room::avatarMediaId() const { return d->avatar.mediaId(); } @@ -598,7 +588,8 @@ JoinState Room::memberJoinState(User* user) const Membership Room::memberState(const QString& userId) const { - return d->getCurrentState<RoomMemberEvent>(userId)->membership(); + return currentState().queryOr(userId, &RoomMemberEvent::membership, + Membership::Leave); } bool Room::isMember(const QString& userId) const @@ -876,8 +867,9 @@ bool Room::canSwitchVersions() const if (!successorId().isEmpty()) return false; // No one can upgrade a room that's already upgraded - if (const auto* plEvt = d->getCurrentState<RoomPowerLevelsEvent>()) { - const auto currentUserLevel = plEvt->powerLevelForUser(localUser()->id()); + if (const auto* plEvt = currentState().get<RoomPowerLevelsEvent>()) { + const auto currentUserLevel = + plEvt->powerLevelForUser(localUser()->id()); const auto tombstonePowerLevel = plEvt->powerLevelForState("m.room.tombstone"_ls); return currentUserLevel >= tombstonePowerLevel; @@ -973,18 +965,28 @@ Room::findPendingEvent(const QString& txnId) const }); } -const Room::RelatedEvents Room::relatedEvents(const QString& evtId, - const char* relType) const +const Room::RelatedEvents Room::relatedEvents( + const QString& evtId, EventRelation::reltypeid_t relType) const { return d->relations.value({ evtId, relType }); } -const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt, - const char* relType) const +const Room::RelatedEvents Room::relatedEvents( + const RoomEvent& evt, EventRelation::reltypeid_t relType) const { return relatedEvents(evt.id(), relType); } +const RoomCreateEvent* Room::creation() const +{ + return currentState().get<RoomCreateEvent>(); +} + +const RoomTombstoneEvent *Room::tombstone() const +{ + return currentState().get<RoomTombstoneEvent>(); +} + void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. @@ -1449,7 +1451,9 @@ int Room::timelineSize() const { return int(d->timeline.size()); } bool Room::usesEncryption() const { - return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty(); + return !currentState() + .queryOr(&EncryptionEvent::algorithm, QString()) + .isEmpty(); } const StateEventBase* Room::getCurrentState(const QString& evtType, @@ -1458,13 +1462,7 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, return d->getCurrentState({ evtType, stateKey }); } -const QVector<const StateEventBase*> -Room::stateEventsOfType(const QString& evtType) const -{ - return d->stateEventsOfType(evtType); -} - -const QHash<StateEventKey, const StateEventBase*>& Room::currentState() const +RoomStateView Room::currentState() const { return d->currentState; } @@ -1558,7 +1556,7 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) void Room::Private::insertMemberIntoMap(User* u) { const auto maybeUserName = - getCurrentState<RoomMemberEvent>(u->id())->newDisplayName(); + currentState.query(u->id(), &RoomMemberEvent::newDisplayName); if (!maybeUserName) qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id() << "has no name (even empty)"; @@ -1588,9 +1586,9 @@ void Room::Private::insertMemberIntoMap(User* u) void Room::Private::removeMemberFromMap(User* u) { - const auto userName = - getCurrentState<RoomMemberEvent>( - u->id())->newDisplayName().value_or(QString()); + const auto userName = currentState.queryOr(u->id(), + &RoomMemberEvent::newDisplayName, + QString()); qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName << "for user" << u->id(); @@ -1673,10 +1671,13 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, QString Room::memberName(const QString& mxId) const { // See https://github.com/matrix-org/matrix-doc/issues/1375 - const auto rme = getCurrentState<RoomMemberEvent>(mxId); - return rme->newDisplayName() ? *rme->newDisplayName() - : rme->prevContent() ? rme->prevContent()->displayName.value_or(QString()) - : QString(); + if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) { + if (rme->newDisplayName()) + return *rme->newDisplayName(); + if (rme->prevContent() && rme->prevContent()->displayName) + return *rme->prevContent()->displayName; + } + return {}; } QString Room::roomMembername(const User* u) const @@ -1731,10 +1732,13 @@ QString Room::htmlSafeMemberName(const QString& userId) const QUrl Room::memberAvatarUrl(const QString &mxId) const { // See https://github.com/matrix-org/matrix-doc/issues/1375 - const auto rme = getCurrentState<RoomMemberEvent>(mxId); - return rme->newAvatarUrl() ? *rme->newAvatarUrl() - : rme->prevContent() ? rme->prevContent()->avatarUrl.value_or(QUrl()) - : QUrl(); + if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) { + if (rme->newAvatarUrl()) + return *rme->newAvatarUrl(); + if (rme->prevContent() && rme->prevContent()->avatarUrl) + return *rme->prevContent()->avatarUrl; + } + return {}; } Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, @@ -2126,29 +2130,41 @@ QString Room::postJson(const QString& matrixType, return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent)); } -SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const +SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) +{ + return d->requestSetState(evt.matrixType(), evt.stateKey(), + evt.contentJson()); +} + +SetRoomStateWithKeyJob* Room::setState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson) { - return d->requestSetState(evt); + return d->requestSetState(evtType, stateKey, contentJson); } void Room::setName(const QString& newName) { - d->requestSetState<RoomNameEvent>(newName); + setState<RoomNameEvent>(newName); } void Room::setCanonicalAlias(const QString& newAlias) { - d->requestSetState<RoomCanonicalAliasEvent>(newAlias, altAliases()); + setState<RoomCanonicalAliasEvent>(newAlias, altAliases()); } +void Room::setPinnedEvents(const QStringList& events) +{ + setState<RoomPinnedEvent>(events); +} void Room::setLocalAliases(const QStringList& aliases) { - d->requestSetState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases); + setState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases); } void Room::setTopic(const QString& newTopic) { - d->requestSetState<RoomTopicEvent>(newTopic); + setState<RoomTopicEvent>(newTopic); } bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) @@ -2488,12 +2504,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); qCDebug(EVENTS) << "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 + // Check whether the old event was a part of current state; if it was, + // update the current state to the redacted event object. + const auto currentStateEvt = + currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); + Q_ASSERT(currentStateEvt); + if (currentStateEvt == oldEvent.get()) { + // Historical states can't be in currentState + Q_ASSERT(ti.index() >= 0); qCDebug(STATE).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); @@ -2505,8 +2523,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) { const auto& targetEvtId = reaction->relation().eventId; - const auto lookupKey = - qMakePair(targetEvtId, EventRelation::Annotation()); + const QPair lookupKey { targetEvtId, EventRelation::AnnotationType }; if (relations.contains(lookupKey)) { relations[lookupKey].removeOne(reaction); emit q->updatedEvent(targetEvtId); @@ -2514,6 +2531,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); + // By now, all references to oldEvent must have been updated to ti.event() return true; } @@ -2784,7 +2802,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) for (const auto& eptr : events) { const auto& e = *eptr; if (e.isStateEvent() - && !currentState.contains({ e.matrixType(), e.stateKey() })) { + && !currentState.contains(e.matrixType(), e.stateKey())) { changes |= q->processStateEvent(e); } } @@ -2821,13 +2839,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return Change::None; // Find a value (create an empty one if necessary) and get a reference - // to it. Can't use getCurrentState<>() because it (creates and) returns - // a stub if a value is not found, and what's needed here is a "real" event - // or nullptr. + // to it, anticipating a change further in the function. auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change // clang-format off - const bool proceed = visit(e + const bool proceed = switchOnType(e , [this, curStateEvent](const RoomMemberEvent& rme) { // clang-format on auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent); @@ -2925,7 +2941,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // Update internal structures as per the change and work out the return value // clang-format off - const auto result = visit(e + const auto result = switchOnType(e , [] (const RoomNameEvent&) { return Change::Name; } @@ -2933,7 +2949,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // clang-format on setObjectName(cae.alias().isEmpty() ? d->id : cae.alias()); const auto* oldCae = - static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent); + static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent); QStringList previousAltAliases {}; if (oldCae) { previousAltAliases = oldCae->altAliases(); @@ -2945,10 +2961,15 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!cae.alias().isEmpty()) newAliases.push_front(cae.alias()); - connection()->updateRoomAliases(id(), previousAltAliases, newAliases); + connection()->updateRoomAliases(id(), previousAltAliases, + newAliases); return Change::Aliases; // clang-format off } + , [this] (const RoomPinnedEvent&) { + emit pinnedEventsChanged(); + return Change::Other; + } , [] (const RoomTopicEvent&) { return Change::Topic; } @@ -10,6 +10,7 @@ #pragma once #include "connection.h" +#include "roomstateview.h" #include "eventitem.h" #include "quotient_common.h" @@ -21,6 +22,7 @@ #include "events/roommessageevent.h" #include "events/roomcreateevent.h" #include "events/roomtombstoneevent.h" +#include "events/eventrelation.h" #include <QtCore/QJsonObject> #include <QtGui/QImage> @@ -45,7 +47,7 @@ class RedactEventJob; * This is specifically tuned to work with QML exposing all traits as * Q_PROPERTY values. */ -class FileTransferInfo { +class QUOTIENT_API FileTransferInfo { Q_GADGET Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) Q_PROPERTY(bool active READ active CONSTANT) @@ -73,7 +75,7 @@ public: //! \brief Data structure for a room member's read receipt //! \sa Room::lastReadReceipt -class ReadReceipt { +class QUOTIENT_API ReadReceipt { Q_GADGET Q_PROPERTY(QString eventId MEMBER eventId CONSTANT) Q_PROPERTY(QDateTime timestamp MEMBER timestamp CONSTANT) @@ -101,7 +103,7 @@ struct EventStats; struct Notification { enum Type { None = 0, Basic, Highlight }; - Q_ENUM(Notification) + Q_ENUM(Type) Type type = None; @@ -110,7 +112,7 @@ private: Q_PROPERTY(Type type MEMBER type CONSTANT) }; -class Room : public QObject { +class QUOTIENT_API Room : public QObject { Q_OBJECT Q_PROPERTY(Connection* connection READ connection CONSTANT) Q_PROPERTY(User* localUser READ localUser CONSTANT) @@ -124,6 +126,8 @@ class Room : public QObject { Q_PROPERTY(QStringList altAliases READ altAliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) + Q_PROPERTY(QStringList pinnedEventIds READ pinnedEventIds WRITE setPinnedEvents + NOTIFY pinnedEventsChanged) Q_PROPERTY(QString displayNameForHtml READ displayNameForHtml NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged @@ -253,6 +257,9 @@ public: //! Get a list of both canonical and alternative aliases QStringList aliases() const; QString displayName() const; + QStringList pinnedEventIds() const; + // Returns events available locally, use pinnedEventIds() for full list + QVector<const RoomEvent*> pinnedEvents() const; QString displayNameForHtml() const; QString topic() const; QString avatarMediaId() const; @@ -389,18 +396,12 @@ public: PendingEvents::const_iterator findPendingEvent(const QString& txnId) const; const RelatedEvents relatedEvents(const QString& evtId, - const char* relType) const; + EventRelation::reltypeid_t relType) const; const RelatedEvents relatedEvents(const RoomEvent& evt, - const char* relType) const; + EventRelation::reltypeid_t relType) const; - const RoomCreateEvent* creation() const - { - return getCurrentState<RoomCreateEvent>(); - } - const RoomTombstoneEvent* tombstone() const - { - return getCurrentState<RoomTombstoneEvent>(); - } + const RoomCreateEvent* creation() const; + const RoomTombstoneEvent* tombstone() const; bool displayed() const; /// Mark the room as currently displayed to the user @@ -754,47 +755,45 @@ public: /*! This method returns a (potentially empty) state event corresponding * to the pair of event type \p evtType and state key \p stateKey. */ - Q_INVOKABLE const Quotient::StateEventBase* + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // + const Quotient::StateEventBase* getCurrentState(const QString& evtType, const QString& stateKey = {}) const; - /// Get all state events in the room. - /*! This method returns all known state events that have occured in - * the room, as a mapping from the event type and state key to value. - */ - const QHash<StateEventKey, const StateEventBase*>& currentState() const; - - /// Get all state events in the room of a certain type. - /*! This method returns all known state events that have occured in - * the room of the given type. - */ - Q_INVOKABLE const QVector<const StateEventBase*> - stateEventsOfType(const QString& evtType) const; - /// Get a state event with the given event type and state key /*! This is a typesafe overload that accepts a C++ event type instead of * its Matrix name. */ template <typename EvT> + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // const EvT* getCurrentState(const QString& stateKey = {}) const { - const auto* evt = - eventCast<const EvT>(getCurrentState(EvT::matrixTypeId(), stateKey)); + QT_IGNORE_DEPRECATIONS( + const auto* evt = eventCast<const EvT>( + getCurrentState(EvT::matrixTypeId(), stateKey));) Q_ASSERT(evt); Q_ASSERT(evt->matrixTypeId() == EvT::matrixTypeId() && evt->stateKey() == stateKey); return evt; } - /// Set a state event of the given type with the given arguments - /*! This typesafe overload attempts to send a state event with the type - * \p EvT and the content defined by \p args. Specifically, the function - * creates a temporary object of type \p EvT passing \p args to - * the constructor, and sends a request to the homeserver using - * the Matrix event type defined by \p EvT and the event content produced - * via EvT::contentJson(). - */ + /// \brief Get the current room state + RoomStateView currentState() const; + + //! Send a request to update the room state with the given event + SetRoomStateWithKeyJob* setState(const StateEventBase& evt); + + //! \brief Set a state event of the given type with the given arguments + //! + //! This typesafe overload attempts to send a state event with the type + //! \p EvT and the content defined by \p args. Specifically, the function + //! creates a temporary object of type \p EvT passing \p args to + //! the constructor, and sends a request to the homeserver using + //! the Matrix event type defined by \p EvT and the event content produced + //! via EvT::contentJson(). template <typename EvT, typename... ArgTs> - auto setState(ArgTs&&... args) const + auto setState(ArgTs&&... args) { return setState(EvT(std::forward<ArgTs>(args)...)); } @@ -828,10 +827,13 @@ public Q_SLOTS: QString retryMessage(const QString& txnId); void discardMessage(const QString& txnId); - /// Send a request to update the room state with the given event - SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; + //! Send a request to update the room state based on freeform inputs + SetRoomStateWithKeyJob* setState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson); void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); + void setPinnedEvents(const QStringList& events); /// Set room aliases on the user's current server void setLocalAliases(const QStringList& aliases); void setTopic(const QString& newTopic); @@ -938,6 +940,7 @@ Q_SIGNALS: void namesChanged(Quotient::Room* room); void displaynameAboutToChange(Quotient::Room* room); void displaynameChanged(Quotient::Room* room, QString oldName); + void pinnedEventsChanged(); void topicChanged(); void avatarChanged(); void userAdded(Quotient::User* user); @@ -1037,7 +1040,7 @@ private: void setJoinState(JoinState state); }; -class MemberSorter { +class QUOTIENT_API MemberSorter { public: explicit MemberSorter(const Room* r) : room(r) {} diff --git a/lib/roomstateview.cpp b/lib/roomstateview.cpp new file mode 100644 index 00000000..94c88eee --- /dev/null +++ b/lib/roomstateview.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "roomstateview.h" + +using namespace Quotient; + +const StateEventBase* RoomStateView::get(const QString& evtType, + const QString& stateKey) const +{ + return value({ evtType, stateKey }); +} + +bool RoomStateView::contains(const QString& evtType, + const QString& stateKey) const +{ + return contains({ evtType, stateKey }); +} + +QJsonObject RoomStateView::contentJson(const QString& evtType, + const QString& stateKey) const +{ + return queryOr(evtType, stateKey, &Event::contentJson, QJsonObject()); +} + +const QVector<const StateEventBase*> +RoomStateView::eventsOfType(const QString& evtType) const +{ + auto vals = QVector<const StateEventBase*>(); + for (auto it = cbegin(); it != cend(); ++it) + if (it.key().first == evtType) + vals.append(it.value()); + + return vals; +} diff --git a/lib/roomstateview.h b/lib/roomstateview.h new file mode 100644 index 00000000..cab69ae3 --- /dev/null +++ b/lib/roomstateview.h @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/stateevent.h" + +#include <QtCore/QHash> + +namespace Quotient { + +class Room; + +class RoomStateView : private QHash<StateEventKey, const StateEventBase*> { + Q_GADGET +public: + const QHash<StateEventKey, const StateEventBase*>& events() const + { + return *this; + } + + //! \brief Get a state event with the given event type and state key + //! \return A state event corresponding to the pair of event type + //! \p evtType and state key \p stateKey, or nullptr if there's + //! no such \p evtType / \p stateKey combination in the current + //! state. + //! \warning In libQuotient 0.7 the return type changed to an OmittableCref + //! which is effectively a nullable const reference wrapper. You + //! have to check that it has_value() before using. Alternatively + //! you can now use queryCurrentState() to access state safely. + //! \sa getCurrentStateContentJson + const StateEventBase* get(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief Get a state event with the given event type and state key + //! + //! This is a typesafe overload that accepts a C++ event type instead of + //! its Matrix name. + //! \warning In libQuotient 0.7 the return type changed to an Omittable with + //! a reference wrapper inside - you have to check that it + //! has_value() before using. Alternatively you can now use + //! queryCurrentState() to access state safely. + template <typename EvT> + const EvT* get(const QString& stateKey = {}) const + { + static_assert(std::is_base_of_v<StateEventBase, EvT>); + if (const auto* evt = get(EvT::matrixTypeId(), stateKey)) { + Q_ASSERT(evt->matrixType() == EvT::matrixTypeId() + && evt->stateKey() == stateKey); + return eventCast<const EvT>(evt); + } + return nullptr; + } + + using QHash::contains; + + bool contains(const QString& evtType, const QString& stateKey = {}) const; + + template <typename EvT> + bool contains(const QString& stateKey = {}) const + { + return contains(EvT::matrixTypeId(), stateKey); + } + + //! \brief Get the content of the current state event with the given + //! event type and state key + //! \return An empty object if there's no event in the current state with + //! this event type and state key; the contents of the event + //! <tt>'content'</tt> object otherwise + Q_INVOKABLE QJsonObject contentJson(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief Get all state events in the room of a certain type. + //! + //! This method returns all known state events that have occured in + //! the room of the given type. + const QVector<const StateEventBase*> + eventsOfType(const QString& evtType) const; + + template <typename FnT> + auto query(const QString& evtType, const QString& stateKey, FnT&& fn) const + { + return lift(std::forward<FnT>(fn), get(evtType, stateKey)); + } + + template <typename FnT> + auto query(const QString& stateKey, FnT&& fn) const + { + using EventT = std::decay_t<fn_arg_t<FnT>>; + static_assert(std::is_base_of_v<StateEventBase, EventT>); + return lift(std::forward<FnT>(fn), get<EventT>(stateKey)); + } + + template <typename FnT, typename FallbackT> + auto queryOr(const QString& evtType, const QString& stateKey, FnT&& fn, + FallbackT&& fallback) const + { + return lift(std::forward<FnT>(fn), get(evtType, stateKey)) + .value_or(std::forward<FallbackT>(fallback)); + } + + template <typename FnT> + auto query(FnT&& fn) const + { + return query({}, std::forward<FnT>(fn)); + } + + template <typename FnT, typename FallbackT> + auto queryOr(const QString& stateKey, FnT&& fn, FallbackT&& fallback) const + { + using EventT = std::decay_t<fn_arg_t<FnT>>; + static_assert(std::is_base_of_v<StateEventBase, EventT>); + return lift(std::forward<FnT>(fn), get<EventT>(stateKey)) + .value_or(std::forward<FallbackT>(fallback)); + } + + template <typename FnT, typename FallbackT> + auto queryOr(FnT&& fn, FallbackT&& fallback) const + { + return queryOr({}, std::forward<FnT>(fn), + std::forward<FallbackT>(fallback)); + } + +private: + friend class Room; +}; +} // namespace Quotient diff --git a/lib/settings.cpp b/lib/settings.cpp index f9b4f471..510d253c 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -3,6 +3,7 @@ #include "settings.h" +#include "util.h" #include "logging.h" #include <QtCore/QUrl> @@ -102,17 +103,18 @@ void SettingsGroup::remove(const QString& key) Settings::remove(fullKey); } -QTNT_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, +QUO_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId) -QTNT_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, +QUO_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName) -QTNT_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, +QUO_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) -static const auto HomeserverKey = QStringLiteral("homeserver"); -static const auto AccessTokenKey = QStringLiteral("access_token"); -static const auto EncryptionAccountPickleKey = - QStringLiteral("encryption_account_pickle"); +namespace { +constexpr auto HomeserverKey = "homeserver"_ls; +constexpr auto AccessTokenKey = "access_token"_ls; +constexpr auto EncryptionAccountPickleKey = "encryption_account_pickle"_ls; +} QUrl AccountSettings::homeserver() const { diff --git a/lib/settings.h b/lib/settings.h index efd0d714..ff99d488 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -3,6 +3,8 @@ #pragma once +#include "quotient_export.h" + #include <QtCore/QSettings> #include <QtCore/QUrl> #include <QtCore/QVector> @@ -11,7 +13,7 @@ class QVariant; namespace Quotient { -class Settings : public QSettings { +class QUOTIENT_API Settings : public QSettings { Q_OBJECT public: /// Add a legacy organisation/application name to migrate settings from @@ -76,7 +78,7 @@ protected: QSettings legacySettings { legacyOrganizationName, legacyApplicationName }; }; -class SettingsGroup : public Settings { +class QUOTIENT_API SettingsGroup : public Settings { public: explicit SettingsGroup(QString path, QObject* parent = nullptr) : Settings(parent) @@ -104,7 +106,7 @@ private: QString groupPath; }; -#define QTNT_DECLARE_SETTING(type, propname, setter) \ +#define QUO_DECLARE_SETTING(type, propname, setter) \ Q_PROPERTY(type propname READ propname WRITE setter) \ public: \ type propname() const; \ @@ -112,7 +114,7 @@ public: \ \ private: -#define QTNT_DEFINE_SETTING(classname, type, propname, qsettingname, \ +#define QUO_DEFINE_SETTING(classname, type, propname, qsettingname, \ defaultValue, setter) \ type classname::propname() const \ { \ @@ -124,12 +126,12 @@ private: setValue(QStringLiteral(qsettingname), std::move(newValue)); \ } -class AccountSettings : public SettingsGroup { +class QUOTIENT_API AccountSettings : public SettingsGroup { Q_OBJECT Q_PROPERTY(QString userId READ userId CONSTANT) - QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId) - QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName) - QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) + QUO_DECLARE_SETTING(QString, deviceId, setDeviceId) + QUO_DECLARE_SETTING(QString, deviceName, setDeviceName) + QUO_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle WRITE setEncryptionAccountPickle) public: diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp index a1d27504..93e252cc 100644 --- a/lib/ssosession.cpp +++ b/lib/ssosession.cpp @@ -15,10 +15,10 @@ using namespace Quotient; class SsoSession::Private { public: - Private(SsoSession* q, const QString& initialDeviceName = {}, - const QString& deviceId = {}, Connection* connection = nullptr) - : initialDeviceName(initialDeviceName) - , deviceId(deviceId) + Private(SsoSession* q, QString initialDeviceName = {}, + QString deviceId = {}, Connection* connection = nullptr) + : initialDeviceName(std::move(initialDeviceName)) + , deviceId(std::move(deviceId)) , connection(connection) { auto* server = new QTcpServer(q); @@ -29,7 +29,7 @@ public: .arg(server->serverPort()); ssoUrl = connection->getUrlForApi<RedirectToSSOJob>(callbackUrl); - QObject::connect(server, &QTcpServer::newConnection, q, [this, server] { + QObject::connect(server, &QTcpServer::newConnection, q, [this, q, server] { qCDebug(MAIN) << "SSO callback initiated"; socket = server->nextPendingConnection(); server->close(); @@ -43,8 +43,14 @@ public: }); QObject::connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater); + QObject::connect(socket, &QObject::destroyed, q, + &QObject::deleteLater); }); + qCDebug(MAIN) << "SSO session constructed"; } + ~Private() { qCDebug(MAIN) << "SSO session deconstructed"; } + Q_DISABLE_COPY_MOVE(Private) + void processCallback(); void sendHttpResponse(const QByteArray& code, const QByteArray& msg); void onError(const QByteArray& code, const QString& errorMsg); @@ -61,15 +67,8 @@ public: SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName, const QString& deviceId) : QObject(connection) - , d(std::make_unique<Private>(this, initialDeviceName, deviceId, connection)) -{ - qCDebug(MAIN) << "SSO session constructed"; -} - -SsoSession::~SsoSession() -{ - qCDebug(MAIN) << "SSO session deconstructed"; -} + , d(makeImpl<Private>(this, initialDeviceName, deviceId, connection)) +{} QUrl SsoSession::ssoUrl() const { return d->ssoUrl; } @@ -82,29 +81,29 @@ void SsoSession::Private::processCallback() // (see at https://github.com/clementine-player/Clementine/) const auto& requestParts = requestData.split(' '); if (requestParts.size() < 2 || requestParts[1].isEmpty()) { - onError("400 Bad Request", tr("No login token in SSO callback")); + onError("400 Bad Request", tr("Malformed single sign-on callback")); return; } const auto& QueryItemName = QStringLiteral("loginToken"); QUrlQuery query { QUrl(requestParts[1]).query() }; if (!query.hasQueryItem(QueryItemName)) { - onError("400 Bad Request", tr("Malformed single sign-on callback")); + onError("400 Bad Request", tr("No login token in SSO callback")); + return; } qCDebug(MAIN) << "Found the token in SSO callback, logging in"; connection->loginWithToken(query.queryItemValue(QueryItemName).toLatin1(), initialDeviceName, deviceId); connect(connection, &Connection::connected, socket, [this] { - const QString msg = - "The application '" % QCoreApplication::applicationName() - % "' has successfully logged in as a user " % connection->userId() - % " with device id " % connection->deviceId() - % ". This window can be closed. Thank you.\r\n"; + const auto msg = + tr("The application '%1' has successfully logged in as a user %2 " + "with device id %3. This window can be closed. Thank you.\r\n") + .arg(QCoreApplication::applicationName(), connection->userId(), + connection->deviceId()); sendHttpResponse("200 OK", msg.toHtmlEscaped().toUtf8()); socket->disconnectFromHost(); }); connect(connection, &Connection::loginError, socket, [this] { onError("401 Unauthorised", tr("Login failed")); - socket->disconnectFromHost(); }); } @@ -128,4 +127,5 @@ void SsoSession::Private::onError(const QByteArray& code, // [kitsune] Yeah, I know, dirty. Maybe the "right" way would be to have // an intermediate signal but that seems just a fight for purity. emit connection->loginError(errorMsg, requestData); + socket->disconnectFromHost(); } diff --git a/lib/ssosession.h b/lib/ssosession.h index 72dd60c4..e6a3f8fb 100644 --- a/lib/ssosession.h +++ b/lib/ssosession.h @@ -3,14 +3,11 @@ #pragma once +#include "util.h" + #include <QtCore/QUrl> #include <QtCore/QObject> -#include <memory> - -class QTcpServer; -class QTcpSocket; - namespace Quotient { class Connection; @@ -29,19 +26,20 @@ class Connection; * connection->prepareForSso(initialDeviceName)->ssoUrl()); * \endcode */ -class SsoSession : public QObject { +class QUOTIENT_API SsoSession : public QObject { Q_OBJECT Q_PROPERTY(QUrl ssoUrl READ ssoUrl CONSTANT) Q_PROPERTY(QUrl callbackUrl READ callbackUrl CONSTANT) public: SsoSession(Connection* connection, const QString& initialDeviceName, const QString& deviceId = {}); - ~SsoSession() override; + ~SsoSession() override = default; + QUrl ssoUrl() const; QUrl callbackUrl() const; private: class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } // namespace Quotient diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index d0533fc9..78957cbe 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -219,14 +219,18 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) // We have a Qt container on the right and an STL one on the left roomData.reserve(roomData.size() + 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; - } + QJsonObject roomJson; + if (!baseDir.isEmpty()) { + // Loading data from the local cache, with room objects saved in + // individual files rather than inline + roomJson = loadJson(baseDir + fileNameForRoom(roomIt.key())); + if (roomJson.isEmpty()) { + unresolvedRoomIds.push_back(roomIt.key()); + continue; + } + } else // When loading from /sync response, everything is inline + roomJson = roomIt->toObject(); + roomData.emplace_back(roomIt.key(), joinState, roomJson); const auto& r = roomData.back(); totalEvents += r.state.size() + r.ephemeral.size() diff --git a/lib/syncdata.h b/lib/syncdata.h index 7fa77eda..633f4b52 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -22,7 +22,7 @@ constexpr auto HighlightCountKey = "highlight_count"_ls; * 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 { +struct QUOTIENT_API RoomSummary { Omittable<int> joinedMemberCount; Omittable<int> invitedMemberCount; Omittable<QStringList> heroes; //< mxids of users to take part in the room diff --git a/lib/uri.cpp b/lib/uri.cpp index c8843dda..6b7d1d20 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -3,29 +3,34 @@ #include "uri.h" +#include "util.h" #include "logging.h" #include <QtCore/QRegularExpression> using namespace Quotient; -struct ReplacePair { QByteArray uriString; char sigil; }; +namespace { + +struct ReplacePair { QLatin1String uriString; char sigil; }; /// \brief Defines bi-directional mapping of path prefixes and sigils /// /// When there are two prefixes for the same sigil, the first matching /// entry for a given sigil is used. -static const auto replacePairs = { - ReplacePair { "u/", '@' }, - { "user/", '@' }, - { "roomid/", '!' }, - { "r/", '#' }, - { "room/", '#' }, +const ReplacePair replacePairs[] = { + { "u/"_ls, '@' }, + { "user/"_ls, '@' }, + { "roomid/"_ls, '!' }, + { "r/"_ls, '#' }, + { "room/"_ls, '#' }, // The notation for bare event ids is not proposed in MSC2312 but there's // https://github.com/matrix-org/matrix-doc/pull/2644 - { "e/", '$' }, - { "event/", '$' } + { "e/"_ls, '$' }, + { "event/"_ls, '$' } }; +} + Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) { if (primaryId.isEmpty()) @@ -75,7 +80,7 @@ static auto decodeFragmentPart(QStringView part) return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8(); } -static auto matrixToUrlRegexInit() +static inline auto matrixToUrlRegexInit() { // See https://matrix.org/docs/spec/appendices#matrix-to-navigation const QRegularExpression MatrixToUrlRE { @@ -23,7 +23,7 @@ namespace Quotient { * its type, and obtain components, also in either unencoded (for displaying) * or encoded (for APIs) form. */ -class Uri : private QUrl { +class QUOTIENT_API Uri : private QUrl { Q_GADGET public: enum Type : char { diff --git a/lib/uriresolver.h b/lib/uriresolver.h index ff97324a..9140046c 100644 --- a/lib/uriresolver.h +++ b/lib/uriresolver.h @@ -25,7 +25,7 @@ class User; * gradual implementation. Derived classes are encouraged to override as many * of them as possible. */ -class UriResolverBase { +class QUOTIENT_API UriResolverBase { public: /*! \brief Resolve the resource and dispatch an action depending on its type * @@ -105,7 +105,7 @@ protected: * * \sa UriResolverBase, UriDispatcher */ -UriResolveResult +QUOTIENT_API UriResolveResult visitResource(Connection* account, const Uri& uri, std::function<UriResolveResult(User*, QString)> userHandler, std::function<void(Room*, QString)> roomEventHandler, @@ -141,7 +141,7 @@ inline UriResolveResult checkResource(Connection* account, const Uri& uri) * synchronously - the returned value is the result of resolving the URI, * not acting on it. */ -class UriDispatcher : public QObject, public UriResolverBase { +class QUOTIENT_API UriDispatcher : public QObject, public UriResolverBase { Q_OBJECT public: explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {} diff --git a/lib/user.cpp b/lib/user.cpp index 7da71dba..4c3fc9e2 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -46,7 +46,7 @@ public: decltype(User::Private::otherAvatars) User::Private::otherAvatars {}; User::User(QString userId, Connection* connection) - : QObject(connection), d(new Private(move(userId))) + : QObject(connection), d(makeImpl<Private>(move(userId))) { setObjectName(id()); if (connection->userId() == id()) { @@ -61,8 +61,6 @@ Connection* User::connection() const return static_cast<Connection*>(parent()); } -User::~User() = default; - void User::load() { auto* profileJob = @@ -112,7 +110,7 @@ void User::rename(const QString& newName) }); } -void User::rename(const QString& newName, const Room* r) +void User::rename(const QString& newName, Room* r) { if (!r) { qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" @@ -121,12 +119,17 @@ void User::rename(const QString& newName, const Room* r) return; } // #481: take the current state and update it with the new name - auto evtC = r->getCurrentState<RoomMemberEvent>(id())->content(); - Q_ASSERT_X(evtC.membership == Membership::Join, __FUNCTION__, - "Attempt to rename a user that's not a room member"); - evtC.displayName = sanitized(newName); - r->setState<RoomMemberEvent>(id(), move(evtC)); - // The state will be updated locally after it arrives with sync + if (const auto& maybeEvt = r->currentState().get<RoomMemberEvent>(id())) { + auto content = maybeEvt->content(); + if (content.membership == Membership::Join) { + content.displayName = sanitized(newName); + r->setState<RoomMemberEvent>(id(), move(content)); + // The state will be updated locally after it arrives with sync + return; + } + } + qCCritical(MEMBERS) + << "Attempt to rename a non-member in a room context - ignored"; } template <typename SourceT> @@ -5,6 +5,7 @@ #pragma once #include "avatar.h" +#include "util.h" #include <QtCore/QObject> @@ -13,7 +14,7 @@ class Connection; class Room; class RoomMemberEvent; -class User : public QObject { +class QUOTIENT_API User : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id CONSTANT) Q_PROPERTY(bool isGuest READ isGuest CONSTANT) @@ -26,7 +27,6 @@ class User : public QObject { Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged) public: User(QString userId, Connection* connection); - ~User() override; Connection* connection() const; @@ -96,7 +96,7 @@ public Q_SLOTS: /// Set a new name in the global user profile void rename(const QString& newName); /// Set a new name for the user in one room - void rename(const QString& newName, const Room* r); + void rename(const QString& newName, Room* r); /// Upload the file and use it as an avatar bool setAvatar(const QString& fileName); /// Upload contents of the QIODevice and set that as an avatar @@ -125,7 +125,7 @@ Q_SIGNALS: private: class Private; - QScopedPointer<Private> d; + ImplPtr<Private> d; template <typename SourceT> bool doSetAvatar(SourceT&& source); diff --git a/lib/util.cpp b/lib/util.cpp index 2dfb09a6..03ebf325 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -135,35 +135,3 @@ int Quotient::patchVersion() { return Quotient_VERSION_PATCH; } - -// Tests for function_traits<> - -using namespace Quotient; - -int f_(); -static_assert(std::is_same<fn_return_t<decltype(f_)>, int>::value, - "Test fn_return_t<>"); - -void f1_(int, QString); -static_assert(std::is_same<fn_arg_t<decltype(f1_), 1>, QString>::value, - "Test fn_arg_t<>"); - -struct Fo { - int operator()(); - static constexpr auto l = [] { return 0.0f; }; -}; -static_assert(std::is_same<fn_return_t<Fo>, int>::value, - "Test return type of function object"); -static_assert(std::is_same<fn_return_t<decltype(Fo::l)>, float>::value, - "Test return type of lambda"); - -struct Fo1 { - void operator()(int); -}; -static_assert(std::is_same<fn_arg_t<Fo1>, int>(), - "Test fn_arg_t defaulting to first argument"); - -template <typename T> -static QString ft(T&&); -static_assert(std::is_same<fn_arg_t<decltype(ft<QString>)>, QString&&>(), - "Test function templates"); @@ -4,13 +4,13 @@ #pragma once +#include "quotient_export.h" + #include <QtCore/QLatin1String> #include <QtCore/QHashFunctions> -#include <functional> #include <memory> #include <unordered_map> -#include <optional> #ifndef Q_DISABLE_MOVE // Q_DISABLE_MOVE was introduced in Q_VERSION_CHECK(5,13,0) @@ -50,198 +50,7 @@ struct HashQ { template <typename KeyT, typename ValT> using UnorderedMap = std::unordered_map<KeyT, ValT, HashQ<KeyT>>; -namespace _impl { - template <typename TT> - constexpr inline auto IsOmittableValue = false; - template <typename TT> - constexpr inline auto IsOmittable = IsOmittableValue<std::decay_t<TT>>; -} - -constexpr auto none = std::nullopt; - -/** `std::optional` with tweaks - * - * The tweaks are: - * - streamlined assignment (operator=)/emplace()ment of values that can be - * used to implicitly construct the underlying type, including - * direct-list-initialisation, e.g.: - * \code - * struct S { int a; char b; } - * Omittable<S> o; - * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } - * \endcode - * - entirely deleted value(). The technical reason is that Xcode 10 doesn't - * have it; but besides that, value_or() or (after explicit checking) - * `operator*()`/`operator->()` are better alternatives within Quotient - * that doesn't practice throwing exceptions (as doesn't most of Qt). - * - disabled non-const lvalue operator*() and operator->(), as it's too easy - * to inadvertently cause a value change through them. - * - edit() to provide a safe and explicit lvalue accessor instead of those - * above. Requires the underlying type to be default-constructible. - * Allows chained initialisation of nested Omittables: - * \code - * struct Inner { int member = 10; Omittable<int> innermost; }; - * struct Outer { int anotherMember = 10; Omittable<Inner> inner; }; - * Omittable<Outer> o; // = { 10, std::nullopt }; - * o.edit().inner.edit().innermost.emplace(42); - * \endcode - * - merge() - a soft version of operator= that only overwrites its first - * operand with the second one if the second one is not empty. - */ -template <typename T> -class Omittable : public std::optional<T> { -public: - using base_type = std::optional<T>; - using value_type = std::decay_t<T>; - - using std::optional<T>::optional; - - // Overload emplace() and operator=() to allow passing braced-init-lists - // (the standard emplace() does direct-initialisation but - // not direct-list-initialisation). - using base_type::operator=; - Omittable& operator=(const value_type& v) - { - base_type::operator=(v); - return *this; - } - Omittable& operator=(value_type&& v) - { - base_type::operator=(v); - return *this; - } - using base_type::emplace; - T& emplace(const T& val) { return base_type::emplace(val); } - T& emplace(T&& val) { return base_type::emplace(std::move(val)); } - - // use value_or() or check (with operator! or has_value) before accessing - // with operator-> or operator* - // The technical reason is that Xcode 10 has incomplete std::optional - // that has no value(); but using value() may also mean that you rely - // on the optional throwing an exception (which is not assumed practice - // throughout Quotient) or that you spend unnecessary CPU cycles on - // an extraneous has_value() check. - value_type& value() = delete; - const value_type& value() const = delete; - value_type& edit() - { - return this->has_value() ? base_type::operator*() : this->emplace(); - } - - [[deprecated("Use '!o' or '!o.has_value()' instead of 'o.omitted()'")]] - bool omitted() const - { - return !this->has_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_v<T1, T>, bool> - { - if (!other || (this->has_value() && **this == *other)) - return false; - emplace(*other); - return true; - } - - // Hide non-const lvalue operator-> and operator* as these are - // a bit too surprising: value() & doesn't lazy-create an object; - // and it's too easy to inadvertently change the underlying value. - - const value_type* operator->() const& { return base_type::operator->(); } - value_type* operator->() && { return base_type::operator->(); } - const value_type& operator*() const& { return base_type::operator*(); } - value_type& operator*() && { return base_type::operator*(); } -}; -template <typename T> -Omittable(T&&) -> Omittable<T>; - -namespace _impl { - template <typename T> - constexpr inline auto IsOmittableValue<Omittable<T>> = true; -} - -template <typename T1, typename T2> -inline auto merge(Omittable<T1>& lhs, T2&& rhs) -{ - return lhs.merge(std::forward<T2>(rhs)); -} - -//! \brief Merge the value from an Omittable -//! This is an adaptation of Omittable::merge() to the case when the value -//! on the left hand side is not an Omittable. -//! \return true if \p rhs is not omitted and the \p lhs value was different, -//! in other words, if \p lhs has changed; -//! false otherwise -template <typename T1, typename T2> -inline auto merge(T1& lhs, const Omittable<T2>& rhs) - -> std::enable_if_t<!_impl::IsOmittable<T1> - && std::is_convertible_v<T2, T1>, bool> -{ - if (!rhs || lhs == *rhs) - return false; - lhs = *rhs; - return true; -} - -namespace _impl { - template <typename AlwaysVoid, typename> - struct fn_traits {}; -} - -/// Determine traits of an arbitrary function/lambda/functor -/*! - * 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 _impl::fn_traits<void, std::remove_reference_t<T>> {}; - -// Specialisation for a function -template <typename ReturnT, typename... ArgTs> -struct function_traits<ReturnT(ArgTs...)> { - using return_type = ReturnT; - using arg_types = std::tuple<ArgTs...>; - // Doesn't (and there's no plan to make it) work for "classic" - // member functions (i.e. outside of functors). - // See also the comment for wrap_in_function() below - using function_type = std::function<ReturnT(ArgTs...)>; -}; - -namespace _impl { - // Specialisation for function objects with (non-overloaded) operator() - // (this includes non-generic lambdas) - template <typename T> - struct fn_traits<decltype(void(&T::operator())), T> - : public fn_traits<void, decltype(&T::operator())> {}; - - // 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, int ArgN = 0> -using fn_arg_t = - std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; - -inline constexpr auto operator"" _ls(const char* s, std::size_t size) +constexpr auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); } @@ -295,27 +104,68 @@ inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last, return std::make_pair(last, sLast); } +//! \brief An owning implementation pointer +//! +//! This is basically std::unique_ptr<> to hold your pimpl's but without having +//! to define default constructors/operator=() out of line. +//! Thanks to https://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html +//! for inspiration +template <typename ImplType> +using ImplPtr = std::unique_ptr<ImplType, void (*)(ImplType*)>; + +// Why this works (see also the link above): because this defers the moment +// of requiring sizeof of ImplType to the place where makeImpl is invoked +// (which is located, necessarily, in the .cpp file after ImplType definition). +// The stock unique_ptr deleter (std::default_delete) normally needs sizeof +// at the same spot - as long as you defer definition of the owning type +// constructors and operator='s to the .cpp file as well. Which means you +// have to explicitly declare and define them (even if with = default), +// formally breaking the rule of zero; informally, just adding boilerplate code. +// The custom deleter itself is instantiated at makeImpl invocation - there's +// no way earlier to even know how ImplType will be deleted and whether that +// will need sizeof(ImplType) earlier. In theory it's a tad slower because +// the deleter is called by the pointer; however, the difference will not +// be noticeable (if exist at all) for any class with non-trivial contents. + +//! \brief make_unique for ImplPtr +//! +//! Since std::make_unique is not compatible with ImplPtr, this should be used +//! in constructors of frontend classes to create implementation instances. +template <typename ImplType, typename DeleterType = void (*)(ImplType*), + typename... ArgTs> +inline ImplPtr<ImplType> makeImpl(ArgTs&&... args) +{ + return ImplPtr<ImplType> { new ImplType(std::forward<ArgTs>(args)...), + [](ImplType* impl) { delete impl; } }; +} + +template <typename ImplType> +const inline ImplPtr<ImplType> ZeroImpl() +{ + return { nullptr, [](ImplType*) { /* nullptr doesn't need deletion */ } }; +} + /** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ -void linkifyUrls(QString& htmlEscapedText); +QUOTIENT_API void linkifyUrls(QString& htmlEscapedText); /** Sanitize the text before showing in HTML * * This does toHtmlEscaped() and removes Unicode BiDi marks. */ -QString sanitized(const QString& plainText); +QUOTIENT_API QString sanitized(const QString& plainText); /** Pretty-print plain text into HTML * * This includes HTML escaping of <,>,",& and calling linkifyUrls() */ -QString prettyPrint(const QString& plainText); +QUOTIENT_API 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); +QUOTIENT_API QString cacheLocation(const QString& dirName); /** Hue color component of based of the hash of the string. * @@ -324,13 +174,13 @@ QString cacheLocation(const QString& dirName); * Naming and range are the same as QColor's hueF method: * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision */ -qreal stringToHueF(const QString& s); +QUOTIENT_API qreal stringToHueF(const QString& s); /** Extract the serverpart from MXID */ -QString serverPart(const QString& mxId); +QUOTIENT_API QString serverPart(const QString& mxId); -QString versionString(); -int majorVersion(); -int minorVersion(); -int patchVersion(); +QUOTIENT_API QString versionString(); +QUOTIENT_API int majorVersion(); +QUOTIENT_API int minorVersion(); +QUOTIENT_API int patchVersion(); } // namespace Quotient diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt index 59334e30..ec305620 100644 --- a/quotest/CMakeLists.txt +++ b/quotest/CMakeLists.txt @@ -8,7 +8,27 @@ find_package(${Qt} COMPONENTS Concurrent) add_executable(quotest ${quotest_SRCS}) target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${Qt}::Concurrent ${PROJECT_NAME}) -option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) +set_target_properties(quotest PROPERTIES + VISIBILITY_INLINES_HIDDEN ON + CXX_VISIBILITY_PRESET hidden +) + +if (MSVC) + target_compile_options(quotest PUBLIC /EHsc /W4 + /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 + /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 + /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) +else() + foreach (FLAG W Wall Wpedantic Wextra Wno-unused-parameter Werror=return-type) + CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) + if (COMPILER_${FLAG}_SUPPORTED AND + NOT CMAKE_CXX_FLAGS MATCHES "(^| )-?${FLAG}($| )") + target_compile_options(quotest PUBLIC -${FLAG}) + endif () + endforeach () +endif() + +option(${PROJECT_NAME}_INSTALL_TESTS "install quotest application" ON) add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS "the library functional test suite") diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index d006c7fb..792faabd 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -14,6 +14,7 @@ #include "events/reactionevent.h" #include "events/redactionevent.h" #include "events/simplestateevents.h" +#include "events/roommemberevent.h" #include <QtTest/QSignalSpy> #include <QtCore/QCoreApplication> @@ -101,6 +102,7 @@ private slots: TEST_DECL(sendMessage) TEST_DECL(sendReaction) TEST_DECL(sendFile) + TEST_DECL(sendCustomEvent) TEST_DECL(setTopic) TEST_DECL(changeName) TEST_DECL(sendAndRedact) @@ -125,6 +127,7 @@ private: [[nodiscard]] bool checkRedactionOutcome(const QByteArray& thisTest, const QString& evtIdToRedact); + template <class EventT> [[nodiscard]] bool validatePendingEvent(const QString& txnId); [[nodiscard]] bool checkDirectChat() const; void finishTest(const TestToken& token, bool condition, const char* file, @@ -152,12 +155,14 @@ void TestSuite::doTest(const QByteArray& testName) Q_ARG(TestToken, testName)); } +template <class EventT> bool TestSuite::validatePendingEvent(const QString& txnId) { auto it = targetRoom->findPendingEvent(txnId); return it != targetRoom->pendingEvents().end() && it->deliveryStatus() == EventStatus::Submitted - && (*it)->transactionId() == txnId; + && (*it)->transactionId() == txnId && is<EventT>(**it) + && (*it)->matrixType() == EventT::matrixTypeId(); } void TestSuite::finishTest(const TestToken& token, bool condition, @@ -214,6 +219,7 @@ TestManager::TestManager(int& argc, char** argv) // Big countdown watchdog QTimer::singleShot(180000, this, [this] { + clog << "Time is up, stopping the session\n"; if (testSuite) conclude(); else @@ -340,7 +346,7 @@ TEST_IMPL(loadMembers) TEST_IMPL(sendMessage) { auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); - if (!validatePendingEvent(txnId)) { + if (!validatePendingEvent<RoomMessageEvent>(txnId)) { clog << "Invalid pending event right after submitting" << endl; FAIL_TEST(); } @@ -366,7 +372,7 @@ TEST_IMPL(sendReaction) const auto targetEvtId = targetRoom->messageEvents().back()->id(); const auto key = QStringLiteral("+1"); const auto txnId = targetRoom->postReaction(targetEvtId, key); - if (!validatePendingEvent(txnId)) { + if (!validatePendingEvent<ReactionEvent>(txnId)) { clog << "Invalid pending event right after submitting" << endl; FAIL_TEST(); } @@ -376,7 +382,7 @@ TEST_IMPL(sendReaction) if (actualTargetEvtId != targetEvtId) return false; const auto reactions = targetRoom->relatedEvents( - targetEvtId, EventRelation::Annotation()); + targetEvtId, EventRelation::AnnotationType); // It's a test room, assuming no interference there should // be exactly one reaction if (reactions.size() != 1) @@ -408,7 +414,7 @@ TEST_IMPL(sendFile) clog << "Sending file " << tfName.toStdString() << endl; const auto txnId = targetRoom->postFile( "Test file", new EventContent::FileContent(tfi)); - if (!validatePendingEvent(txnId)) { + if (!validatePendingEvent<RoomMessageEvent>(txnId)) { clog << "Invalid pending event right after submitting" << endl; tf->deleteLater(); FAIL_TEST(); @@ -498,8 +504,8 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, clog << "File event " << txnId.toStdString() << " arrived in the timeline" << endl; - // This part tests visit() - return visit( + // This part tests switchOnType() + return switchOnType( *evt, [&](const RoomMessageEvent& e) { // TODO: check #366 once #368 is implemented @@ -517,6 +523,50 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, return true; } +class CustomEvent : public RoomEvent { +public: + DEFINE_EVENT_TYPEID("quotest.custom", CustomEvent) + + CustomEvent(const QJsonObject& jo) + : RoomEvent(typeId(), jo) + {} + CustomEvent(int testValue) + : RoomEvent(typeId(), + basicEventJson(matrixTypeId(), + QJsonObject { { "testValue"_ls, + toJson(testValue) } })) + {} + + auto testValue() const { return contentPart<int>("testValue"_ls); } +}; +REGISTER_EVENT_TYPE(CustomEvent) + +TEST_IMPL(sendCustomEvent) +{ + auto txnId = targetRoom->postEvent(new CustomEvent(42)); + if (!validatePendingEvent<CustomEvent>(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + FAIL_TEST(); + } + connectUntil( + targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, 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; + + return switchOnType(*evt, + [this, thisTest, &evt](const CustomEvent& e) { + FINISH_TEST(!evt->id().isEmpty() && e.testValue() == 42); + }, + [this, thisTest] (const RoomEvent&) { FAIL_TEST(); }); + }); + return false; + +} + TEST_IMPL(setTopic) { const auto newTopic = connection()->generateTxnId(); // Just a way to make @@ -537,18 +587,41 @@ TEST_IMPL(setTopic) TEST_IMPL(changeName) { - auto* const localUser = connection()->user(); - const auto& newName = connection()->generateTxnId(); // See setTopic() - clog << "Renaming the user to " << newName.toStdString() << endl; - localUser->rename(newName); - connectUntil(localUser, &User::defaultNameChanged, this, - [this, thisTest, localUser, newName] { - FINISH_TEST(localUser->name() == newName); - }); + connectSingleShot(targetRoom, &Room::allMembersLoaded, this, [this, thisTest] { + auto* const localUser = connection()->user(); + const auto& newName = connection()->generateTxnId(); // See setTopic() + clog << "Renaming the user to " << newName.toStdString() + << " in the target room" << endl; + localUser->rename(newName, targetRoom); + connectUntil( + targetRoom, &Room::aboutToAddNewMessages, this, + [this, thisTest, localUser, newName](const RoomEventsRange& evts) { + for (const auto& e : evts) { + if (const auto* rme = eventCast<const RoomMemberEvent>(e)) { + if (rme->stateKey() != localUser->id() + || !rme->isRename()) + continue; + if (!rme->newDisplayName() + || *rme->newDisplayName() != newName) + FAIL_TEST(); + clog << "Member rename successful, renaming the account" + << endl; + const auto newN = newName.mid(0, 5); + localUser->rename(newN); + connectUntil(localUser, &User::defaultNameChanged, this, + [this, thisTest, localUser, newN] { + targetRoom->localUser()->rename({}); + FINISH_TEST(localUser->name() == newN); + }); + return true; + } + } + return false; + }); + }); return false; } - TEST_IMPL(showLocalUsername) { auto* const localUser = connection()->user(); |