aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.appveyor.yml50
-rw-r--r--.clang-format32
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md29
-rw-r--r--.github/ISSUE_TEMPLATE/change-request.md20
-rw-r--r--.github/workflows/ci.yml170
-rw-r--r--.lgtm.yml18
-rw-r--r--CMakeLists.txt237
-rw-r--r--CONTRIBUTING.md30
-rw-r--r--ISSUE_TEMPLATE.md48
-rw-r--r--README.md137
-rw-r--r--autotests/CMakeLists.txt2
-rw-r--r--autotests/callcandidateseventtest.cpp2
-rw-r--r--gtad/data.h.mustache3
-rw-r--r--gtad/gtad.yaml52
-rw-r--r--gtad/operation.cpp.mustache9
-rw-r--r--gtad/operation.h.mustache3
-rw-r--r--lib/accountregistry.cpp97
-rw-r--r--lib/accountregistry.h45
-rw-r--r--lib/application-service/definitions/protocol.h9
-rw-r--r--lib/avatar.h4
-rw-r--r--lib/connection.cpp97
-rw-r--r--lib/connection.h85
-rw-r--r--lib/connectiondata.cpp12
-rw-r--r--lib/connectiondata.h4
-rw-r--r--lib/converters.cpp11
-rw-r--r--lib/converters.h150
-rw-r--r--lib/csapi/account-data.cpp32
-rw-r--r--lib/csapi/account-data.h4
-rw-r--r--lib/csapi/admin.cpp8
-rw-r--r--lib/csapi/administrative_contact.cpp31
-rw-r--r--lib/csapi/administrative_contact.h65
-rw-r--r--lib/csapi/appservice_room_directory.cpp10
-rw-r--r--lib/csapi/appservice_room_directory.h14
-rw-r--r--lib/csapi/banning.cpp11
-rw-r--r--lib/csapi/banning.h10
-rw-r--r--lib/csapi/capabilities.cpp9
-rw-r--r--lib/csapi/content-repo.cpp59
-rw-r--r--lib/csapi/content-repo.h50
-rw-r--r--lib/csapi/create_room.cpp4
-rw-r--r--lib/csapi/create_room.h205
-rw-r--r--lib/csapi/cross_signing.cpp31
-rw-r--r--lib/csapi/cross_signing.h72
-rw-r--r--lib/csapi/definitions/cross_signing_key.h47
-rw-r--r--lib/csapi/definitions/device_keys.h8
-rw-r--r--lib/csapi/definitions/event_filter.h8
-rw-r--r--lib/csapi/definitions/openid_token.h4
-rw-r--r--lib/csapi/definitions/public_rooms_response.h10
-rw-r--r--lib/csapi/definitions/push_condition.h17
-rw-r--r--lib/csapi/definitions/push_rule.h4
-rw-r--r--lib/csapi/definitions/request_email_validation.h5
-rw-r--r--lib/csapi/definitions/request_msisdn_validation.h5
-rw-r--r--lib/csapi/definitions/request_token_response.h8
-rw-r--r--lib/csapi/definitions/room_event_filter.h25
-rw-r--r--lib/csapi/definitions/sync_filter.h8
-rw-r--r--lib/csapi/definitions/third_party_signed.h2
-rw-r--r--lib/csapi/definitions/user_identifier.h5
-rw-r--r--lib/csapi/definitions/wellknown/homeserver.h2
-rw-r--r--lib/csapi/definitions/wellknown/identity_server.h2
-rw-r--r--lib/csapi/device_management.cpp19
-rw-r--r--lib/csapi/device_management.h6
-rw-r--r--lib/csapi/directory.cpp26
-rw-r--r--lib/csapi/directory.h20
-rw-r--r--lib/csapi/event_context.cpp13
-rw-r--r--lib/csapi/event_context.h14
-rw-r--r--lib/csapi/filter.cpp15
-rw-r--r--lib/csapi/filter.h2
-rw-r--r--lib/csapi/inviting.cpp9
-rw-r--r--lib/csapi/inviting.h16
-rw-r--r--lib/csapi/joining.cpp16
-rw-r--r--lib/csapi/joining.h32
-rw-r--r--lib/csapi/keys.cpp16
-rw-r--r--lib/csapi/keys.h68
-rw-r--r--lib/csapi/kicking.cpp4
-rw-r--r--lib/csapi/kicking.h9
-rw-r--r--lib/csapi/knocking.cpp26
-rw-r--r--lib/csapi/knocking.h55
-rw-r--r--lib/csapi/leaving.cpp25
-rw-r--r--lib/csapi/leaving.h11
-rw-r--r--lib/csapi/list_joined_rooms.cpp9
-rw-r--r--lib/csapi/list_joined_rooms.h2
-rw-r--r--lib/csapi/list_public_rooms.cpp24
-rw-r--r--lib/csapi/list_public_rooms.h2
-rw-r--r--lib/csapi/login.cpp9
-rw-r--r--lib/csapi/login.h38
-rw-r--r--lib/csapi/logout.cpp14
-rw-r--r--lib/csapi/logout.h11
-rw-r--r--lib/csapi/message_pagination.cpp9
-rw-r--r--lib/csapi/message_pagination.h34
-rw-r--r--lib/csapi/notifications.cpp10
-rw-r--r--lib/csapi/notifications.h10
-rw-r--r--lib/csapi/openid.cpp8
-rw-r--r--lib/csapi/openid.h9
-rw-r--r--lib/csapi/peeking_events.cpp9
-rw-r--r--lib/csapi/peeking_events.h14
-rw-r--r--lib/csapi/presence.cpp12
-rw-r--r--lib/csapi/presence.h2
-rw-r--r--lib/csapi/profile.cpp30
-rw-r--r--lib/csapi/profile.h12
-rw-r--r--lib/csapi/pusher.cpp9
-rw-r--r--lib/csapi/pusher.h42
-rw-r--r--lib/csapi/pushrules.cpp61
-rw-r--r--lib/csapi/pushrules.h32
-rw-r--r--lib/csapi/read_markers.cpp5
-rw-r--r--lib/csapi/read_markers.h2
-rw-r--r--lib/csapi/receipts.cpp8
-rw-r--r--lib/csapi/receipts.h4
-rw-r--r--lib/csapi/redaction.cpp6
-rw-r--r--lib/csapi/redaction.h9
-rw-r--r--lib/csapi/registration.cpp40
-rw-r--r--lib/csapi/registration.h137
-rw-r--r--lib/csapi/report_content.cpp12
-rw-r--r--lib/csapi/report_content.h3
-rw-r--r--lib/csapi/room_send.cpp8
-rw-r--r--lib/csapi/room_send.h5
-rw-r--r--lib/csapi/room_state.cpp8
-rw-r--r--lib/csapi/room_state.h28
-rw-r--r--lib/csapi/room_upgrades.cpp5
-rw-r--r--lib/csapi/rooms.cpp43
-rw-r--r--lib/csapi/rooms.h18
-rw-r--r--lib/csapi/search.cpp6
-rw-r--r--lib/csapi/search.h26
-rw-r--r--lib/csapi/sso_login_redirect.cpp33
-rw-r--r--lib/csapi/sso_login_redirect.h36
-rw-r--r--lib/csapi/tags.cpp24
-rw-r--r--lib/csapi/tags.h4
-rw-r--r--lib/csapi/third_party_lookup.cpp49
-rw-r--r--lib/csapi/third_party_membership.cpp5
-rw-r--r--lib/csapi/third_party_membership.h15
-rw-r--r--lib/csapi/to_device.cpp8
-rw-r--r--lib/csapi/to_device.h2
-rw-r--r--lib/csapi/typing.cpp6
-rw-r--r--lib/csapi/typing.h6
-rw-r--r--lib/csapi/users.cpp4
-rw-r--r--lib/csapi/users.h6
-rw-r--r--lib/csapi/versions.cpp7
-rw-r--r--lib/csapi/versions.h10
-rw-r--r--lib/csapi/voip.cpp9
-rw-r--r--lib/csapi/wellknown.cpp7
-rw-r--r--lib/csapi/wellknown.h2
-rw-r--r--lib/csapi/whoami.cpp9
-rw-r--r--lib/csapi/whoami.h12
-rw-r--r--lib/eventitem.h14
-rw-r--r--lib/events/accountdataevents.h15
-rw-r--r--lib/events/callanswerevent.h4
-rw-r--r--lib/events/callcandidatesevent.cpp27
-rw-r--r--lib/events/callcandidatesevent.h6
-rw-r--r--lib/events/callhangupevent.cpp36
-rw-r--r--lib/events/callhangupevent.h8
-rw-r--r--lib/events/callinviteevent.h4
-rw-r--r--lib/events/encryptedevent.h14
-rw-r--r--lib/events/encryptedfile.h88
-rw-r--r--lib/events/encryptionevent.cpp8
-rw-r--r--lib/events/encryptionevent.h5
-rw-r--r--lib/events/event.cpp3
-rw-r--r--lib/events/event.h298
-rw-r--r--lib/events/eventcontent.cpp63
-rw-r--r--lib/events/eventcontent.h52
-rw-r--r--lib/events/eventloader.h18
-rw-r--r--lib/events/reactionevent.h2
-rw-r--r--lib/events/receiptevent.cpp29
-rw-r--r--lib/events/receiptevent.h5
-rw-r--r--lib/events/redactionevent.h2
-rw-r--r--lib/events/roomavatarevent.h4
-rw-r--r--lib/events/roomcreateevent.cpp27
-rw-r--r--lib/events/roomcreateevent.h2
-rw-r--r--lib/events/roomevent.cpp13
-rw-r--r--lib/events/roomevent.h52
-rw-r--r--lib/events/roomkeyevent.h8
-rw-r--r--lib/events/roommemberevent.cpp77
-rw-r--r--lib/events/roommemberevent.h58
-rw-r--r--lib/events/roommessageevent.cpp18
-rw-r--r--lib/events/roommessageevent.h25
-rw-r--r--lib/events/roomtombstoneevent.cpp4
-rw-r--r--lib/events/simplestateevents.h19
-rw-r--r--lib/events/stateevent.cpp31
-rw-r--r--lib/events/stateevent.h27
-rw-r--r--lib/events/stickerevent.cpp2
-rw-r--r--lib/events/typingevent.cpp2
-rw-r--r--lib/eventstats.cpp98
-rw-r--r--lib/eventstats.h114
-rw-r--r--lib/function_traits.cpp53
-rw-r--r--lib/function_traits.h93
-rw-r--r--lib/identity/definitions/request_email_validation.h6
-rw-r--r--lib/identity/definitions/request_msisdn_validation.h8
-rw-r--r--lib/jobs/basejob.cpp174
-rw-r--r--lib/jobs/basejob.h94
-rw-r--r--lib/jobs/mediathumbnailjob.cpp8
-rw-r--r--lib/jobs/requestdata.cpp5
-rw-r--r--lib/jobs/requestdata.h5
-rw-r--r--lib/jobs/syncjob.cpp2
-rw-r--r--lib/joinstate.h32
-rw-r--r--lib/logging.cpp2
-rw-r--r--lib/logging.h39
-rw-r--r--lib/mxcreply.cpp70
-rw-r--r--lib/mxcreply.h29
-rw-r--r--lib/networkaccessmanager.cpp78
-rw-r--r--lib/networkaccessmanager.h5
-rw-r--r--lib/qt_connection_util.h30
-rw-r--r--lib/quotient_common.h103
-rw-r--r--lib/room.cpp1196
-rw-r--r--lib/room.h473
-rw-r--r--lib/settings.cpp15
-rw-r--r--lib/settings.h19
-rw-r--r--lib/syncdata.cpp58
-rw-r--r--lib/syncdata.h24
-rw-r--r--lib/uri.cpp22
-rw-r--r--lib/uriresolver.cpp2
-rw-r--r--lib/uriresolver.h19
-rw-r--r--lib/user.cpp35
-rw-r--r--lib/user.h24
-rw-r--r--lib/util.cpp57
-rw-r--r--lib/util.h136
-rw-r--r--quotest/CMakeLists.txt20
-rw-r--r--quotest/quotest.cpp168
-rw-r--r--sonar-project.properties11
215 files changed, 4898 insertions, 3071 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
deleted file mode 100644
index 69a58ba0..00000000
--- a/.appveyor.yml
+++ /dev/null
@@ -1,50 +0,0 @@
-image: Visual Studio 2017
-
-environment:
- CMAKE_ARGS: '-G "NMake Makefiles JOM" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo'
- matrix:
- - QTDIR: C:\Qt\5.13\msvc2017 # Fresh Qt, 32-bit
- VCVARS: "vcvars32.bat"
- PLATFORM: x86
- - QTDIR: C:\Qt\5.13\msvc2017_64 # Fresh Qt, 64-bit
- VCVARS: "vcvars64.bat"
- PLATFORM:
- - QTDIR: C:\Qt\5.9\msvc2017_64 # Oldest supported Qt, 64-bit, E2EE
- VCVARS: "vcvars64.bat"
- QMAKE_E2EE_ARGS: '"DEFINES += Quotient_E2EE_ENABLED USE_INTREE_LIBQOLM" "INCLUDEPATH += olm/include" "LIBS += -Lbuild/olm"'
- CMAKE_E2EE_ARGS: '-DQuotient_ENABLE_E2EE=ON -DOlm_DIR=build/olm'
- PLATFORM:
- - QTDIR: C:\Qt\5.13\msvc2017_64 # Fresh Qt, 64-bit, E2EE
- VCVARS: "vcvars64.bat"
- QMAKE_E2EE_ARGS: '"DEFINES += Quotient_E2EE_ENABLED USE_INTREE_LIBQOLM" "INCLUDEPATH += olm/include" "LIBS += -Lbuild/olm"'
- CMAKE_E2EE_ARGS: '-DQuotient_ENABLE_E2EE=ON -DOlm_DIR=build/olm'
- PLATFORM:
-
-init:
-- call "%QTDIR%\bin\qtenv2.bat"
-- set PATH=C:\Qt\Tools\QtCreator\bin;%PATH%
-- call "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\%VCVARS%" %PLATFORM%
-- cd /D "%APPVEYOR_BUILD_FOLDER%"
-
-before_build:
-- git submodule update --init --recursive
-- git clone https://gitlab.matrix.org/matrix-org/olm.git
-- cmake %CMAKE_ARGS% -Holm -Bbuild/olm
-- cmake --build build/olm
-
-build_script:
-- cmake %CMAKE_ARGS% %CMAKE_E2EE_ARGS% -H. -Bbuild
-- cmake --build build
-
-#after_build:
-#- cmake --build build --target install
-#- 7z a quotient.zip "%DEPLOY_DIR%\"
-
-# Uncomment this to connect to the AppVeyor build worker
-#on_finish:
-# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
-
-test: off
-
-#artifacts:
-#- path: quotient.zip
diff --git a/.clang-format b/.clang-format
index 4510d46e..6e13223e 100644
--- a/.clang-format
+++ b/.clang-format
@@ -23,16 +23,17 @@ AlignAfterOpenBracket: Align
#AlignConsecutiveAssignments: false
#AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
-AlignOperands: true
+AlignOperands: true # 'Align' since ClangFormat 11
#AlignTrailingComments: false
#AllowAllArgumentsOnNextLine: true
#AllowAllConstructorInitializersOnNextLine: true
#AllowAllParametersOfDeclarationOnNextLine: true
-#AllowShortBlocksOnASingleLine: false # 'Empty' since ClangFormat 10
+#AllowShortEnumsOnASingleLine: true
+#AllowShortBlocksOnASingleLine: Empty
#AllowShortCaseLabelsOnASingleLine: false
#AllowShortFunctionsOnASingleLine: All
#AllowShortLambdasOnASingleLine: All
-#AllowShortIfStatementsOnASingleLine: false # 'Never' since ClangFormat 10
+#AllowShortIfStatementsOnASingleLine: Never
#AllowShortLoopsOnASingleLine: false
#AlwaysBreakAfterDefinitionReturnType: None # deprecated
#AlwaysBreakAfterReturnType: None
@@ -43,7 +44,7 @@ AlwaysBreakTemplateDeclarations: Yes
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
- AfterControlStatement: false
+ AfterControlStatement: Never # Switch to MultiLine, once https://bugs.llvm.org/show_bug.cgi?id=47936 is fixed
AfterEnum: false
AfterFunction: true
AfterNamespace: false
@@ -61,22 +62,18 @@ BreakBeforeBraces: Custom
#BreakBeforeInheritanceComma: false
#BreakInheritanceList: BeforeColon
#BreakBeforeTernaryOperators: true
-#BreakConstructorInitializersBeforeComma: false
+#BreakConstructorInitializersBeforeComma: false # deprecated?
#BreakConstructorInitializers: BeforeComma
#BreakStringLiterals: true
ColumnLimit: 80
-CompactNamespaces: false
-ConstructorInitializerAllOnOneLineOrOnePerLine: true
+#CompactNamespaces: false
+#ConstructorInitializerAllOnOneLineOrOnePerLine: false
#ConstructorInitializerIndentWidth: 4
#ContinuationIndentWidth: 4
Cpp11BracedListStyle: false
#DeriveLineEnding: true
#DerivePointerAlignment: false
FixNamespaceComments: true
-ForEachMacros:
- - foreach
- - Q_FOREACH
- - forever
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<Qt.+/'
@@ -89,10 +86,10 @@ IncludeCategories:
Priority: 4
- Regex: '.*'
Priority: 1
-#IncludeIsMainRegex: '(_test)?$'
+IncludeIsMainRegex: '(_test)?$'
#IncludeIsMainSourceRegex: ''
#IndentCaseLabels: false
-#IndentGotoLabels: false # Uncomment once on ClangFormat 10
+IndentGotoLabels: false
IndentPPDirectives: AfterHash
#IndentWidth: 4
#IndentWrappedFunctionNames: false
@@ -112,7 +109,7 @@ PenaltyReturnTypeOnItsOwnLine: 60
#PointerAlignment: Left
#ReflowComments: true
#SortIncludes: true
-SortUsingDeclarations: false
+#SortUsingDeclarations: true
#SpaceAfterCStyleCast: false
#SpaceAfterLogicalNot: false
#SpaceAfterTemplateKeyword: true
@@ -121,8 +118,8 @@ SortUsingDeclarations: false
#SpaceBeforeCtorInitializerColon: true
#SpaceBeforeInheritanceColon: true
#SpaceBeforeParens: ControlStatements
-SpaceBeforeRangeBasedForLoopColon: true
-#SpaceInEmptyBlock: false # Uncomment once on ClangFormat 10
+#SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
#SpaceInEmptyParentheses: false
#SpacesBeforeTrailingComments: 1
#SpacesInAngles: false
@@ -132,10 +129,11 @@ SpaceBeforeRangeBasedForLoopColon: true
#SpacesInParentheses: false
#SpacesInSquareBrackets: false
#SpaceBeforeSquareBrackets: false
-Standard: Cpp11 # Once on ClangFormat 10, switch to Cpp17
+Standard: c++17
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
+ - DEFINE_EVENT_TYPEID
TabWidth: 4
#UseCRLF: false
#UseTab: Never
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..637cb4b3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,29 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+<!-- Put below a clear and concise summary of what the bug is. -->
+
+**To Reproduce**
+Steps to reproduce the behaviour, and the description of the actual result:
+1.
+2.
+3.
+
+**Expected behavior**
+<!-- Put below a clear and concise description of what you expected to happen. -->
+
+**Is it environment-specific?**
+<!-- If yes, please provide details below; if no, delete this section -->
+ - OS: [e.g. Windows 11]
+ - Version of the library [e.g. 0.6.10]
+ - Linkage: static, dynamic, any
+
+**Additional context**
+<!-- Add below any other context about the problem. -->
diff --git a/.github/ISSUE_TEMPLATE/change-request.md b/.github/ISSUE_TEMPLATE/change-request.md
new file mode 100644
index 00000000..09deeaa6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/change-request.md
@@ -0,0 +1,20 @@
+---
+name: Change request
+about: Suggest an idea or improvement for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your request related to a problem?**
+<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
+
+**Describe the suggested change/improvement you'd like**
+<!-- A clear and concise description of what you want to happen. -->
+
+**Describe alternatives you've considered**
+<!-- A clear and concise description of any alternative solutions or features you've considered. -->
+
+**Additional context**
+<!-- Add any other context or screenshots about the feature request here. -->
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 24681460..a1b6f0c0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,27 +9,52 @@ defaults:
run:
shell: bash
+concurrency: ci-${{ github.ref }}
+
jobs:
CI:
runs-on: ${{ matrix.os }}
+ continue-on-error: ${{ matrix.update-api != '' }} # the current upstream API definitions are expected to fail the test
strategy:
fail-fast: false
max-parallel: 1
matrix:
- os: [ubuntu-18.04, macos-10.15]
+ os: [ubuntu-20.04, macos-10.15]
compiler: [ GCC, Clang ]
+ qt-version: [ '5.12.10' ]
# Not using binary values here, to make the job captions more readable
- e2ee: [ '', 'E2EE' ]
+ e2ee: [ '' ]
update-api: [ '', 'update-api' ]
+ sonar: [ '' ]
+ platform: [ '' ]
+ qt-arch: [ '' ]
exclude:
- os: macos-10.15
compiler: GCC
- - e2ee: '' # Somewhat reduce the number of combinations to check
- update-api: 'update-api'
+ include:
+ - os: ubuntu-latest
+ compiler: GCC
+ qt-version: '5.12.10'
+ sonar: 'sonar'
+ - os: windows-2019
+ compiler: MSVC
+ platform: x64
+ qt-version: '5.12.10'
+ qt-arch: win64_msvc2017_64
+ - os: windows-2019
+ compiler: MSVC
+ platform: x64
+ qt-version: '5.12.10'
+ qt-arch: win64_msvc2017_64
+ update-api: update-api
+
+ env:
+ SONAR_SERVER_URL: 'https://sonarcloud.io'
steps:
- uses: actions/checkout@v2
with:
+ fetch-depth: 0
submodules: ${{ matrix.e2ee != '' }}
- name: Cache Qt
@@ -37,15 +62,16 @@ jobs:
uses: actions/cache@v2
with:
path: ${{ runner.workspace }}/Qt
- key: ${{ runner.os }}-QtCache
+ key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache
- name: Install Qt
uses: jurplel/install-qt-action@v2.11.1
with:
- version: '5.9.9'
+ version: ${{ matrix.qt-version }}
+ arch: ${{ matrix.qt-arch }}
cached: ${{ steps.cache-qt.outputs.cache-hit }}
- - name: Install Ninja (macOS)
+ - name: Install Ninja (macOS/Windows)
if: ${{ !startsWith(matrix.os, 'ubuntu') }}
uses: seanmiddleditch/gha-setup-ninja@v3
@@ -53,17 +79,22 @@ 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: |
if [ "${{ matrix.compiler }}" == "GCC" ]; then
- if [ -n "${{ matrix.update-api }}" ]; then VERSION_POSTFIX='-9'; fi
- echo "CC=gcc$VERSION_POSTFIX" >>$GITHUB_ENV
- echo "CXX=g++$VERSION_POSTFIX" >>$GITHUB_ENV
- else
- echo "CC=clang" >>$GITHUB_ENV
- echo "CXX=clang++" >>$GITHUB_ENV
+ CXX_VERSION_POSTFIX='-10'
+ echo "CC=gcc$CXX_VERSION_POSTFIX" >>$GITHUB_ENV
+ echo "CXX=g++$CXX_VERSION_POSTFIX" >>$GITHUB_ENV
+ elif [[ '${{ matrix.compiler }}' == 'Clang' ]]; then
+ if [[ '${{ runner.os }}' == 'Linux' ]]; then
+ CXX_VERSION_POSTFIX='-11'
+ # Do CodeQL analysis on one of Linux branches
+ echo "CODEQL_ANALYSIS=true" >>$GITHUB_ENV
+ fi
+ echo "CC=clang$CXX_VERSION_POSTFIX" >>$GITHUB_ENV
+ echo "CXX=clang++$CXX_VERSION_POSTFIX" >>$GITHUB_ENV
fi
if grep -q 'refs/tags' <<<'${{ github.ref }}'; then
VERSION="$(git describe --tags)"
@@ -73,48 +104,127 @@ 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
+
+ CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \
+ -DBUILD_SHARED_LIBS=false \
+ -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
+ 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
+ if: matrix.compiler == 'MSVC'
+ 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: Build and install olm
if: matrix.e2ee
+ working-directory: ${{ runner.workspace }}
run: |
- cd ${{ runner.workspace }}
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: Pull CS API and build GTAD
if: matrix.update-api
+ working-directory: ${{ runner.workspace }}
run: |
- cd ${{ runner.workspace }}
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
- cmake --build 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 }}/gtad/gtad" \
+ -DGTAD_PATH=${{ runner.workspace }}/build/gtad/gtad" \
>>$GITHUB_ENV
- echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN and API files regeneration" >>$GITHUB_ENV
+ echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with API files regeneration" >>$GITHUB_ENV
+
+ - name: Initialize CodeQL tools
+ if: env.CODEQL_ANALYSIS
+ uses: github/codeql-action/init@v1
+ with:
+ languages: cpp
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Configure libQuotient
- run: cmake -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }}
+ run: |
+ 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/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_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: |
+ ctest --test-dir $BUILD_PATH --output-on-failure
+ [[ -z "$TEST_USER" ]] || \
+ $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: |
- [[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN"
- timeout-minutes: 5 # quotest is supposed to finish within 3 minutes, actually
+ 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
diff --git a/.lgtm.yml b/.lgtm.yml
index f6dfb229..308675a8 100644
--- a/.lgtm.yml
+++ b/.lgtm.yml
@@ -6,15 +6,13 @@ path_classifiers:
extraction:
cpp:
prepare:
- packages: # Assuming package base of cosmic
- - ninja-build
- - qt5-default
+ packages: # Assuming package base of eoan
- qtmultimedia5-dev
- after_prepare:
- - git clone https://gitlab.matrix.org/matrix-org/olm.git
- - pushd olm
- - cmake . -Bbuild -GNinja
- - cmake --build build
- - popd
+# after_prepare:
+# - git clone https://gitlab.matrix.org/matrix-org/olm.git
+# - pushd olm
+# - cmake . -Bbuild -GNinja
+# - cmake --build build
+# - popd
configure:
- command: "cmake . -GNinja -DOlm_DIR=olm/build"
+ command: "cmake . -GNinja" # -DOlm_DIR=olm/build"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 39b1b03a..adb5be7b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,6 @@
-cmake_minimum_required(VERSION 3.10)
+# Officially CMake 3.16+ is needed but LGTM.com still sits on eoan that only
+# has CMake 3.13
+cmake_minimum_required(VERSION 3.13)
if (POLICY CMP0092)
cmake_policy(SET CMP0092 NEW)
endif()
@@ -37,7 +39,7 @@ if (MSVC)
/wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706
/wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027)
else()
- foreach (FLAG Wall W Wpedantic Wextra Werror=return-type Wno-unused-parameter
+ foreach (FLAG Wall Wpedantic Wextra Werror=return-type Wno-unused-parameter
Wno-gnu-zero-variadic-macro-arguments fvisibility-inlines-hidden)
CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED)
if ( COMPILER_${FLAG}_SUPPORTED AND
@@ -72,9 +74,18 @@ message(STATUS " Header files will be installed to ${CMAKE_INSTALL_PREFIX}/${${
# Instruct CMake to run moc automatically when needed.
set(CMAKE_AUTOMOC ON)
-find_package(Qt5 5.9 REQUIRED Core Network Gui Multimedia Test)
-get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE)
-message(STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}")
+option(BUILD_WITH_QT6 "Build Quotient with Qt 6 (EXPERIMENTAL)" OFF)
+
+if (BUILD_WITH_QT6)
+ set(QtMinVersion "6.0")
+else()
+ set(QtMinVersion "5.12")
+ set(QtExtraModules "Multimedia") # See #483
+endif()
+string(REGEX REPLACE "^(.).*" "Qt\\1" Qt ${QtMinVersion}) # makes "Qt5" or "Qt6"
+find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModules})
+get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE)
+message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}")
if (${PROJECT_NAME}_ENABLE_E2EE)
if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM)
@@ -112,50 +123,59 @@ if (${PROJECT_NAME}_ENABLE_E2EE)
endif ()
# Set up source files
-set(lib_SRCS
- 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/syncdata.cpp
- lib/settings.cpp
- lib/networksettings.cpp
- lib/converters.cpp
- lib/util.cpp
- lib/encryptionmanager.cpp
- lib/eventitem.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/jobs/requestdata.cpp
- lib/jobs/basejob.cpp
- lib/jobs/syncjob.cpp
- lib/jobs/mediathumbnailjob.cpp
- lib/jobs/downloadfilejob.cpp
+list(APPEND lib_SRCS
+ lib/quotient_common.h
+ lib/function_traits.h lib/function_traits.cpp
+ 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/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/encryptionmanager.h lib/encryptionmanager.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/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/reactionevent.cpp
+ 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/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
)
# Configure API files generation
@@ -165,48 +185,50 @@ set(FULL_CSAPI_DIR lib/${CSAPI_DIR})
set(ASAPI_DEF_DIR application-service/definitions)
set(ISAPI_DEF_DIR identity/definitions)
-if (GTAD_PATH)
+set(API_GENERATION_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)
+ get_filename_component(ABS_GTAD_PATH "${ABS_GTAD_PATH}" PROGRAM PROGRAM_ARGS GTAD_ARGS)
+ if (EXISTS ${ABS_GTAD_PATH})
+ get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/data/api" REALPATH)
+ if (NOT IS_DIRECTORY ${ABS_API_DEF_PATH})
+ # Check the old place of API files
+ get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH)
+ endif ()
+ if (IS_DIRECTORY ${ABS_API_DEF_PATH})
+ set(API_GENERATION_ENABLED 1)
+ else ()
+ message( WARNING "${MATRIX_DOC_PATH} doesn't seem to point to a valid matrix-doc repo; disabling API stubs generation")
+ endif ()
+ else (EXISTS ${ABS_GTAD_PATH})
+ message( WARNING "${GTAD_PATH} is not executable; disabling API stubs generation")
+ endif ()
endif ()
-if (MATRIX_DOC_PATH)
- get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH)
-endif ()
-if (ABS_GTAD_PATH AND ABS_API_DEF_PATH)
+if (API_GENERATION_ENABLED)
message( STATUS "Using GTAD at ${ABS_GTAD_PATH}" )
- message( STATUS "Using API files at ${ABS_API_DEF_PATH}" )
- set(API_GENERATION_ENABLED 1)
+ message( STATUS "Found API files at ${ABS_API_DEF_PATH}" )
if (NOT CLANG_FORMAT)
set(CLANG_FORMAT clang-format)
endif()
- get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM)
- 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")
+ get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM PROGRAM_ARGS CLANG_FORMAT_ARGS)
+ if (NOT ABS_CLANG_FORMAT)
+ message( WARNING "${CLANG_FORMAT} is NOT FOUND; API files won't be formatted")
endif ()
- if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.12.0")
- # 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")
- endif()
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
key_backup.yaml- # immature and buggy in terms of API definition
sync.yaml- # we have a better handcrafted implementation
+ ${GTAD_ARGS}
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/lib
SOURCES gtad/gtad.yaml
gtad/data.h.mustache
@@ -215,53 +237,33 @@ if (ABS_GTAD_PATH AND ABS_API_DEF_PATH)
${API_DEFS}
VERBATIM
)
- add_custom_target(update-api DEPENDS generate-unformatted-api)
- if (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")
-add_feature_info(EnableApiFormatting "${API_FORMATTING_ENABLED}"
- "formatting of generated API files with clang-format")
-
-# 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})
target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_TO_ASCII)
-if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0"
- AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553
- target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h)
-endif ()
+
+target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
+ ${PROJECT_NAME}_VERSION_MINOR=${PROJECT_VERSION_MINOR} ${PROJECT_NAME}_VERSION_PATCH=${PROJECT_VERSION_PATCH}
+ ${PROJECT_NAME}_VERSION_STRING=\"${PROJECT_VERSION}\")
if (${PROJECT_NAME}_ENABLE_E2EE)
target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_E2EE_ENABLED)
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES
+ CXX_STANDARD 20
+ CXX_EXTENSIONS OFF
VERSION "${PROJECT_VERSION}"
SOVERSION ${API_VERSION}
INTERFACE_${PROJECT_NAME}_MAJOR_VERSION ${API_VERSION}
@@ -269,8 +271,16 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY
COMPATIBLE_INTERFACE_STRING ${PROJECT_NAME}_MAJOR_VERSION)
+# C++17 required, C++20 desired (see above)
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
+# TODO: Bump the CMake requirement and drop the version check here once
+# LGTM upgrades
+if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0"
+ AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553
+ target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h)
+endif ()
+
target_include_directories(${PROJECT_NAME} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib>
$<INSTALL_INTERFACE:${${PROJECT_NAME}_INSTALL_INCLUDEDIR}>
@@ -279,7 +289,10 @@ if (${PROJECT_NAME}_ENABLE_E2EE)
target_link_libraries(${PROJECT_NAME} QtOlm)
set(FIND_DEPS "find_dependency(QtOlm)") # For QuotientConfig.cmake.in
endif()
-target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia)
+target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui)
+if (Qt STREQUAL Qt5) # See #483
+ target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia)
+endif()
configure_file(${PROJECT_NAME}.pc.in ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc @ONLY NEWLINE_STYLE UNIX)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f09b1529..3eeac68c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -155,18 +155,20 @@ just don't bankrupt us with it. Refactoring is welcome.
### Code style and formatting
-As of Quotient 0.6, the C++ standard for newly written code is C++17, with a few
-restrictions, notably:
+As of Quotient 0.6, the C++ standard for newly written code is C++17 with C++20
+compatibility and a few restrictions, notably:
* standard library's _deduction guides_ cannot be used to lighten up syntax
in template instantiation, i.e. you have to still write
- `std::array<int, 2> { 1, 2 }` instead of `std::array { 1, 2 }` or use helpers
- like `std::make_pair` - once we move over to the later Apple toolchain, this
- will be no more necessary.
-* enumerators and slots cannot have `[[attributes]]` because moc of Qt 5.9
- chokes on them. This will be lifted when we move on to Qt 5.12 for the oldest
- supported version.
-* things from `std::filesystem` cannot be used until we push the oldest
- required g++/libc to version 8.
+ `std::array<int, 2> { 1, 2 }` instead of `std::array { 1, 2 }` (or use
+ `Quotient::make_array` helper from `util.h`), use `std::make_pair` to create
+ pairs etc. - once we move over to the later Apple toolchain, this will be
+ no more necessary;
+* enumerators and slots cannot have `[[attributes]]` because moc from Qt 5.12
+ chokes on them - this will be lifted when we move on to Qt 5.13 for the oldest
+ supported version, in the meantime use `Q_DECL_DEPRECATED` and similar Qt
+ macros - they expand to nothing when the code is passed to moc.
+* explicit lists in lambda captures are preferred over `[=]`; note that C++20
+ deprecates implicit `this` capture in `[=]`.
The code style is defined by `.clang-format`, and in general, all C++ files
should follow it. Files with minor deviations from the defined style are still
@@ -408,7 +410,7 @@ I (Kitsune) will be very glad to help you out.
The types map in `gtad.yaml` is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:":
* `avoidCopy` - this attribute defines whether a const ref should be used instead of a value. For basic types like int this is obviously unnecessary; but compound types like `QVector` should rather be taken by reference when possible.
* `moveOnly` - some types are not copyable at all and must be moved instead (an obvious example is anything "tainted" with a member of type `std::unique_ptr<>`). The template will use `T&&` instead of `T` or `const T&` to pass such types around.
-* `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h` - a substitute for `std::optional` from C++17 (we're still at C++14 yet).
+* `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h`, a drop-in upgrade over `std::optional`.
* `omittedValue` - an alternative for `useOmittable`, just provide a value used for an omitted parameter. This is used for bool parameters which normally are considered false if omitted (or they have an explicit default value, passed in the "official" GTAD's `defaultValue` variable).
* `initializer` - this is a _partial_ (see GTAD and Mustache documentation for explanations but basically it's a variable that is a Mustache template itself) that specifies how exactly a default value should be passed to the parameter. E.g., the default value for a `QString` parameter is enclosed into `QStringLiteral`.
@@ -439,9 +441,9 @@ on any platform double-check that it's not your code causing it - and fix
### clang-format
-We strongly recommend using clang-format or, even better, use an IDE that
-supports it. This will lay over a tedious task of following the assumed
-code style from your shoulders (and fingers) to your computer.
+We strongly recommend using clang-format (version 10 or newer) or, even better,
+use an IDE that supports it. This will lay over a tedious task of following
+the assumed code style from your shoulders (and fingers) to your computer.
### Other tools
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
deleted file mode 100644
index 1e3f172b..00000000
--- a/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,48 +0,0 @@
-<!--
-
-This is a bug report template. By following the instructions below and
-filling out the sections with your information, you will help the us to get all
-the necessary data to fix your issue.
-
-You can also preview your report before submitting it. You may remove sections
-that aren't relevant to your particular case.
-
-Text between <!-- and --​> marks will be invisible in the report.
-
--->
-
-### Description
-
-Describe here the problem that you are experiencing, or the feature
-you are requesting.
-
-### Steps to reproduce
-
-- For bugs, list the steps
-- that reproduce the bug
-- using hyphens as bullet points
-
-Describe how what happens differs from what you expected.
-
-libQuotient-based clients either have a log file or dump log
-to the standard output. If you can identify any log snippets relevant
-to your issue, please include those here (please be careful to remove
-any personal or private data):
-
-### Version information
-
-<!-- IMPORTANT: please answer the following questions,
- to help us narrow down the problem -->
-
-- **The client application**:
-<!-- the problem might be not with the library but with the client -->
-- **libQuotient version if you know it**:
-<!-- try to find it basing on the client version -->
-- **Qt version**:
-<!-- for Linux systems, it's usually installed system-wide; for other OSes,
-as well as Flatpak/AppImage/etc. containerised environments,
-it's a version used in the container. -->
-- **Install method**:
-<!-- package manager/Flatpak/archive downloaded (from which site?) -->
-- **Platform**:
-<!-- Operating system and anything about your platform you think can be relevant -->
diff --git a/README.md b/README.md
index c993c31e..71f2d04c 100644
--- a/README.md
+++ b/README.md
@@ -8,14 +8,13 @@
[![](https://img.shields.io/cii/percentage/1023.svg?label=CII%20best%20practices)](https://bestpractices.coreinfrastructure.org/projects/1023/badge)
![](https://img.shields.io/github/commit-activity/y/quotient-im/libQuotient.svg)
[![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/quotient-im/libQuotient.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/quotient-im/libQuotient/context:cpp)
-[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
+[![merge-chance-badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fmerge-chance.info%2Fbadge%3Frepo%3Dquotient-im/libquotient)](https://merge-chance.info/target?repo=quotient-im/libquotient)
The Quotient project aims to produce a Qt5-based SDK to develop applications
for [Matrix](https://matrix.org). libQuotient is a library that enables client
applications. It is the backbone of
[Quaternion](https://github.com/quotient-im/Quaternion),
-[Spectral](https://matrix.org/docs/projects/client/spectral.html) and
-other projects.
+[NeoChat](https://matrix.org/docs/projects/client/neo-chat) and other projects.
Versions 0.5.x and older use the previous name - libQMatrixClient.
## Contacts
@@ -38,61 +37,62 @@ and bundle it with your application.
### Pre-requisites
- A recent Linux, macOS or Windows system (desktop versions are known to work;
mobile operating systems where Qt is available might work too)
- - Recent enough Linux examples: Debian Buster; Fedora 28; openSUSE Leap 15;
- Ubuntu Bionic Beaver.
-- Qt 5 (either Open Source or Commercial), 5.9 or higher;
- 5.12 is recommended, especially if you use qmake
-- A build configuration tool (CMake is recommended, qmake is supported):
- - CMake 3.10 or newer (from your package management system or
- [the official website](https://cmake.org/download/))
- - or qmake (comes with Qt)
-- A C++ toolchain with _reasonably complete_ C++17 support:
- - GCC 7 (Windows, Linux, macOS), Clang 6 (Linux), Apple Clang 10 (macOS)
- and Visual Studio 2017 (Windows) are the oldest officially supported.
-- Any build system that works with CMake and/or qmake should be fine:
- GNU Make, ninja (any platform), NMake, jom (Windows) are known to work.
+ - Recent enough Linux examples: Debian Bullseye; Fedora 33; openSUSE Leap 15.3;
+ Ubuntu Focal Fossa.
+- Qt 5 (either Open Source or Commercial), 5.12 or higher
+- CMake 3.16 or newer (from your package management system or
+ [the official website](https://cmake.org/download/))
+- A C++ toolchain with complete (as much as possible) C++17 and basic C++20:
+ - GCC 10 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS)
+ and Visual Studio 2019 (Windows) are the oldest officially supported.
+- Any build system that works with CMake should be fine:
+ GNU Make and ninja on any platform, NMake and jom on Windows are known to work.
+ Ninja is recommended.
#### Linux
-Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to run cmake/qmake and look at error messages. The library is entirely offscreen (QtCore and QtNetwork are essential) but it also depends on QtGui in order to handle avatar thumbnails.
+Just install things from the list above using your preferred package manager.
+If your Qt package base is fine-grained you might want to run cmake and look
+at error messages. The library is entirely offscreen but aside from QtCore and
+QtNetwork it also depends on QtGui in order to handle avatar thumbnails.
#### macOS
-`brew install qt5` should get you a recent Qt5. If you plan to use CMake, you will need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)`
+`brew install qt5` should get you a recent Qt5. You may need to pass
+`-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)` to make it aware of the Qt location.
#### Windows
-Install Qt5, using their official installer; if you plan to build with CMake,
-make sure to tick the CMake box in the list of installed components.
-
-The commands in further sections imply that cmake/qmake is in your PATH,
-otherwise you have to prepend those commands with actual paths. As an option
-it's a good idea to run a `qtenv2.bat` script that can be found in
-`C:\Qt\<Qt version>\<toolchain>\bin` (assuming you installed Qt to `C:\Qt`);
-the only thing it does is adding necessary paths to PATH. You might not want
-to run that script on system startup but it's very handy to setup
-the environment before building. For CMake you can alternatively point
-`CMAKE_PREFIX_PATH` to your Qt installation and leave PATH unchanged; but
-in that case you'll have to supply the full path to CMake when calling it.
+Install Qt5 using their official installer; make sure to tick the CMake box
+in the list of installed components unless you already have it installed.
+
+The commands in further sections imply that cmake is in your PATH, otherwise
+you have to prepend those commands with actual paths. It's a good idea to run
+a `qtenv2.bat` script that can be found in `C:\Qt\<Qt version>\<toolchain>\bin`
+(assuming you installed Qt to `C:\Qt`) if you're building from the command line;
+the script adds necessary paths to PATH. You might not want to run that script
+on system startup but it's very handy to setup the environment before building.
+Alternatively you can add the Qt path to `CMAKE_PREFIX_PATH` and leave PATH
+unchanged.
### Using the library
-If you use CMake, `find_package(Quotient)` sets up the client code to use
-libQuotient, assuming the library development files are installed. There's no
-documented procedure to use a preinstalled library with qmake; consider
-introducing a submodule in your source tree and build it along with the rest
-of the application for now. Note also that qmake is considered for phase-out
-in Qt 6 so you should probably think of moving over to CMake eventually.
+If you're just starting a project using libQuotient from scratch, you can copy
+`quotest/CMakeLists.txt` to your project and change `quotest` to your
+project name. If you already have an existing CMakeLists.txt, you need to insert
+a `find_package(Quotient REQUIRED)` line to an appropriate place in it (use
+`find_package(Quotient)` if libQuotient is not a hard dependency for you) and
+then add `Quotient` to your `target_link_libraries()` line.
Building with dynamic linkage is only tested on Linux at the moment and is
a recommended way of linking your application with libQuotient on this platform.
Static linkage is the default on Windows/macOS; feel free to experiment
with dynamic linking and submit PRs if you get reusable results.
-[Quotest](quotest), the test application that comes with libQuotient, includes
-most common use cases such as sending messages, uploading files,
-setting room state etc.; for more extensive usage check out the source code
-of [Quaternion](https://github.com/quotient-im/Quaternion)
-(the reference client of Quotient) or [Spectral](https://gitlab.com/b0/spectral).
-
-To ease the first step, `quotest/CMakeLists.txt` is a good starting point
-for your own CMake-based project using libQuotient.
+As for the actual API usage, a (very basic) overview can be found at
+[the respective wiki page](https://github.com/quotient-im/libQuotient/wiki/libQuotient-overview).
+Beyond that, looking at [Quotest](quotest) - the test application that comes
+with libQuotient - may help you with most common use cases such as sending
+messages, uploading files, setting room state etc. For more extensive usage
+feel free to check out (and copy, with appropriate attribution) the source code
+of [Quaternion](https://github.com/quotient-im/Quaternion) (the reference client
+for libQuotient) or [NeoChat](https://invent.kde.org/network/neochat).
## Building the library
[The source code is at GitHub](https://github.com/quotient-im/libQuotient).
@@ -101,28 +101,29 @@ along with submodules is strongly recommended. If you want to hack on
the library as a part of another project (e.g. you are working on Quaternion
but need to do some changes to the library code), it makes sense
to make a recursive check out of that project (in this case, Quaternion)
-and update the library submodule (also recursively) to its master branch.
+and update the library submodule (also recursively) within the appropriate
+branch. Be mindful of API compatibility restrictions: e.g., Quaternion 0.0.95
+will not build with the master branch of libQuotient.
Tags consisting of digits and periods represent released versions; tags ending
with `-betaN` or `-rcN` mark pre-releases. If/when packaging pre-releases,
it is advised to replace a dash with a tilde.
-### CMake-based
-In the root directory of the project sources:
+The following commands issued in the root directory of the project sources:
```shell script
mkdir build_dir
cd build_dir
cmake .. # [-D<cmake-variable>=<value>...], see below
cmake --build . --target all
```
-This will get you the compiled library in `build_dir` inside your project
-sources. Static builds are tested on all supported platforms, building
-the library as a shared object (aka dynamic library) is supported on Linux
-and macOS but is very likely to be broken on Windows.
-
-The first CMake invocation configures the build. You can pass CMake variables,
-such as `-DCMAKE_PREFIX_PATH="path1;path2;..."` and
-`-DCMAKE_INSTALL_PREFIX=path` here if needed.
+will get you a compiled library in `build_dir` inside your project sources.
+Static builds are tested on all supported platforms, building the library as
+a shared object (aka dynamic library) is supported on Linux and macOS but is
+very likely to be broken on Windows.
+
+The first CMake invocation above configures the build. You can pass CMake
+variables (such as `-DCMAKE_PREFIX_PATH="path1;path2;..."` and
+`-DCMAKE_INSTALL_PREFIX=path`) here if needed.
[CMake documentation](https://cmake.org/cmake/help/latest/index.html)
(pick the CMake version at the top of the page that you use) describes
the standard variables coming with CMake. On top of them, Quotient introduces:
@@ -160,22 +161,6 @@ with the _installed_ library. Installation of the `quotest` binary
along with the rest of the library can be skipped
by setting `Quotient_INSTALL_TESTS` to `OFF`.
-### qmake-based
-The library provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `quotest.pro` that will build a minimal example of library usage for you. In the root directory of the project sources:
-```shell script
-qmake quotest.pro
-make all
-```
-This will get you `debug/quotest` and `release/quotest`
-console executables that login to the Matrix server at matrix.org with
-credentials of your choosing (pass the username and password as arguments),
-run a sync long-polling loop and do some tests of the library API. Note that
-qmake didn't really know about C++17 until Qt 5.12 so if your Qt is older
-you may have quite a bit of warnings during the compilation process.
-
-Installing the standalone library with qmake is not implemented yet; PRs are
-welcome though.
-
## Troubleshooting
#### Building fails
@@ -196,11 +181,11 @@ libQuotient uses Qt's logging categories to make switching certain types of logg
quotient.<category>.<level>=<flag>
```
where
-- `<category>` is one of: `main`, `jobs`, `jobs.sync`, `events`, `events.state`
- (covering both the "usual" room state and account data), `events.messages`,
- `events.ephemeral`, `e2ee` and `profiler` (you can always find the full list
- in `lib/logging.cpp`)
-- `<level>` is one of `debug`, `info`, and `warning`
+- `<category>` is one of: `main`, `jobs`, `jobs.sync`, `jobs.thumbnail`,
+ `events`, `events.state` (covering both the "usual" room state and account
+ data), `events.messages`, `events.ephemeral`, `e2ee` and `profiler` (you can
+ always find the full list in `lib/logging.cpp`);
+- `<level>` is one of `debug`, `info`, and `warning`;
- `<flag>` is either `true` or `false`.
`*` can be used as a wildcard for any part between two dots, and semicolon is used for a separator. Latter statements override former ones, so if you want to switch on all debug logs except `jobs` you can set
diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt
index 07f1f046..282ab036 100644
--- a/autotests/CMakeLists.txt
+++ b/autotests/CMakeLists.txt
@@ -7,7 +7,7 @@ include(CMakeParseArguments)
function(QUOTIENT_ADD_TEST)
cmake_parse_arguments(ARG "" "NAME" "" ${ARGN})
add_executable(${ARG_NAME} ${ARG_NAME}.cpp)
- target_link_libraries(${ARG_NAME} Qt5::Core Qt5::Test Quotient)
+ target_link_libraries(${ARG_NAME} ${Qt}::Core ${Qt}::Test Quotient)
add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME})
endfunction()
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/gtad/data.h.mustache b/gtad/data.h.mustache
index a2193380..1b511262 100644
--- a/gtad/data.h.mustache
+++ b/gtad/data.h.mustache
@@ -1,8 +1,7 @@
{{!
SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
SPDX-License-Identifier: LGPL-2.1-or-later
-}}
-{{>preamble}}
+}}{{>preamble}}
#pragma once
#include "converters.h"
diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml
index adf5024a..943ac013 100644
--- a/gtad/gtad.yaml
+++ b/gtad/gtad.yaml
@@ -68,6 +68,9 @@ analyzer:
- dateTime:
type: QDateTime
initializer: QDateTime::fromString("{{defaultValue}}")
+ - uri:
+ type: QUrl
+ initializer: QUrl::fromEncoded("{{defaultValue}}")
- //: &QString
type: QString
initializer: QStringLiteral("{{defaultValue}}")
@@ -77,28 +80,31 @@ analyzer:
+on:
- object: &QJsonObject { type: QJsonObject }
- $ref:
- - +set: { moveOnly: }
+ - +set:
+ moveOnly:
+ imports: '"events/eventloader.h"'
+ +on:
+ - /state_event.yaml$/: StateEventPtr
+ - /room_event.yaml$/: RoomEventPtr
+ - /event.yaml$/: EventPtr
+ - /m\.room\.member/: void # Skip resolving; see EventsArray<> below
+ - +set:
+ # This renderer actually applies to all $ref things
+ _importRenderer: '"{{#segments}}{{_}}{{#_join}}/{{/_join}}{{/segments}}.h"'
+on:
- - /state_event.yaml$/:
- { type: StateEventPtr, imports: "events/eventloader.h" }
- - /room_event.yaml$/:
- { type: RoomEventPtr, imports: "events/eventloader.h" }
- - /event.yaml$/:
- { type: EventPtr, imports: "events/eventloader.h" }
- - /m\.room\.member$/: void # Skip resolving; see EventsArray<> below
- - '/^(\./)?definitions/request_email_validation.yaml$/':
- title: EmailValidationData
- - '/^(\./)?definitions/request_msisdn_validation.yaml$/':
- title: MsisdnValidationData
- - /_filter.yaml$/: # Event/RoomEventFilters do NOT need Omittable<>
- - /public_rooms_response.yaml$/: { _inline: true }
- - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types
+ - '/^(\./)?definitions/request_email_validation.yaml$/':
+ title: EmailValidationData
+ - '/^(\./)?definitions/request_msisdn_validation.yaml$/':
+ title: MsisdnValidationData
+ - /_filter.yaml$/: # Event/RoomEventFilters do NOT need Omittable<>
+ - /public_rooms_response.yaml$/: { _inline: true }
+ - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types
- schema:
- getTurnServer<: *QJsonObject # It's used as an opaque JSON object
- PublicRoomResponse: { _inline: true }
# - defineFilter>: &Filter # Force folding into a structure
# type: Filter
-# imports: "csapi/definitions/sync_filter.h"
+# imports: '"csapi/definitions/sync_filter.h"'
# - getFilter<: *Filter
- RoomFilter: # A structure inside Filter, same story as with *_filter.yaml
- //: *UseOmittable
@@ -108,10 +114,10 @@ analyzer:
+on:
- /^Notification|Result$/:
type: "std::vector<{{1}}>"
- imports: "events/eventloader.h"
- - /m\.room\.member$/: # Only used in an array (see also above)
+ imports: '"events/eventloader.h"'
+ - /m\.room\.member/: # Only used in an array (see also above)
type: "EventsArray<RoomMemberEvent>"
- imports: "events/roommemberevent.h"
+ imports: '"events/roommemberevent.h"'
- /state_event.yaml$/: StateEvents
- /room_event.yaml$/: RoomEvents
- /event.yaml$/: Events
@@ -129,12 +135,12 @@ analyzer:
#operations:
mustache:
+# delimiter: '%| |%' # or something else instead of '{{ }}'
constants:
# Syntax elements used by GTAD
# _quote: '"' # Common quote for left and right
# _leftQuote: '"'
-# _rightQuote: '"'
-# _joinChar: ',' # The character used by {{_join}} - not working yet
+# _rightQuote: '"_ls'
_comment: '//'
copyrightName: Kitsune Ral
copyrightEmail: <kitsune-ral@users.sf.net>
@@ -184,8 +190,8 @@ mustache:
joinedParamDef: "{{>maybeCrefType}} {{paramName}}{{>cjoin}}"
passPathAndMaybeQuery: >-
- QStringLiteral("{{basePathWithoutHost}}")
- {{#pathParts}} % {{_}}{{/pathParts}}{{#queryParams?}},
+ makePath("{{basePathWithoutHost}}"{{#pathParts}},
+ {{_}}{{/pathParts}}){{#queryParams?}},
queryTo{{camelCaseOperationId}}(
{{#queryParams}}{{paramName}}{{>cjoin}}{{/queryParams}}){{/queryParams?}}
diff --git a/gtad/operation.cpp.mustache b/gtad/operation.cpp.mustache
index 1d0ae476..3d26ec73 100644
--- a/gtad/operation.cpp.mustache
+++ b/gtad/operation.cpp.mustache
@@ -1,12 +1,9 @@
{{!
SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
SPDX-License-Identifier: LGPL-2.1-or-later
-}}
-{{>preamble}}
+}}{{>preamble}}
#include "{{filenameBase}}.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
{{#operations}}{{#operation}}
{{#queryParams?}}
@@ -14,7 +11,7 @@ using namespace Quotient;
auto queryTo{{camelCaseOperationId}}(
{{#queryParams}}{{>joinedParamDef}}{{/queryParams}})
{
- BaseJob::Query _q;{{#queryParams}}
+ QUrlQuery _q;{{#queryParams}}
addParam<{{^required?}}IfNotEmpty{{/required?}}>(_q,
QStringLiteral("{{baseName}}"), {{paramName}});{{/queryParams}}
return _q;
@@ -37,7 +34,7 @@ QUrl {{camelCaseOperationId}}Job::makeRequestUrl(QUrl baseUrl{{#allParams?}},
{ {{#headerParams}}
setRequestHeader("{{baseName}}", {{paramName}}.toLatin1());
{{/headerParams}}{{#inlineBody}}{{^propertyMap}}{{^bodyParams?}}
- setRequestData(Data({{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson?
+ setRequestData(RequestData({{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson?
}}{{^consumesNonJson?}}toJson({{nameCamelCase}}){{/consumesNonJson?}}));
{{/bodyParams?}}{{/propertyMap}}{{/inlineBody
}}{{^consumesNonJson?}}{{#bodyParams?}}
diff --git a/gtad/operation.h.mustache b/gtad/operation.h.mustache
index 135eee55..f91dc66c 100644
--- a/gtad/operation.h.mustache
+++ b/gtad/operation.h.mustache
@@ -1,8 +1,7 @@
{{!
SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
SPDX-License-Identifier: LGPL-2.1-or-later
-}}
-{{>preamble}}
+}}{{>preamble}}
#pragma once
#include "jobs/basejob.h"
diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp
new file mode 100644
index 00000000..a292ed45
--- /dev/null
+++ b/lib/accountregistry.cpp
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "accountregistry.h"
+
+#include "connection.h"
+
+using namespace Quotient;
+
+void AccountRegistry::add(Connection* c)
+{
+ if (m_accounts.contains(c))
+ return;
+ beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size());
+ m_accounts += c;
+ endInsertRows();
+}
+
+void AccountRegistry::drop(Connection* c)
+{
+ beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c));
+ m_accounts.removeOne(c);
+ endRemoveRows();
+ Q_ASSERT(!m_accounts.contains(c));
+}
+
+bool AccountRegistry::isLoggedIn(const QString &userId) const
+{
+ return std::any_of(m_accounts.cbegin(), m_accounts.cend(),
+ [&userId](Connection* a) { return a->userId() == userId; });
+}
+
+bool AccountRegistry::contains(Connection *c) 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()) {
+ return {};
+ }
+
+ auto account = m_accounts[index.row()];
+
+ if (role == ConnectionRole) {
+ return QVariant::fromValue(account);
+ }
+
+ return {};
+}
+
+int AccountRegistry::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+
+ return m_accounts.count();
+}
+
+QHash<int, QByteArray> AccountRegistry::roleNames() const
+{
+ return {{ConnectionRole, "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) {
+ return connection;
+ }
+ }
+ return nullptr;
+}
diff --git a/lib/accountregistry.h b/lib/accountregistry.h
new file mode 100644
index 00000000..5efda459
--- /dev/null
+++ b/lib/accountregistry.h
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtCore/QObject>
+#include <QtCore/QList>
+#include <QtCore/QAbstractListModel>
+
+namespace Quotient {
+class Connection;
+
+class AccountRegistry : public QAbstractListModel {
+ Q_OBJECT
+public:
+ enum EventRoles {
+ ConnectionRole = Qt::UserRole + 1,
+ };
+
+ static AccountRegistry &instance() {
+ static AccountRegistry _instance;
+ return _instance;
+ }
+
+ const QVector<Connection*> accounts() const;
+ void add(Connection* a);
+ void drop(Connection* a);
+ 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;
+
+ [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
+
+private:
+ AccountRegistry();
+
+ QVector<Connection *> m_accounts;
+};
+}
diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h
index 6aee9c57..213dbf19 100644
--- a/lib/application-service/definitions/protocol.h
+++ b/lib/application-service/definitions/protocol.h
@@ -40,7 +40,7 @@ struct ProtocolInstance {
/// provided at the higher level Protocol object.
QString icon;
- /// Preset values for ``fields`` the client may use to search by.
+ /// Preset values for `fields` the client may use to search by.
QJsonObject fields;
/// A unique identifier across all instances.
@@ -81,10 +81,9 @@ struct ThirdPartyProtocol {
/// A content URI representing an icon for the third party protocol.
QString icon;
- /// The type definitions for the fields defined in the ``user_fields`` and
- /// ``location_fields``. Each entry in those arrays MUST have an entry here.
- /// The
- /// ``string`` key for this object is field name itself.
+ /// The type definitions for the fields defined in the `user_fields` and
+ /// `location_fields`. Each entry in those arrays MUST have an entry here.
+ /// The `string` key for this object is field name itself.
///
/// May be an empty object if no fields are defined.
QHash<QString, FieldType> fieldTypes;
diff --git a/lib/avatar.h b/lib/avatar.h
index be125c17..d4634aea 100644
--- a/lib/avatar.h
+++ b/lib/avatar.h
@@ -21,7 +21,7 @@ public:
Avatar& operator=(Avatar&&);
using get_callback_t = std::function<void()>;
- using upload_callback_t = std::function<void(QString)>;
+ using upload_callback_t = std::function<void(QUrl)>;
QImage get(Connection* connection, int dimension,
get_callback_t callback) const;
@@ -42,5 +42,3 @@ private:
std::unique_ptr<Private> d;
};
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
diff --git a/lib/connection.cpp b/lib/connection.cpp
index a384783c..8d1c80f1 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -13,6 +13,10 @@
#include "room.h"
#include "settings.h"
#include "user.h"
+#include "accountregistry.h"
+
+// NB: since Qt 6, moc_connection.cpp needs Room and User fully defined
+#include "moc_connection.cpp"
#include "csapi/account-data.h"
#include "csapi/capabilities.h"
@@ -55,7 +59,7 @@ using namespace Quotient;
// This is very much Qt-specific; STL iterators don't have key() and value()
template <typename HashT, typename Pred>
-HashT erase_if(HashT& hashMap, Pred pred)
+HashT remove_if(HashT& hashMap, Pred pred)
{
HashT removals;
for (auto it = hashMap.begin(); it != hashMap.end();) {
@@ -73,8 +77,6 @@ public:
explicit Private(std::unique_ptr<ConnectionData>&& connection)
: data(move(connection))
{}
- Q_DISABLE_COPY(Private)
- DISABLE_MOVE(Private)
Connection* q = nullptr;
std::unique_ptr<ConnectionData> data;
@@ -255,6 +257,7 @@ Connection::~Connection()
{
qCDebug(MAIN) << "deconstructing connection object for" << userId();
stopSync();
+ AccountRegistry::instance().drop(this);
}
void Connection::resolveServer(const QString& mxid)
@@ -283,7 +286,7 @@ void Connection::resolveServer(const QString& mxid)
if (d->resolverJob->error() == BaseJob::Abandoned)
return;
- if (d->resolverJob->error() != BaseJob::NotFoundError) {
+ if (d->resolverJob->error() != BaseJob::NotFound) {
if (!d->resolverJob->status().good()) {
qCWarning(MAIN)
<< "Fetching .well-known file failed, FAIL_PROMPT";
@@ -311,12 +314,6 @@ void Connection::resolveServer(const QString& mxid)
setHomeserver(maybeBaseUrl);
}
Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver()
- connect(d->loginFlowsJob, &BaseJob::success, this,
- &Connection::resolved);
- connect(d->loginFlowsJob, &BaseJob::failure, this, [this] {
- qCWarning(MAIN) << "Homeserver base URL sanity check failed";
- emit resolveError(tr("The homeserver doesn't seem to be working"));
- });
});
}
@@ -338,7 +335,7 @@ void Connection::loginWithPassword(const QString& userId,
const QString& initialDeviceName,
const QString& deviceId)
{
- d->checkAndConnect(userId, [=] {
+ d->checkAndConnect(userId, [=,this] {
d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId),
password, /*token*/ "", deviceId, initialDeviceName);
}, LoginFlows::Password);
@@ -398,7 +395,7 @@ void Connection::reloadCapabilities()
" disabling version upgrade recommendations to reduce noise";
});
connect(d->capabilitiesJob, &BaseJob::failure, this, [this] {
- if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError)
+ if (d->capabilitiesJob->error() == BaseJob::IncorrectRequest)
qCDebug(MAIN) << "Server doesn't support /capabilities;"
" version upgrade recommendations won't be issued";
});
@@ -440,6 +437,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);
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
@@ -474,10 +472,11 @@ void Connection::Private::checkAndConnect(const QString& userId,
connectFn();
else
emit q->loginError(
+ tr("Unsupported login flow"),
tr("The homeserver at %1 does not support"
" the login flow '%2'")
- .arg(data->baseUrl().toDisplayString()),
- flow->type);
+ .arg(data->baseUrl().toDisplayString(),
+ flow->type));
});
else
connectSingleShot(q, &Connection::homeserverChanged, q, connectFn);
@@ -637,7 +636,7 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList,
}
qWarning(MAIN) << "Room" << roomData.roomId
<< "has just been forgotten but /sync returned it in"
- << toCString(roomData.joinState)
+ << terse << roomData.joinState
<< "state - suspiciously fast turnaround";
}
if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) {
@@ -660,21 +659,21 @@ 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();
DirectChatsMap remoteRemovals =
- erase_if(directChats, [&usersToDCs, this](auto it) {
+ remove_if(directChats, [&usersToDCs, this](auto it) {
return !(
usersToDCs.contains(it.key()->id(), it.value())
|| dcLocalAdditions.contains(it.key(), it.value()));
});
- erase_if(directChatUsers, [&remoteRemovals](auto it) {
+ remove_if(directChatUsers, [&remoteRemovals](auto it) {
return remoteRemovals.contains(it.value(), it.key());
});
// Remove from dcLocalRemovals what the server already has.
- erase_if(dcLocalRemovals, [&remoteRemovals](auto it) {
+ remove_if(dcLocalRemovals, [&remoteRemovals](auto it) {
return remoteRemovals.contains(it.key(), it.value());
});
if (MAIN().isDebugEnabled())
@@ -702,7 +701,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents)
<< "Couldn't get a user object for" << it.key();
}
// Remove from dcLocalAdditions what the server already has.
- erase_if(dcLocalAdditions, [&remoteAdditions](auto it) {
+ remove_if(dcLocalAdditions, [&remoteAdditions](auto it) {
return remoteAdditions.contains(it.key(), it.value());
});
if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty())
@@ -761,7 +760,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents)
<< ee.senderId();
// encryptionManager->updateDeviceKeys();
- visit(*sessionDecryptMessage(ee),
+ switchOnType(*sessionDecryptMessage(ee),
[this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) {
if (auto* detectedRoom = q->room(roomKeyEvent.roomId()))
detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey);
@@ -792,22 +791,17 @@ void Connection::stopSync()
QString Connection::nextBatchToken() const { return d->data->lastEvent(); }
-PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event)
-{
- return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
-}
-
JoinRoomJob* Connection::joinRoom(const QString& roomAlias,
const QStringList& serverNames)
{
- auto job = callApi<JoinRoomJob>(roomAlias, serverNames);
- // Upon completion, ensure a room object in Join state is created
- // (or it might already be there due to a sync completing earlier).
- // finished() is used here instead of success() to overtake clients
- // that may add their own slots to finished().
+ auto* const job = callApi<JoinRoomJob>(roomAlias, serverNames);
+ // Upon completion, ensure a room object is created in case it hasn't come
+ // with a sync yet. If the room object is not there, provideRoom() will
+ // create it in Join state. finished() is used here instead of success()
+ // to overtake clients that may add their own slots to finished().
connect(job, &BaseJob::finished, this, [this, job] {
if (job->status().good())
- provideRoom(job->roomId(), JoinState::Join);
+ provideRoom(job->roomId());
});
return job;
}
@@ -840,6 +834,15 @@ inline auto splitMediaId(const QString& mediaId)
return idParts;
}
+QUrl Connection::makeMediaUrl(QUrl mxcUrl) const
+{
+ Q_ASSERT(mxcUrl.scheme() == "mxc");
+ QUrlQuery q(mxcUrl.query());
+ q.addQueryItem(QStringLiteral("user_id"), userId());
+ mxcUrl.setQuery(q);
+ return mxcUrl;
+}
+
MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId,
QSize requestedSize,
RunningPolicy policy)
@@ -1050,7 +1053,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
connect(leaveJob, &BaseJob::result, this,
[this, leaveJob, forgetJob, room] {
if (leaveJob->error() == BaseJob::Success
- || leaveJob->error() == BaseJob::NotFoundError) {
+ || leaveJob->error() == BaseJob::NotFound) {
run(forgetJob);
// If the matching /sync response hasn't arrived yet,
// mark the room for explicit deletion
@@ -1069,7 +1072,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] {
// Leave room in case of success, or room not known by server
if (forgetJob->error() == BaseJob::Success
- || forgetJob->error() == BaseJob::NotFoundError)
+ || forgetJob->error() == BaseJob::NotFound)
d->removeRoom(id); // Delete the room from roomMap
else
qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": "
@@ -1236,20 +1239,6 @@ int Connection::millisToReconnect() const
return d->syncJob ? d->syncJob->millisToRetry() : 0;
}
-QHash<QPair<QString, bool>, Room*> Connection::roomMap() const
-{
- // Copy-on-write-and-remove-elements is faster than copying elements one by
- // one.
- QHash<QPair<QString, bool>, Room*> roomMap = d->roomMap;
- for (auto it = roomMap.begin(); it != roomMap.end();) {
- if (it.value()->joinState() == JoinState::Leave)
- it = roomMap.erase(it);
- else
- ++it;
- }
- return roomMap;
-}
-
QVector<Room*> Connection::allRooms() const
{
QVector<Room*> result;
@@ -1352,8 +1341,8 @@ void Connection::Private::removeRoom(const QString& roomId)
{
for (auto f : { false, true })
if (auto r = roomMap.take({ roomId, f })) {
- qCDebug(MAIN) << "Room" << r->objectName() << "in state"
- << toCString(r->joinState()) << "will be deleted";
+ qCDebug(MAIN) << "Room" << r->objectName() << "in state" << terse
+ << r->joinState() << "will be deleted";
emit r->beforeDestruction(r);
r->deleteLater();
}
@@ -1385,7 +1374,7 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user)
removals.insert(user, roomId);
d->dcLocalRemovals.insert(user, roomId);
} else {
- removals = erase_if(d->directChats,
+ removals = remove_if(d->directChats,
[&roomId](auto it) { return it.value() == roomId; });
d->directChatUsers.remove(roomId);
d->dcLocalRemovals += removals;
@@ -1524,8 +1513,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
{
@@ -1722,7 +1711,7 @@ void Connection::getTurnServers()
{
auto job = callApi<GetTurnServerJob>();
connect(job, &GetTurnServerJob::success, this,
- [=] { emit turnServersChanged(job->data()); });
+ [this,job] { emit turnServersChanged(job->data()); });
}
const QString Connection::SupportedRoomVersion::StableTag =
diff --git a/lib/connection.h b/lib/connection.h
index 0d22d01f..0713af16 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -6,7 +6,6 @@
#pragma once
#include "ssosession.h"
-#include "joinstate.h"
#include "qt_connection_util.h"
#include "quotient_common.h"
@@ -82,11 +81,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
@@ -95,9 +92,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
@@ -125,6 +122,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 =
@@ -139,15 +137,6 @@ public:
explicit Connection(const QUrl& server, QObject* parent = nullptr);
~Connection() override;
- /// Get all Invited and Joined rooms
- /*!
- * \return a hashmap from a composite key - room name and whether
- * it's an Invite rather than Join - to room pointers
- * \sa allRooms, rooms, roomsWithTag
- */
- [[deprecated("Use allRooms(), roomsWithTag() or rooms(joinStates) instead")]]
- QHash<QPair<QString, bool>, Room*> roomMap() const;
-
/// Get all rooms known within this Connection
/*!
* This includes Invite, Join and Leave rooms, in no particular order.
@@ -478,21 +467,34 @@ 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>);
}
public Q_SLOTS:
- /** Set the homeserver base URL */
+ /// \brief Set the homeserver base URL and retrieve its login flows
+ ///
+ /// \sa LoginFlowsJob, loginFlows, loginFlowsChanged, homeserverChanged
void setHomeserver(const QUrl& baseUrl);
- /** Determine and set the homeserver from MXID */
+ /// \brief Determine and set the homeserver from MXID
+ ///
+ /// This attempts to resolve the homeserver by requesting
+ /// .well-known/matrix/client record from the server taken from the MXID
+ /// serverpart. If there is no record found, the serverpart itself is
+ /// attempted as the homeserver base URL; if the record is there but
+ /// is malformed (e.g., the homeserver base URL cannot be found in it)
+ /// resolveError() is emitted and further processing stops. Otherwise,
+ /// setHomeserver is called, preparing the Connection object for the login
+ /// attempt.
+ /// \param mxid user Matrix ID, such as @someone:example.org
+ /// \sa setHomeserver, homeserverChanged, loginFlowsChanged, resolveError
void resolveServer(const QString& mxid);
/** \brief Log in using a username and password pair
@@ -525,30 +527,12 @@ public Q_SLOTS:
*/
void assumeIdentity(const QString& mxId, const QString& accessToken,
const QString& deviceId);
- /*! \deprecated Use loginWithPassword instead */
- void connectToServer(const QString& userId, const QString& password,
- const QString& initialDeviceName,
- const QString& deviceId = {})
- {
- loginWithPassword(userId, password, initialDeviceName, deviceId);
- }
- /*! \deprecated
- * Use assumeIdentity() if you have an access token or
- * loginWithToken() if you have a login token.
- */
- void connectWithToken(const QString& userId, const QString& accessToken,
- const QString& deviceId)
- {
- assumeIdentity(userId, accessToken, deviceId);
- }
/// Explicitly request capabilities from the server
void reloadCapabilities();
/// Find out if capabilites are still loading from the server
bool loadingCapabilities() const;
- /** @deprecated Use stopSync() instead */
- void disconnectFromServer() { stopSync(); }
void logout();
void sync(int timeout = -1);
@@ -557,6 +541,8 @@ public Q_SLOTS:
void stopSync();
QString nextBatchToken() const;
+ Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const;
+
virtual MediaThumbnailJob*
getThumbnail(const QString& mediaId, QSize requestedSize,
RunningPolicy policy = BackgroundRequest);
@@ -663,24 +649,12 @@ public Q_SLOTS:
/** \deprecated Do not use this directly, use Room::leaveRoom() instead */
virtual LeaveRoomJob* leaveRoom(Room* room);
- // Old API that will be abolished any time soon. DO NOT USE.
-
- /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead
- */
- virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event);
-
Q_SIGNALS:
- /**
- * @deprecated
- * This was a signal resulting from a successful resolveServer().
- * Since Connection now provides setHomeserver(), the HS URL
- * may change even without resolveServer() invocation. Use
- * loginFLowsChanged() instead of resolved(). You can also use
- * loginWith*() and assumeIdentity() without the HS URL set in
- * advance (i.e. without calling resolveServer), as they trigger
- * server name resolution from MXID if the server URL is not valid.
- */
- void resolved();
+ /// \brief Initial server resolution has failed
+ ///
+ /// This signal is emitted when resolveServer() did not manage to resolve
+ /// the homeserver using its .well-known/client record or otherwise.
+ /// \sa resolveServer
void resolveError(QString error);
void homeserverChanged(QUrl baseUrl);
@@ -688,7 +662,6 @@ Q_SIGNALS:
void capabilitiesLoaded();
void connected();
- void reconnected(); //< \deprecated Use connected() instead
void loggedOut();
/** Login data or state have changed
*
diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp
index e54d909b..87ad4577 100644
--- a/lib/connectiondata.cpp
+++ b/lib/connectiondata.cpp
@@ -118,18 +118,6 @@ void ConnectionData::setToken(QByteArray token)
d->accessToken = std::move(token);
}
-void ConnectionData::setHost(QString host)
-{
- d->baseUrl.setHost(host);
- qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl;
-}
-
-void ConnectionData::setPort(int port)
-{
- d->baseUrl.setPort(port);
- qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl;
-}
-
const QString& ConnectionData::deviceId() const { return d->deviceId; }
const QString& ConnectionData::userId() const { return d->userId; }
diff --git a/lib/connectiondata.h b/lib/connectiondata.h
index 7dd96f26..e16a2dac 100644
--- a/lib/connectiondata.h
+++ b/lib/connectiondata.h
@@ -31,10 +31,6 @@ public:
void setBaseUrl(QUrl baseUrl);
void setToken(QByteArray accessToken);
- [[deprecated("Use setBaseUrl() instead")]]
- void setHost(QString host);
- [[deprecated("Use setBaseUrl() instead")]]
- void setPort(int port);
void setDeviceId(const QString& deviceId);
void setUserId(const QString& userId);
void setNeedsToken(const QString& requestName);
diff --git a/lib/converters.cpp b/lib/converters.cpp
index 4338e8ed..444ca4f6 100644
--- a/lib/converters.cpp
+++ b/lib/converters.cpp
@@ -5,24 +5,23 @@
#include <QtCore/QVariant>
-using namespace Quotient;
-
-QJsonValue JsonConverter<QVariant>::dump(const QVariant& v)
+QJsonValue Quotient::JsonConverter<QVariant>::dump(const QVariant& v)
{
return QJsonValue::fromVariant(v);
}
-QVariant JsonConverter<QVariant>::load(const QJsonValue& jv)
+QVariant Quotient::JsonConverter<QVariant>::load(const QJsonValue& jv)
{
return jv.toVariant();
}
-QJsonObject JsonConverter<QVariantHash>::dump(const QVariantHash& vh)
+QJsonObject Quotient::toJson(const QVariantHash& vh)
{
return QJsonObject::fromVariantHash(vh);
}
-QVariantHash JsonConverter<QVariantHash>::load(const QJsonValue& jv)
+template<>
+QVariantHash Quotient::fromJson(const QJsonValue& jv)
{
return jv.toObject().toVariantHash();
}
diff --git a/lib/converters.h b/lib/converters.h
index e07b6ee4..9c3d5749 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -24,6 +24,21 @@ struct JsonObjectConverter {
static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); }
};
+//! \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
+//! 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
+//! QJsonObject/QJsonValue) and most simple cases such as primitive types or
+//! QString this class is not needed.
+//!
+//! Do NOT call the functions of this class directly unless you know what you're
+//! doing; and do not try to specialise basic things unless you're really sure
+//! 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)
@@ -42,13 +57,17 @@ struct JsonConverter {
static T load(const QJsonDocument& jd) { return doLoad(jd.object()); }
};
-template <typename T>
+template <typename T,
+ typename = std::enable_if_t<!std::is_constructible_v<QJsonValue, T>>>
inline auto toJson(const T& pod)
+// -> can return anything from which QJsonValue or, in some cases, QJsonDocument
+// is constructible
{
return JsonConverter<T>::dump(pod);
}
inline auto toJson(const QJsonObject& jo) { return jo; }
+inline auto toJson(const QJsonValue& jv) { return jv; }
template <typename T>
inline void fillJson(QJsonObject& json, const T& data)
@@ -62,6 +81,9 @@ inline T fromJson(const QJsonValue& jv)
return JsonConverter<T>::load(jv);
}
+template<>
+inline QJsonValue fromJson(const QJsonValue& jv) { return jv; }
+
template <typename T>
inline T fromJson(const QJsonDocument& jd)
{
@@ -95,80 +117,68 @@ inline void fillFromJson(const QJsonValue& jv, T& pod)
// JsonConverter<> specialisations
-template <typename T>
-struct TrivialJsonDumper {
- // Works for: QJsonValue (and all things it can consume),
- // QJsonObject, QJsonArray
- static auto dump(const T& val) { return val; }
-};
+template<>
+inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); }
template <>
-struct JsonConverter<bool> : public TrivialJsonDumper<bool> {
- static auto load(const QJsonValue& jv) { return jv.toBool(); }
-};
+inline int fromJson(const QJsonValue& jv) { return jv.toInt(); }
template <>
-struct JsonConverter<int> : public TrivialJsonDumper<int> {
- static auto load(const QJsonValue& jv) { return jv.toInt(); }
-};
+inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); }
template <>
-struct JsonConverter<double> : public TrivialJsonDumper<double> {
- static auto load(const QJsonValue& jv) { return jv.toDouble(); }
-};
+inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); }
template <>
-struct JsonConverter<float> : public TrivialJsonDumper<float> {
- static auto load(const QJsonValue& jv) { return float(jv.toDouble()); }
-};
+inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); }
template <>
-struct JsonConverter<qint64> : public TrivialJsonDumper<qint64> {
- static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); }
-};
+inline QString fromJson(const QJsonValue& jv) { return jv.toString(); }
template <>
-struct JsonConverter<QString> : public TrivialJsonDumper<QString> {
- static auto load(const QJsonValue& jv) { return jv.toString(); }
-};
+inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); }
template <>
-struct JsonConverter<QDateTime> {
- static auto dump(const QDateTime& val) = delete; // not provided yet
- static auto load(const QJsonValue& jv)
- {
- return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
- }
-};
+inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); }
+inline QJsonValue toJson(const QDateTime& val)
+{
+ return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue();
+}
template <>
-struct JsonConverter<QDate> {
- static auto dump(const QDate& val) = delete; // not provided yet
- static auto load(const QJsonValue& jv)
- {
- return fromJson<QDateTime>(jv).date();
- }
-};
+inline QDateTime fromJson(const QJsonValue& jv)
+{
+ return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
+}
+inline QJsonValue toJson(const QDate& val) {
+ return toJson(
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ QDateTime(val)
+#else
+ val.startOfDay()
+#endif
+ );
+}
template <>
-struct JsonConverter<QUrl> : JsonConverter<QString> {
- static auto dump(const QUrl& url) // Override on top of that for QString
- {
- return JsonConverter<QString>::dump(url.toString(QUrl::FullyEncoded));
- }
-};
+inline QDate fromJson(const QJsonValue& jv)
+{
+ return fromJson<QDateTime>(jv).date();
+}
-template <>
-struct JsonConverter<QJsonArray> : public TrivialJsonDumper<QJsonArray> {
- static auto load(const QJsonValue& jv) { return jv.toArray(); }
-};
+// Insulate QVariant and QUrl conversions into JsonConverter so that they don't
+// interfere with toJson(const QJsonValue&) over QString, since both types are
+// constructible from QString (even if QUrl requires explicit construction).
template <>
-struct JsonConverter<QByteArray> {
- static QString dump(const QByteArray& ba) { return ba.constData(); }
+struct JsonConverter<QUrl> {
static auto load(const QJsonValue& jv)
{
- return fromJson<QString>(jv).toLatin1();
+ return QUrl(jv.toString());
+ }
+ static auto dump(const QUrl& url)
+ {
+ return url.toString(QUrl::FullyEncoded);
}
};
@@ -194,15 +204,11 @@ struct JsonConverter<Omittable<T>> {
template <typename VectorT, typename T = typename VectorT::value_type>
struct JsonArrayConverter {
- static void dumpTo(QJsonArray& ar, const VectorT& vals)
- {
- for (const auto& v : vals)
- ar.push_back(toJson(v));
- }
static auto dump(const VectorT& vals)
{
QJsonArray ja;
- dumpTo(ja, vals);
+ for (const auto& v : vals)
+ ja.push_back(toJson(v));
return ja;
}
static auto load(const QJsonArray& ja)
@@ -221,14 +227,16 @@ template <typename T>
struct JsonConverter<std::vector<T>>
: public JsonArrayConverter<std::vector<T>> {};
+#if QT_VERSION_MAJOR < 6 // QVector is an alias of QList in Qt6 but not in Qt 5
template <typename T>
struct JsonConverter<QVector<T>> : public JsonArrayConverter<QVector<T>> {};
+#endif
template <typename T>
struct JsonConverter<QList<T>> : public JsonArrayConverter<QList<T>> {};
template <>
-struct JsonConverter<QStringList> : public JsonConverter<QList<QString>> {
+struct JsonConverter<QStringList> : public JsonArrayConverter<QStringList> {
static auto dump(const QStringList& sl)
{
return QJsonArray::fromStringList(sl);
@@ -240,14 +248,13 @@ struct JsonObjectConverter<QSet<QString>> {
static void dumpTo(QJsonObject& json, const QSet<QString>& s)
{
for (const auto& e : s)
- json.insert(toJson(e), QJsonObject {});
+ json.insert(e, QJsonObject {});
}
- static auto fillFrom(const QJsonObject& json, QSet<QString>& s)
+ static void fillFrom(const QJsonObject& json, QSet<QString>& s)
{
s.reserve(s.size() + json.size());
for (auto it = json.begin(); it != json.end(); ++it)
s.insert(it.key());
- return s;
}
};
@@ -260,7 +267,7 @@ struct HashMapFromJson {
}
static void fillFrom(const QJsonObject& jo, HashMapT& h)
{
- h.reserve(jo.size());
+ h.reserve(h.size() + jo.size());
for (auto it = jo.begin(); it != jo.end(); ++it)
h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value());
}
@@ -274,11 +281,9 @@ template <typename T>
struct JsonObjectConverter<QHash<QString, T>>
: public HashMapFromJson<QHash<QString, T>> {};
+QJsonObject toJson(const QVariantHash& vh);
template <>
-struct JsonConverter<QVariantHash> {
- static QJsonObject dump(const QVariantHash& vh);
- static QVariantHash load(const QJsonValue& jv);
-};
+QVariantHash fromJson(const QJsonValue& jv);
// Conditional insertion into a QJsonObject
@@ -302,16 +307,15 @@ namespace _impl {
q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false"));
}
- inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals)
+ inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v)
{
- for (const auto& v : vals)
- q.addQueryItem(k, v);
+ q.addQueryItem(k, v.toEncoded());
}
- inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals)
+ inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals)
{
- for (auto it = vals.begin(); it != vals.end(); ++it)
- q.addQueryItem(it.key(), it.value().toString());
+ for (const auto& v : vals)
+ q.addQueryItem(k, v);
}
// This one is for types that don't have isEmpty() and for all types
diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp
index 6a40e908..09fc8d40 100644
--- a/lib/csapi/account-data.cpp
+++ b/lib/csapi/account-data.cpp
@@ -4,31 +4,29 @@
#include "account-data.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type,
const QJsonObject& content)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/account_data/" % type)
+ makePath("/_matrix/client/r0", "/user/", userId, "/account_data/",
+ type))
{
- setRequestData(Data(toJson(content)));
+ setRequestData(RequestData(toJson(content)));
}
QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& type)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/user/"
- % userId % "/account_data/" % type);
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/account_data/", type));
}
GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/account_data/" % type)
+ makePath("/_matrix/client/r0", "/user/", userId, "/account_data/",
+ type))
{}
SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId,
@@ -36,10 +34,10 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId,
const QString& type,
const QJsonObject& content)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/account_data/" % type)
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/account_data/", type))
{
- setRequestData(Data(toJson(content)));
+ setRequestData(RequestData(toJson(content)));
}
QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl,
@@ -48,15 +46,15 @@ QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl,
const QString& type)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/user/" % userId % "/rooms/" % roomId
- % "/account_data/" % type);
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/rooms/", roomId,
+ "/account_data/", type));
}
GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId,
const QString& roomId,
const QString& type)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataPerRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/account_data/" % type)
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/account_data/", type))
{}
diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h
index 9a31596f..0c760e80 100644
--- a/lib/csapi/account-data.h
+++ b/lib/csapi/account-data.h
@@ -12,7 +12,7 @@ namespace Quotient {
*
* Set some account_data for the client. This config is only visible to the user
* that set the account_data. The config will be synced to clients in the
- * top-level ``account_data``.
+ * top-level `account_data`.
*/
class SetAccountDataJob : public BaseJob {
public:
@@ -65,7 +65,7 @@ public:
*
* Set some account_data for the client on a given room. This config is only
* visible to the user that set the account_data. The config will be synced to
- * clients in the per-room ``account_data``.
+ * clients in the per-room `account_data`.
*/
class SetAccountDataPerRoomJob : public BaseJob {
public:
diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp
index 9619c441..81dd0624 100644
--- a/lib/csapi/admin.cpp
+++ b/lib/csapi/admin.cpp
@@ -4,18 +4,16 @@
#include "admin.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/admin/whois/" % userId);
+ makePath("/_matrix/client/r0",
+ "/admin/whois/", userId));
}
GetWhoIsJob::GetWhoIsJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"),
- QStringLiteral("/_matrix/client/r0") % "/admin/whois/" % userId)
+ makePath("/_matrix/client/r0", "/admin/whois/", userId))
{}
diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp
index fa4f475a..589c9fc1 100644
--- a/lib/csapi/administrative_contact.cpp
+++ b/lib/csapi/administrative_contact.cpp
@@ -4,25 +4,22 @@
#include "administrative_contact.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/account/3pid");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/account/3pid"));
}
GetAccount3PIDsJob::GetAccount3PIDsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid")
+ makePath("/_matrix/client/r0", "/account/3pid"))
{}
Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds)
: BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid")
+ makePath("/_matrix/client/r0", "/account/3pid"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds);
@@ -32,7 +29,7 @@ Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds)
Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/add")
+ makePath("/_matrix/client/r0", "/account/3pid/add"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
@@ -44,7 +41,7 @@ Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid,
Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer,
const QString& idAccessToken, const QString& sid)
: BaseJob(HttpVerb::Post, QStringLiteral("Bind3PIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/bind")
+ makePath("/_matrix/client/r0", "/account/3pid/bind"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("client_secret"), clientSecret);
@@ -58,7 +55,7 @@ Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium,
const QString& address,
const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/delete")
+ makePath("/_matrix/client/r0", "/account/3pid/delete"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer);
@@ -72,7 +69,7 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium,
const QString& address,
const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/unbind")
+ makePath("/_matrix/client/r0", "/account/3pid/unbind"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer);
@@ -85,19 +82,19 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium,
RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/3pid/email/requestToken",
+ makePath("/_matrix/client/r0",
+ "/account/3pid/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/3pid/msisdn/requestToken",
+ makePath("/_matrix/client/r0",
+ "/account/3pid/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h
index 1966d533..e436971d 100644
--- a/lib/csapi/administrative_contact.h
+++ b/lib/csapi/administrative_contact.h
@@ -93,14 +93,14 @@ struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> {
*
* Adds contact information to the user's account.
*
- * This endpoint is deprecated in favour of the more specific ``/3pid/add``
- * and ``/3pid/bind`` endpoints.
+ * This endpoint is deprecated in favour of the more specific `/3pid/add`
+ * and `/3pid/bind` endpoints.
*
- * .. Note::
- * Previously this endpoint supported a ``bind`` parameter. This parameter
- * has been removed, making this endpoint behave as though it was ``false``.
- * This results in this endpoint being an equivalent to ``/3pid/bind`` rather
- * than dual-purpose.
+ * **Note:**
+ * Previously this endpoint supported a `bind` parameter. This parameter
+ * has been removed, making this endpoint behave as though it was `false`.
+ * This results in this endpoint being an equivalent to `/3pid/bind` rather
+ * than dual-purpose.
*/
class Post3PIDsJob : public BaseJob {
public:
@@ -144,7 +144,8 @@ struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> {
/*! \brief Adds contact information to the user's account.
*
- * This API endpoint uses the `User-Interactive Authentication API`_.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
*
* Adds contact information to the user's account. Homeservers should use 3PIDs
* added through this endpoint for password resets instead of relying on the
@@ -206,7 +207,7 @@ public:
* Removes a third party identifier from the user's account. This might not
* cause an unbind of the identifier from the identity server.
*
- * Unlike other endpoints, this endpoint does not take an ``id_access_token``
+ * Unlike other endpoints, this endpoint does not take an `id_access_token`
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
@@ -222,9 +223,9 @@ public:
*
* \param idServer
* The identity server to unbind from. If not provided, the homeserver
- * MUST use the ``id_server`` the identifier was added through. If the
- * homeserver does not know the original ``id_server``, it MUST return
- * a ``id_server_unbind_result`` of ``no-support``.
+ * MUST use the `id_server` the identifier was added through. If the
+ * homeserver does not know the original `id_server`, it MUST return
+ * a `id_server_unbind_result` of `no-support`.
*/
explicit Delete3pidFromAccountJob(const QString& medium,
const QString& address,
@@ -233,8 +234,8 @@ public:
// Result properties
/// An indicator as to whether or not the homeserver was able to unbind
- /// the 3PID from the identity server. ``success`` indicates that the
- /// indentity server has unbound the identifier whereas ``no-support``
+ /// the 3PID from the identity server. `success` indicates that the
+ /// indentity server has unbound the identifier whereas `no-support`
/// indicates that the identity server refuses to support the request
/// or the homeserver was not able to determine an identity server to
/// unbind from.
@@ -249,7 +250,7 @@ public:
* Removes a user's third party identifier from the provided identity server
* without removing it from the homeserver.
*
- * Unlike other endpoints, this endpoint does not take an ``id_access_token``
+ * Unlike other endpoints, this endpoint does not take an `id_access_token`
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
@@ -265,9 +266,9 @@ public:
*
* \param idServer
* The identity server to unbind from. If not provided, the homeserver
- * MUST use the ``id_server`` the identifier was added through. If the
- * homeserver does not know the original ``id_server``, it MUST return
- * a ``id_server_unbind_result`` of ``no-support``.
+ * MUST use the `id_server` the identifier was added through. If the
+ * homeserver does not know the original `id_server`, it MUST return
+ * a `id_server_unbind_result` of `no-support`.
*/
explicit Unbind3pidFromAccountJob(const QString& medium,
const QString& address,
@@ -276,8 +277,8 @@ public:
// Result properties
/// An indicator as to whether or not the identity server was able to unbind
- /// the 3PID. ``success`` indicates that the identity server has unbound the
- /// identifier whereas ``no-support`` indicates that the identity server
+ /// the 3PID. `success` indicates that the identity server has unbound the
+ /// identifier whereas `no-support` indicates that the identity server
/// refuses to support the request or the homeserver was not able to
/// determine an identity server to unbind from.
QString idServerUnbindResult() const
@@ -293,7 +294,9 @@ public:
* already associated with an account on this homeserver. This API should
* be used to request validation tokens when adding an email address to an
* account. This API's parameters and response are identical to that of
- * the |/register/email/requestToken|_ endpoint. The homeserver should validate
+ * the
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * endpoint. The homeserver should validate
* the email itself, either by sending a validation email itself or by using
* a service it has control over.
*/
@@ -307,9 +310,11 @@ public:
* already associated with an account on this homeserver. This API should
* be used to request validation tokens when adding an email address to an
* account. This API's parameters and response are identical to that of
- * the |/register/email/requestToken|_ endpoint. The homeserver should
- * validate the email itself, either by sending a validation email itself or
- * by using a service it has control over.
+ * the
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * endpoint. The homeserver should validate
+ * the email itself, either by sending a validation email itself or by
+ * using a service it has control over.
*/
explicit RequestTokenTo3PIDEmailJob(const EmailValidationData& body);
@@ -331,7 +336,9 @@ public:
* already associated with an account on this homeserver. This API should
* be used to request validation tokens when adding a phone number to an
* account. This API's parameters and response are identical to that of
- * the |/register/msisdn/requestToken|_ endpoint. The homeserver should validate
+ * the
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * endpoint. The homeserver should validate
* the phone number itself, either by sending a validation message itself or by
* using a service it has control over.
*/
@@ -345,9 +352,11 @@ public:
* already associated with an account on this homeserver. This API should
* be used to request validation tokens when adding a phone number to an
* account. This API's parameters and response are identical to that of
- * the |/register/msisdn/requestToken|_ endpoint. The homeserver should
- * validate the phone number itself, either by sending a validation message
- * itself or by using a service it has control over.
+ * the
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * endpoint. The homeserver should validate
+ * the phone number itself, either by sending a validation message itself
+ * or by using a service it has control over.
*/
explicit RequestTokenTo3PIDMSISDNJob(const MsisdnValidationData& body);
diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp
index e8ec55bf..40d784c6 100644
--- a/lib/csapi/appservice_room_directory.cpp
+++ b/lib/csapi/appservice_room_directory.cpp
@@ -4,16 +4,14 @@
#include "appservice_room_directory.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
-UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob(
+UpdateAppserviceRoomDirectoryVisibilityJob::UpdateAppserviceRoomDirectoryVisibilityJob(
const QString& networkId, const QString& roomId, const QString& visibility)
: BaseJob(HttpVerb::Put,
- QStringLiteral("UpdateAppserviceRoomDirectoryVsibilityJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/list/appservice/" % networkId % "/" % roomId)
+ QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"),
+ makePath("/_matrix/client/r0", "/directory/list/appservice/",
+ networkId, "/", roomId))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("visibility"), visibility);
diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h
index 3fa02a07..56a69592 100644
--- a/lib/csapi/appservice_room_directory.h
+++ b/lib/csapi/appservice_room_directory.h
@@ -17,11 +17,11 @@ namespace Quotient {
* This API is similar to the room directory visibility API used by clients
* to update the homeserver's more general room directory.
*
- * This API requires the use of an application service access token
- * (``as_token``) instead of a typical client's access_token. This API cannot be
- * invoked by users who are not identified as application services.
+ * This API requires the use of an application service access token (`as_token`)
+ * instead of a typical client's access_token. This API cannot be invoked by
+ * users who are not identified as application services.
*/
-class UpdateAppserviceRoomDirectoryVsibilityJob : public BaseJob {
+class UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob {
public:
/*! \brief Updates a room's visibility in the application service's room
* directory.
@@ -38,9 +38,9 @@ public:
* Whether the room should be visible (public) in the directory
* or not (private).
*/
- explicit UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId,
- const QString& roomId,
- const QString& visibility);
+ explicit UpdateAppserviceRoomDirectoryVisibilityJob(
+ const QString& networkId, const QString& roomId,
+ const QString& visibility);
};
} // namespace Quotient
diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp
index 8a8ec664..472128bb 100644
--- a/lib/csapi/banning.cpp
+++ b/lib/csapi/banning.cpp
@@ -4,14 +4,12 @@
#include "banning.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
BanJob::BanJob(const QString& roomId, const QString& userId,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("BanJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/ban")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/ban"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("user_id"), userId);
@@ -19,12 +17,13 @@ BanJob::BanJob(const QString& roomId, const QString& userId,
setRequestData(std::move(_data));
}
-UnbanJob::UnbanJob(const QString& roomId, const QString& userId)
+UnbanJob::UnbanJob(const QString& roomId, const QString& userId,
+ const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/unban")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/unban"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("user_id"), userId);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
}
diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h
index 37ae91ee..7a9697d3 100644
--- a/lib/csapi/banning.h
+++ b/lib/csapi/banning.h
@@ -30,7 +30,8 @@ public:
*
* \param reason
* The reason the user has been banned. This will be supplied as the
- * ``reason`` on the target's updated `m.room.member`_ event.
+ * `reason` on the target's updated
+ * [`m.room.member`](/client-server-api/#mroommember) event.
*/
explicit BanJob(const QString& roomId, const QString& userId,
const QString& reason = {});
@@ -54,8 +55,13 @@ public:
*
* \param userId
* The fully qualified user ID of the user being unbanned.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
- explicit UnbanJob(const QString& roomId, const QString& userId);
+ explicit UnbanJob(const QString& roomId, const QString& userId,
+ const QString& reason = {});
};
} // namespace Quotient
diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp
index 33a53cad..bc21e462 100644
--- a/lib/csapi/capabilities.cpp
+++ b/lib/csapi/capabilities.cpp
@@ -4,20 +4,17 @@
#include "capabilities.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/capabilities");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/capabilities"));
}
GetCapabilitiesJob::GetCapabilitiesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"),
- QStringLiteral("/_matrix/client/r0") % "/capabilities")
+ makePath("/_matrix/client/r0", "/capabilities"))
{
addExpectedKey("capabilities");
}
diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp
index 7ae89739..6d1e38b6 100644
--- a/lib/csapi/content-repo.cpp
+++ b/lib/csapi/content-repo.cpp
@@ -4,13 +4,11 @@
#include "content-repo.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToUploadContent(const QString& filename)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("filename"), filename);
return _q;
}
@@ -18,17 +16,17 @@ auto queryToUploadContent(const QString& filename)
UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename,
const QString& contentType)
: BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"),
- QStringLiteral("/_matrix/media/r0") % "/upload",
+ makePath("/_matrix/media/r0", "/upload"),
queryToUploadContent(filename))
{
setRequestHeader("Content-Type", contentType.toLatin1());
- setRequestData(Data(content));
+ setRequestData(RequestData(content));
addExpectedKey("content_uri");
}
auto queryToGetContent(bool allowRemote)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote);
return _q;
}
@@ -37,17 +35,16 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName,
const QString& mediaId, bool allowRemote)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/download/" % serverName % "/"
- % mediaId,
+ makePath("/_matrix/media/r0", "/download/",
+ serverName, "/", mediaId),
queryToGetContent(allowRemote));
}
GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"),
- QStringLiteral("/_matrix/media/r0") % "/download/" % serverName
- % "/" % mediaId,
+ makePath("/_matrix/media/r0", "/download/", serverName, "/",
+ mediaId),
queryToGetContent(allowRemote), {}, false)
{
setExpectedContentTypes({ "*/*" });
@@ -55,7 +52,7 @@ GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId,
auto queryToGetContentOverrideName(bool allowRemote)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote);
return _q;
}
@@ -67,9 +64,9 @@ QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl,
bool allowRemote)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/download/" % serverName % "/"
- % mediaId % "/" % fileName,
+ makePath("/_matrix/media/r0", "/download/",
+ serverName, "/", mediaId, "/",
+ fileName),
queryToGetContentOverrideName(allowRemote));
}
@@ -78,8 +75,8 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName,
const QString& fileName,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"),
- QStringLiteral("/_matrix/media/r0") % "/download/" % serverName
- % "/" % mediaId % "/" % fileName,
+ makePath("/_matrix/media/r0", "/download/", serverName, "/",
+ mediaId, "/", fileName),
queryToGetContentOverrideName(allowRemote), {}, false)
{
setExpectedContentTypes({ "*/*" });
@@ -88,7 +85,7 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName,
auto queryToGetContentThumbnail(int width, int height, const QString& method,
bool allowRemote)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("width"), width);
addParam<>(_q, QStringLiteral("height"), height);
addParam<IfNotEmpty>(_q, QStringLiteral("method"), method);
@@ -104,8 +101,7 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName % "/"
- % mediaId,
+ makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", mediaId),
queryToGetContentThumbnail(width, height, method, allowRemote));
}
@@ -114,45 +110,44 @@ GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName,
int height, const QString& method,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"),
- QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName
- % "/" % mediaId,
+ makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/",
+ mediaId),
queryToGetContentThumbnail(width, height, method, allowRemote),
{}, false)
{
setExpectedContentTypes({ "image/jpeg", "image/png" });
}
-auto queryToGetUrlPreview(const QString& url, Omittable<qint64> ts)
+auto queryToGetUrlPreview(const QUrl& url, Omittable<qint64> ts)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("url"), url);
addParam<IfNotEmpty>(_q, QStringLiteral("ts"), ts);
return _q;
}
-QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url,
+QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url,
Omittable<qint64> ts)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/preview_url",
+ makePath("/_matrix/media/r0",
+ "/preview_url"),
queryToGetUrlPreview(url, ts));
}
-GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, Omittable<qint64> ts)
+GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable<qint64> ts)
: BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"),
- QStringLiteral("/_matrix/media/r0") % "/preview_url",
+ makePath("/_matrix/media/r0", "/preview_url"),
queryToGetUrlPreview(url, ts))
{}
QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/config");
+ makePath("/_matrix/media/r0", "/config"));
}
GetConfigJob::GetConfigJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"),
- QStringLiteral("/_matrix/media/r0") % "/config")
+ makePath("/_matrix/media/r0", "/config"))
{}
diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h
index ed67485c..28409f5c 100644
--- a/lib/csapi/content-repo.h
+++ b/lib/csapi/content-repo.h
@@ -32,11 +32,9 @@ public:
// Result properties
- /// The `MXC URI`_ to the uploaded content.
- QString contentUri() const
- {
- return loadFromJson<QString>("content_uri"_ls);
- }
+ /// The [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the
+ /// uploaded content.
+ QUrl contentUri() const { return loadFromJson<QUrl>("content_uri"_ls); }
};
/*! \brief Download content from the content repository.
@@ -47,10 +45,10 @@ public:
/*! \brief Download content from the content repository.
*
* \param serverName
- * The server name from the ``mxc://`` URI (the authoritory component)
+ * The server name from the `mxc://` URI (the authoritory component)
*
* \param mediaId
- * The media ID from the ``mxc://`` URI (the path component)
+ * The media ID from the `mxc://` URI (the path component)
*
* \param allowRemote
* Indicates to the server that it should not attempt to fetch the media
@@ -95,13 +93,13 @@ public:
* name
*
* \param serverName
- * The server name from the ``mxc://`` URI (the authoritory component)
+ * The server name from the `mxc://` URI (the authoritory component)
*
* \param mediaId
- * The media ID from the ``mxc://`` URI (the path component)
+ * The media ID from the `mxc://` URI (the path component)
*
* \param fileName
- * A filename to give in the ``Content-Disposition`` header.
+ * A filename to give in the `Content-Disposition` header.
*
* \param allowRemote
* Indicates to the server that it should not attempt to fetch the media
@@ -127,7 +125,7 @@ public:
/// The content type of the file that was previously uploaded.
QString contentType() const { return reply()->rawHeader("Content-Type"); }
- /// The ``fileName`` requested or the name of the file that was previously
+ /// The `fileName` requested or the name of the file that was previously
/// uploaded, if set.
QString contentDisposition() const
{
@@ -141,17 +139,18 @@ public:
/*! \brief Download a thumbnail of content from the content repository
*
* Download a thumbnail of content from the content repository.
- * See the `thumbnailing <#thumbnails>`_ section for more information.
+ * See the [Thumbnails](/client-server-api/#thumbnails) section for more
+ * information.
*/
class GetContentThumbnailJob : public BaseJob {
public:
/*! \brief Download a thumbnail of content from the content repository
*
* \param serverName
- * The server name from the ``mxc://`` URI (the authoritory component)
+ * The server name from the `mxc://` URI (the authoritory component)
*
* \param mediaId
- * The media ID from the ``mxc://`` URI (the path component)
+ * The media ID from the `mxc://` URI (the path component)
*
* \param width
* The *desired* width of the thumbnail. The actual thumbnail may be
@@ -162,8 +161,8 @@ public:
* larger than the size specified.
*
* \param method
- * The desired resizing method. See the `thumbnailing <#thumbnails>`_
- * section for more information.
+ * The desired resizing method. See the
+ * [Thumbnails](/client-server-api/#thumbnails) section for more information.
*
* \param allowRemote
* Indicates to the server that it should not attempt to fetch
@@ -199,11 +198,11 @@ public:
* Get information about a URL for the client. Typically this is called when a
* client sees a URL in a message and wants to render a preview for the user.
*
- * .. Note::
- * Clients should consider avoiding this endpoint for URLs posted in encrypted
- * rooms. Encrypted rooms often contain more sensitive information the users
- * 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.
+ * **Note:**
+ * Clients should consider avoiding this endpoint for URLs posted in encrypted
+ * rooms. Encrypted rooms often contain more sensitive information the users
+ * 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 {
public:
@@ -217,14 +216,14 @@ public:
* return a newer version if it does not have the requested version
* available.
*/
- explicit GetUrlPreviewJob(const QString& url, Omittable<qint64> ts = none);
+ explicit GetUrlPreviewJob(const QUrl& url, Omittable<qint64> ts = none);
/*! \brief Construct a URL without creating a full-fledged job object
*
* This function can be used when a URL for GetUrlPreviewJob
* is necessary but the job itself isn't.
*/
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& url,
+ static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& url,
Omittable<qint64> ts = none);
// Result properties
@@ -235,8 +234,9 @@ public:
return loadFromJson<Omittable<qint64>>("matrix:image:size"_ls);
}
- /// An `MXC URI`_ to the image. Omitted if there is no image.
- QString ogImage() const { return loadFromJson<QString>("og:image"_ls); }
+ /// An [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the image.
+ /// Omitted if there is no image.
+ QUrl ogImage() const { return loadFromJson<QUrl>("og:image"_ls); }
};
/*! \brief Get the configuration for the content repository.
diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp
index a94f9951..9aaef87f 100644
--- a/lib/csapi/create_room.cpp
+++ b/lib/csapi/create_room.cpp
@@ -4,8 +4,6 @@
#include "create_room.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
CreateRoomJob::CreateRoomJob(const QString& visibility,
@@ -18,7 +16,7 @@ CreateRoomJob::CreateRoomJob(const QString& visibility,
const QString& preset, Omittable<bool> isDirect,
const QJsonObject& powerLevelContentOverride)
: BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/createRoom")
+ makePath("/_matrix/client/r0", "/createRoom"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility);
diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h
index 6a718ff4..81dfbffc 100644
--- a/lib/csapi/create_room.h
+++ b/lib/csapi/create_room.h
@@ -16,47 +16,42 @@ namespace Quotient {
* the new room, including checking power levels for each event. It MUST
* apply the events implied by the request in the following order:
*
- * 1. The ``m.room.create`` event itself. Must be the first event in the
+ * 1. The `m.room.create` event itself. Must be the first event in the
* room.
*
- * 2. An ``m.room.member`` event for the creator to join the room. This is
+ * 2. An `m.room.member` event for the creator to join the room. This is
* needed so the remaining events can be sent.
*
- * 3. A default ``m.room.power_levels`` event, giving the room creator
+ * 3. A default `m.room.power_levels` event, giving the room creator
* (and not other members) permission to send state events. Overridden
- * by the ``power_level_content_override`` parameter.
+ * by the `power_level_content_override` parameter.
*
- * 4. Events set by the ``preset``. Currently these are the
- * ``m.room.join_rules``,
- * ``m.room.history_visibility``, and ``m.room.guest_access`` state events.
+ * 4. Events set by the `preset`. Currently these are the `m.room.join_rules`,
+ * `m.room.history_visibility`, and `m.room.guest_access` state events.
*
- * 5. Events listed in ``initial_state``, in the order that they are
+ * 5. Events listed in `initial_state`, in the order that they are
* listed.
*
- * 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and
- * ``m.room.topic`` state events).
+ * 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
+ * state events).
*
- * 7. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member``
- * with
- * ``membership: invite`` and ``m.room.third_party_invite``).
+ * 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` with
+ * `membership: invite` and `m.room.third_party_invite`).
*
* The available presets do the following with respect to room state:
*
- * ======================== ============== ======================
- * ================ ========= Preset ``join_rules``
- * ``history_visibility`` ``guest_access`` Other
- * ======================== ============== ======================
- * ================ =========
- * ``private_chat`` ``invite`` ``shared`` ``can_join``
- * ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All
- * invitees are given the same power level as the room creator.
- * ``public_chat`` ``public`` ``shared`` ``forbidden``
- * ======================== ============== ======================
- * ================ =========
+ * | Preset | `join_rules` | `history_visibility` |
+ * `guest_access` | Other |
+ * |------------------------|--------------|----------------------|----------------|-------|
+ * | `private_chat` | `invite` | `shared` | `can_join`
+ * | | | `trusted_private_chat` | `invite` | `shared` |
+ * `can_join` | All invitees are given the same power level as the room
+ * creator. | | `public_chat` | `public` | `shared` |
+ * `forbidden` | |
*
- * The server will create a ``m.room.create`` event in the room with the
+ * The server will create a `m.room.create` event in the room with the
* requesting user as the creator, alongside other keys provided in the
- * ``creation_content``.
+ * `creation_content`.
*/
class CreateRoomJob : public BaseJob {
public:
@@ -68,50 +63,44 @@ public:
/// the new room, including checking power levels for each event. It MUST
/// apply the events implied by the request in the following order:
///
- /// 1. The ``m.room.create`` event itself. Must be the first event in the
+ /// 1. The `m.room.create` event itself. Must be the first event in the
/// room.
///
- /// 2. An ``m.room.member`` event for the creator to join the room. This is
+ /// 2. An `m.room.member` event for the creator to join the room. This is
/// needed so the remaining events can be sent.
///
- /// 3. A default ``m.room.power_levels`` event, giving the room creator
+ /// 3. A default `m.room.power_levels` event, giving the room creator
/// (and not other members) permission to send state events. Overridden
- /// by the ``power_level_content_override`` parameter.
+ /// by the `power_level_content_override` parameter.
///
- /// 4. Events set by the ``preset``. Currently these are the
- /// ``m.room.join_rules``,
- /// ``m.room.history_visibility``, and ``m.room.guest_access`` state
- /// events.
+ /// 4. Events set by the `preset`. Currently these are the
+ /// `m.room.join_rules`,
+ /// `m.room.history_visibility`, and `m.room.guest_access` state events.
///
- /// 5. Events listed in ``initial_state``, in the order that they are
+ /// 5. Events listed in `initial_state`, in the order that they are
/// listed.
///
- /// 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and
- /// ``m.room.topic``
+ /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
/// state events).
///
- /// 7. Invite events implied by ``invite`` and ``invite_3pid``
- /// (``m.room.member`` with
- /// ``membership: invite`` and ``m.room.third_party_invite``).
+ /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member`
+ /// with
+ /// `membership: invite` and `m.room.third_party_invite`).
///
/// The available presets do the following with respect to room state:
///
- /// ======================== ============== ======================
- /// ================ =========
- /// Preset ``join_rules`` ``history_visibility``
- /// ``guest_access`` Other
- /// ======================== ============== ======================
- /// ================ =========
- /// ``private_chat`` ``invite`` ``shared`` ``can_join``
- /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All
- /// invitees are given the same power level as the room creator.
- /// ``public_chat`` ``public`` ``shared`` ``forbidden``
- /// ======================== ============== ======================
- /// ================ =========
+ /// | Preset | `join_rules` | `history_visibility` |
+ /// `guest_access` | Other |
+ /// |------------------------|--------------|----------------------|----------------|-------|
+ /// | `private_chat` | `invite` | `shared` |
+ /// `can_join` | | | `trusted_private_chat` | `invite` |
+ /// `shared` | `can_join` | All invitees are given the same
+ /// power level as the room creator. | | `public_chat` | `public`
+ /// | `shared` | `forbidden` | |
///
- /// The server will create a ``m.room.create`` event in the room with the
+ /// The server will create a `m.room.create` event in the room with the
/// requesting user as the creator, alongside other keys provided in the
- /// ``creation_content``.
+ /// `creation_content`.
struct Invite3pid {
/// The hostname+port of the identity server which should be used for
/// third party identifier lookups.
@@ -121,7 +110,7 @@ public:
/// r0.5-compatible clients and this specification version.
QString idAccessToken;
/// The kind of address being passed in the address field, for example
- /// ``email``.
+ /// `email`.
QString medium;
/// The invitee's third party identifier.
QString address;
@@ -133,50 +122,44 @@ public:
/// the new room, including checking power levels for each event. It MUST
/// apply the events implied by the request in the following order:
///
- /// 1. The ``m.room.create`` event itself. Must be the first event in the
+ /// 1. The `m.room.create` event itself. Must be the first event in the
/// room.
///
- /// 2. An ``m.room.member`` event for the creator to join the room. This is
+ /// 2. An `m.room.member` event for the creator to join the room. This is
/// needed so the remaining events can be sent.
///
- /// 3. A default ``m.room.power_levels`` event, giving the room creator
+ /// 3. A default `m.room.power_levels` event, giving the room creator
/// (and not other members) permission to send state events. Overridden
- /// by the ``power_level_content_override`` parameter.
+ /// by the `power_level_content_override` parameter.
///
- /// 4. Events set by the ``preset``. Currently these are the
- /// ``m.room.join_rules``,
- /// ``m.room.history_visibility``, and ``m.room.guest_access`` state
- /// events.
+ /// 4. Events set by the `preset`. Currently these are the
+ /// `m.room.join_rules`,
+ /// `m.room.history_visibility`, and `m.room.guest_access` state events.
///
- /// 5. Events listed in ``initial_state``, in the order that they are
+ /// 5. Events listed in `initial_state`, in the order that they are
/// listed.
///
- /// 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and
- /// ``m.room.topic``
+ /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
/// state events).
///
- /// 7. Invite events implied by ``invite`` and ``invite_3pid``
- /// (``m.room.member`` with
- /// ``membership: invite`` and ``m.room.third_party_invite``).
+ /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member`
+ /// with
+ /// `membership: invite` and `m.room.third_party_invite`).
///
/// The available presets do the following with respect to room state:
///
- /// ======================== ============== ======================
- /// ================ =========
- /// Preset ``join_rules`` ``history_visibility``
- /// ``guest_access`` Other
- /// ======================== ============== ======================
- /// ================ =========
- /// ``private_chat`` ``invite`` ``shared`` ``can_join``
- /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All
- /// invitees are given the same power level as the room creator.
- /// ``public_chat`` ``public`` ``shared`` ``forbidden``
- /// ======================== ============== ======================
- /// ================ =========
+ /// | Preset | `join_rules` | `history_visibility` |
+ /// `guest_access` | Other |
+ /// |------------------------|--------------|----------------------|----------------|-------|
+ /// | `private_chat` | `invite` | `shared` |
+ /// `can_join` | | | `trusted_private_chat` | `invite` |
+ /// `shared` | `can_join` | All invitees are given the same
+ /// power level as the room creator. | | `public_chat` | `public`
+ /// | `shared` | `forbidden` | |
///
- /// The server will create a ``m.room.create`` event in the room with the
+ /// The server will create a `m.room.create` event in the room with the
/// requesting user as the creator, alongside other keys provided in the
- /// ``creation_content``.
+ /// `creation_content`.
struct StateEvent {
/// The type of event to send.
QString type;
@@ -191,12 +174,12 @@ public:
/*! \brief Create a new room
*
* \param visibility
- * A ``public`` visibility indicates that the room will be shown
- * in the published room list. A ``private`` visibility will hide
+ * A `public` visibility indicates that the room will be shown
+ * in the published room list. A `private` visibility will hide
* the room from the published room list. Rooms default to
- * ``private`` visibility if this key is not included. NB: This
- * should not be confused with ``join_rules`` which also uses the
- * word ``public``.
+ * `private` visibility if this key is not included. NB: This
+ * should not be confused with `join_rules` which also uses the
+ * word `public`.
*
* \param roomAliasName
* The desired room alias **local part**. If this is included, a
@@ -204,20 +187,20 @@ public:
* room. The alias will belong on the *same* homeserver which
* created the room. For example, if this was set to "foo" and
* sent to the homeserver "example.com" the complete room alias
- * would be ``#foo:example.com``.
+ * would be `#foo:example.com`.
*
* The complete room alias will become the canonical alias for
* the room.
*
* \param name
- * If this is included, an ``m.room.name`` event will be sent
+ * If this is included, an `m.room.name` event will be sent
* into the room to indicate the name of the room. See Room
- * Events for more information on ``m.room.name``.
+ * Events for more information on `m.room.name`.
*
* \param topic
- * If this is included, an ``m.room.topic`` event will be sent
+ * If this is included, an `m.room.topic` event will be sent
* into the room to indicate the topic for the room. See Room
- * Events for more information on ``m.room.topic``.
+ * Events for more information on `m.room.topic`.
*
* \param invite
* A list of user IDs to invite to the room. This will tell the
@@ -230,14 +213,14 @@ public:
* \param roomVersion
* The room version to set for the room. If not provided, the homeserver
* is to use its configured default. If provided, the homeserver will return
- * a 400 error with the errcode ``M_UNSUPPORTED_ROOM_VERSION`` if it does
- * not support the room version.
+ * a 400 error with the errcode `M_UNSUPPORTED_ROOM_VERSION` if it does not
+ * support the room version.
*
* \param creationContent
- * Extra keys, such as ``m.federate``, to be added to the content
- * of the `m.room.create`_ event. The server will clobber the following
- * keys: ``creator``, ``room_version``. Future versions of the
- * specification may allow the server to clobber other keys.
+ * Extra keys, such as `m.federate`, to be added to the content
+ * of the [`m.room.create`](client-server-api/#mroomcreate) event. The
+ * server will clobber the following keys: `creator`, `room_version`. Future
+ * versions of the specification may allow the server to clobber other keys.
*
* \param initialState
* A list of state events to set in the new room. This allows
@@ -245,28 +228,30 @@ public:
* room. The expected format of the state events are an object
* with type, state_key and content keys set.
*
- * Takes precedence over events set by ``preset``, but gets
- * overriden by ``name`` and ``topic`` keys.
+ * Takes precedence over events set by `preset`, but gets
+ * overriden by `name` and `topic` keys.
*
* \param preset
* Convenience parameter for setting various default state events
* based on a preset.
*
- * If unspecified, the server should use the ``visibility`` to determine
- * which preset to use. A visbility of ``public`` equates to a preset of
- * ``public_chat`` and ``private`` visibility equates to a preset of
- * ``private_chat``.
+ * If unspecified, the server should use the `visibility` to determine
+ * which preset to use. A visbility of `public` equates to a preset of
+ * `public_chat` and `private` visibility equates to a preset of
+ * `private_chat`.
*
* \param isDirect
- * This flag makes the server set the ``is_direct`` flag on the
- * ``m.room.member`` events sent to the users in ``invite`` and
- * ``invite_3pid``. See `Direct Messaging`_ for more information.
+ * This flag makes the server set the `is_direct` flag on the
+ * `m.room.member` events sent to the users in `invite` and
+ * `invite_3pid`. See [Direct
+ * Messaging](/client-server-api/#direct-messaging) for more information.
*
* \param powerLevelContentOverride
* The power level content to override in the default power level
* event. This object is applied on top of the generated
- * `m.room.power_levels`_ event content prior to it being sent to the room.
- * Defaults to overriding nothing.
+ * [`m.room.power_levels`](client-server-api/#mroompower_levels)
+ * event content prior to it being sent to the room. Defaults to
+ * overriding nothing.
*/
explicit CreateRoomJob(const QString& visibility = {},
const QString& roomAliasName = {},
diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp
new file mode 100644
index 00000000..1fa0e949
--- /dev/null
+++ b/lib/csapi/cross_signing.cpp
@@ -0,0 +1,31 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "cross_signing.h"
+
+using namespace Quotient;
+
+UploadCrossSigningKeysJob::UploadCrossSigningKeysJob(
+ const Omittable<CrossSigningKey>& masterKey,
+ const Omittable<CrossSigningKey>& selfSigningKey,
+ const Omittable<CrossSigningKey>& userSigningKey)
+ : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"),
+ makePath("/_matrix/client/r0", "/keys/device_signing/upload"))
+{
+ QJsonObject _data;
+ addParam<IfNotEmpty>(_data, QStringLiteral("master_key"), masterKey);
+ addParam<IfNotEmpty>(_data, QStringLiteral("self_signing_key"),
+ selfSigningKey);
+ addParam<IfNotEmpty>(_data, QStringLiteral("user_signing_key"),
+ userSigningKey);
+ setRequestData(std::move(_data));
+}
+
+UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob(
+ const QHash<QString, QHash<QString, QJsonObject>>& signatures)
+ : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"),
+ makePath("/_matrix/client/r0", "/keys/signatures/upload"))
+{
+ setRequestData(RequestData(toJson(signatures)));
+}
diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h
new file mode 100644
index 00000000..2ab65e06
--- /dev/null
+++ b/lib/csapi/cross_signing.h
@@ -0,0 +1,72 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "csapi/definitions/cross_signing_key.h"
+
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Upload cross-signing keys.
+ *
+ * Publishes cross-signing keys for the user.
+ *
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
+ */
+class UploadCrossSigningKeysJob : public BaseJob {
+public:
+ /*! \brief Upload cross-signing keys.
+ *
+ * \param masterKey
+ * Optional. The user\'s master key.
+ *
+ * \param selfSigningKey
+ * Optional. The user\'s self-signing key. Must be signed by
+ * the accompanying master key, or by the user\'s most recently
+ * uploaded master key if no master key is included in the
+ * request.
+ *
+ * \param userSigningKey
+ * Optional. The user\'s user-signing key. Must be signed by
+ * the accompanying master key, or by the user\'s most recently
+ * uploaded master key if no master key is included in the
+ * request.
+ */
+ explicit UploadCrossSigningKeysJob(
+ const Omittable<CrossSigningKey>& masterKey = none,
+ const Omittable<CrossSigningKey>& selfSigningKey = none,
+ const Omittable<CrossSigningKey>& userSigningKey = none);
+};
+
+/*! \brief Upload cross-signing signatures.
+ *
+ * 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 {
+public:
+ /*! \brief Upload cross-signing signatures.
+ *
+ * \param signatures
+ * The signatures to be published.
+ */
+ explicit UploadCrossSigningSignaturesJob(
+ const QHash<QString, QHash<QString, QJsonObject>>& signatures = {});
+
+ // Result properties
+
+ /// A map from user ID to key ID to an error for any signatures
+ /// that failed. If a signature was invalid, the `errcode` will
+ /// be set to `M_INVALID_SIGNATURE`.
+ QHash<QString, QHash<QString, QJsonObject>> failures() const
+ {
+ return loadFromJson<QHash<QString, QHash<QString, QJsonObject>>>(
+ "failures"_ls);
+ }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/definitions/cross_signing_key.h b/lib/csapi/definitions/cross_signing_key.h
new file mode 100644
index 00000000..0cec8161
--- /dev/null
+++ b/lib/csapi/definitions/cross_signing_key.h
@@ -0,0 +1,47 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "converters.h"
+
+namespace Quotient {
+/// Cross signing key
+struct CrossSigningKey {
+ /// The ID of the user the key belongs to.
+ QString userId;
+
+ /// What the key is used for.
+ QStringList usage;
+
+ /// The public key. The object must have exactly one property, whose name
+ /// is in the form `<algorithm>:<unpadded_base64_public_key>`, and whose
+ /// value is the unpadded base64 public key.
+ QHash<QString, QString> keys;
+
+ /// Signatures of the key, calculated using the process described at
+ /// [Signing JSON](/appendices/#signing-json). Optional for the master key.
+ /// Other keys must be signed by the user\'s master key.
+ QJsonObject signatures;
+};
+
+template <>
+struct JsonObjectConverter<CrossSigningKey> {
+ static void dumpTo(QJsonObject& jo, const CrossSigningKey& pod)
+ {
+ addParam<>(jo, QStringLiteral("user_id"), pod.userId);
+ addParam<>(jo, QStringLiteral("usage"), pod.usage);
+ addParam<>(jo, QStringLiteral("keys"), pod.keys);
+ addParam<IfNotEmpty>(jo, QStringLiteral("signatures"), pod.signatures);
+ }
+ static void fillFrom(const QJsonObject& jo, CrossSigningKey& pod)
+ {
+ fromJson(jo.value("user_id"_ls), pod.userId);
+ fromJson(jo.value("usage"_ls), pod.usage);
+ fromJson(jo.value("keys"_ls), pod.keys);
+ fromJson(jo.value("signatures"_ls), pod.signatures);
+ }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h
index 3065f218..84ecefae 100644
--- a/lib/csapi/definitions/device_keys.h
+++ b/lib/csapi/definitions/device_keys.h
@@ -21,15 +21,15 @@ struct DeviceKeys {
QStringList algorithms;
/// Public identity keys. The names of the properties should be in the
- /// format ``<algorithm>:<device_id>``. The keys themselves should be
+ /// format `<algorithm>:<device_id>`. The keys themselves should be
/// encoded as specified by the key algorithm.
QHash<QString, QString> keys;
/// Signatures for the device key object. A map from user ID, to a map from
- /// ``<algorithm>:<device_id>`` to the signature.
+ /// `<algorithm>:<device_id>` to the signature.
///
- /// The signature is calculated using the process described at `Signing
- /// JSON`_.
+ /// The signature is calculated using the process described at [Signing
+ /// JSON](/appendices/#signing-json).
QHash<QString, QHash<QString, QString>> signatures;
};
diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h
index 67497412..c55d4f92 100644
--- a/lib/csapi/definitions/event_filter.h
+++ b/lib/csapi/definitions/event_filter.h
@@ -14,13 +14,13 @@ struct EventFilter {
/// A list of sender IDs to exclude. If this list is absent then no senders
/// are excluded. A matching sender will be excluded even if it is listed in
- /// the ``'senders'`` filter.
+ /// the `'senders'` filter.
QStringList notSenders;
/// A list of event types to exclude. If this list is absent then no event
/// types are excluded. A matching type will be excluded even if it is
- /// listed in the ``'types'`` filter. A '*' can be used as a wildcard to
- /// match any sequence of characters.
+ /// listed in the `'types'` filter. A '*' can be used as a wildcard to match
+ /// any sequence of characters.
QStringList notTypes;
/// A list of senders IDs to include. If this list is absent then all
@@ -28,7 +28,7 @@ struct EventFilter {
QStringList senders;
/// A list of event types to include. If this list is absent then all event
- /// types are included. A ``'*'`` can be used as a wildcard to match any
+ /// types are included. A `'*'` can be used as a wildcard to match any
/// sequence of characters.
QStringList types;
};
diff --git a/lib/csapi/definitions/openid_token.h b/lib/csapi/definitions/openid_token.h
index 5e68c376..3c447321 100644
--- a/lib/csapi/definitions/openid_token.h
+++ b/lib/csapi/definitions/openid_token.h
@@ -11,10 +11,10 @@ namespace Quotient {
struct OpenidToken {
/// An access token the consumer may use to verify the identity of
/// the person who generated the token. This is given to the federation
- /// API ``GET /openid/userinfo`` to verify the user's identity.
+ /// API `GET /openid/userinfo` to verify the user's identity.
QString accessToken;
- /// The string ``Bearer``.
+ /// The string `Bearer`.
QString tokenType;
/// The homeserver domain the consumer should use when attempting to
diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h
index 8f30e607..2938b4ec 100644
--- a/lib/csapi/definitions/public_rooms_response.h
+++ b/lib/csapi/definitions/public_rooms_response.h
@@ -36,7 +36,13 @@ struct PublicRoomsChunk {
bool guestCanJoin;
/// The URL for the room's avatar, if one is set.
- QString avatarUrl;
+ QUrl avatarUrl;
+
+ /// The room's join rule. When not present, the room is assumed to
+ /// be `public`. Note that rooms with `invite` join rules are not
+ /// expected here, but rooms with `knock` rules are given their
+ /// near-public nature.
+ QString joinRule;
};
template <>
@@ -54,6 +60,7 @@ struct JsonObjectConverter<PublicRoomsChunk> {
addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable);
addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin);
addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl);
+ addParam<IfNotEmpty>(jo, QStringLiteral("join_rule"), pod.joinRule);
}
static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod)
{
@@ -66,6 +73,7 @@ struct JsonObjectConverter<PublicRoomsChunk> {
fromJson(jo.value("world_readable"_ls), pod.worldReadable);
fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin);
fromJson(jo.value("avatar_url"_ls), pod.avatarUrl);
+ fromJson(jo.value("join_rule"_ls), pod.joinRule);
}
};
diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h
index a6decf1b..ce66d075 100644
--- a/lib/csapi/definitions/push_condition.h
+++ b/lib/csapi/definitions/push_condition.h
@@ -9,26 +9,27 @@
namespace Quotient {
struct PushCondition {
- /// The kind of condition to apply. See `conditions <#conditions>`_ for
- /// more information on the allowed kinds and how they work.
+ /// The kind of condition to apply. See
+ /// [conditions](/client-server-api/#conditions) for more information on the
+ /// allowed kinds and how they work.
QString kind;
- /// Required for ``event_match`` conditions. The dot-separated field of the
+ /// Required for `event_match` conditions. The dot-separated field of the
/// event to match.
///
- /// Required for ``sender_notification_permission`` conditions. The field in
+ /// Required for `sender_notification_permission` conditions. The field in
/// the power level event the user needs a minimum power level for. Fields
- /// must be specified under the ``notifications`` property in the power
- /// level event's ``content``.
+ /// must be specified under the `notifications` property in the power level
+ /// event's `content`.
QString key;
- /// Required for ``event_match`` conditions. The glob-style pattern to
+ /// Required for `event_match` conditions. The glob-style pattern to
/// match against. Patterns with no special glob characters should be
/// treated as having asterisks prepended and appended when testing the
/// condition.
QString pattern;
- /// Required for ``room_member_count`` conditions. A decimal integer
+ /// Required for `room_member_count` conditions. A decimal integer
/// optionally prefixed by one of, ==, <, >, >= or <=. A prefix of < matches
/// rooms where the member count is strictly less than the given number and
/// so forth. If no prefix is present, this parameter defaults to ==.
diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h
index 43749bae..135537c1 100644
--- a/lib/csapi/definitions/push_rule.h
+++ b/lib/csapi/definitions/push_rule.h
@@ -25,10 +25,10 @@ struct PushRule {
/// The conditions that must hold true for an event in order for a rule to
/// be applied to an event. A rule with no conditions always matches. Only
- /// applicable to ``underride`` and ``override`` rules.
+ /// applicable to `underride` and `override` rules.
QVector<PushCondition> conditions;
- /// The glob-style pattern to match against. Only applicable to ``content``
+ /// The glob-style pattern to match against. Only applicable to `content`
/// rules.
QString pattern;
};
diff --git a/lib/csapi/definitions/request_email_validation.h b/lib/csapi/definitions/request_email_validation.h
index ab34862e..b1781e27 100644
--- a/lib/csapi/definitions/request_email_validation.h
+++ b/lib/csapi/definitions/request_email_validation.h
@@ -16,15 +16,14 @@ struct EmailValidationData : RequestEmailValidation {
/// 3PID verification.
///
/// This parameter is deprecated with a plan to be removed in a future
- /// specification version for ``/account/password`` and ``/register``
- /// requests.
+ /// specification version for `/account/password` and `/register` requests.
QString idServer;
/// An access token previously registered with the identity server. Servers
/// can treat this as optional to distinguish between r0.5-compatible
/// clients and this specification version.
///
- /// Required if an ``id_server`` is supplied.
+ /// Required if an `id_server` is supplied.
QString idAccessToken;
};
diff --git a/lib/csapi/definitions/request_msisdn_validation.h b/lib/csapi/definitions/request_msisdn_validation.h
index 8539cd98..4600b48c 100644
--- a/lib/csapi/definitions/request_msisdn_validation.h
+++ b/lib/csapi/definitions/request_msisdn_validation.h
@@ -16,15 +16,14 @@ struct MsisdnValidationData : RequestMsisdnValidation {
/// 3PID verification.
///
/// This parameter is deprecated with a plan to be removed in a future
- /// specification version for ``/account/password`` and ``/register``
- /// requests.
+ /// specification version for `/account/password` and `/register` requests.
QString idServer;
/// An access token previously registered with the identity server. Servers
/// can treat this as optional to distinguish between r0.5-compatible
/// clients and this specification version.
///
- /// Required if an ``id_server`` is supplied.
+ /// Required if an `id_server` is supplied.
QString idAccessToken;
};
diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h
index 00222839..d5fbbadb 100644
--- a/lib/csapi/definitions/request_token_response.h
+++ b/lib/csapi/definitions/request_token_response.h
@@ -10,22 +10,22 @@ namespace Quotient {
struct RequestTokenResponse {
/// The session ID. Session IDs are opaque strings that must consist
- /// entirely of the characters ``[0-9a-zA-Z.=_-]``. Their length must not
+ /// entirely of the characters `[0-9a-zA-Z.=_-]`. Their length must not
/// exceed 255 characters and they must not be empty.
QString sid;
/// An optional field containing a URL where the client must submit the
/// validation token to, with identical parameters to the Identity Service
- /// API's ``POST /validate/email/submitToken`` endpoint (without the
+ /// API's `POST /validate/email/submitToken` endpoint (without the
/// requirement for an access token). The homeserver must send this token to
/// the user (if applicable), who should then be prompted to provide it to
/// the client.
///
/// If this field is not present, the client can assume that verification
/// will happen without the client's involvement provided the homeserver
- /// advertises this specification version in the ``/versions`` response
+ /// advertises this specification version in the `/versions` response
/// (ie: r0.5.0).
- QString submitUrl;
+ QUrl submitUrl;
};
template <>
diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h
index 11e87fde..91caf667 100644
--- a/lib/csapi/definitions/room_event_filter.h
+++ b/lib/csapi/definitions/room_event_filter.h
@@ -11,30 +11,31 @@
namespace Quotient {
struct RoomEventFilter : EventFilter {
- /// If ``true``, enables lazy-loading of membership events. See
- /// `Lazy-loading room members <#lazy-loading-room-members>`_
- /// for more information. Defaults to ``false``.
+ /// If `true`, enables lazy-loading of membership events. See
+ /// [Lazy-loading room
+ /// members](/client-server-api/#lazy-loading-room-members) for more
+ /// information. Defaults to `false`.
Omittable<bool> lazyLoadMembers;
- /// If ``true``, sends all membership events for all events, even if they
- /// have already been sent to the client. Does not apply unless
- /// ``lazy_load_members`` is ``true``. See `Lazy-loading room members
- /// <#lazy-loading-room-members>`_ for more information. Defaults to
- /// ``false``.
+ /// If `true`, sends all membership events for all events, even if they have
+ /// already been sent to the client. Does not apply unless
+ /// `lazy_load_members` is `true`. See [Lazy-loading room
+ /// members](/client-server-api/#lazy-loading-room-members) for more
+ /// information. Defaults to `false`.
Omittable<bool> includeRedundantMembers;
/// A list of room IDs to exclude. If this list is absent then no rooms are
/// excluded. A matching room will be excluded even if it is listed in the
- /// ``'rooms'`` filter.
+ /// `'rooms'` filter.
QStringList notRooms;
/// A list of room IDs to include. If this list is absent then all rooms are
/// included.
QStringList rooms;
- /// If ``true``, includes only events with a ``url`` key in their content.
- /// If ``false``, excludes those events. If omitted, ``url`` key is not
- /// considered for filtering.
+ /// If `true`, includes only events with a `url` key in their content. If
+ /// `false`, excludes those events. If omitted, `url` key is not considered
+ /// for filtering.
Omittable<bool> containsUrl;
};
diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h
index 9c8f08d5..62e17962 100644
--- a/lib/csapi/definitions/sync_filter.h
+++ b/lib/csapi/definitions/sync_filter.h
@@ -14,13 +14,13 @@ namespace Quotient {
struct RoomFilter {
/// A list of room IDs to exclude. If this list is absent then no rooms are
/// excluded. A matching room will be excluded even if it is listed in the
- /// ``'rooms'`` filter. This filter is applied before the filters in
- /// ``ephemeral``, ``state``, ``timeline`` or ``account_data``
+ /// `'rooms'` filter. This filter is applied before the filters in
+ /// `ephemeral`, `state`, `timeline` or `account_data`
QStringList notRooms;
/// A list of room IDs to include. If this list is absent then all rooms are
- /// included. This filter is applied before the filters in ``ephemeral``,
- /// ``state``, ``timeline`` or ``account_data``
+ /// included. This filter is applied before the filters in `ephemeral`,
+ /// `state`, `timeline` or `account_data`
QStringList rooms;
/// The events that aren't recorded in the room history, e.g. typing and
diff --git a/lib/csapi/definitions/third_party_signed.h b/lib/csapi/definitions/third_party_signed.h
index 526545d0..7097bda4 100644
--- a/lib/csapi/definitions/third_party_signed.h
+++ b/lib/csapi/definitions/third_party_signed.h
@@ -7,7 +7,7 @@
#include "converters.h"
namespace Quotient {
-/// A signature of an ``m.third_party_invite`` token to prove that this user
+/// A signature of an `m.third_party_invite` token to prove that this user
/// owns a third party identity which has been invited to the room.
struct ThirdPartySigned {
/// The Matrix ID of the user who issued the invite.
diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h
index dadf6f97..cb585a6a 100644
--- a/lib/csapi/definitions/user_identifier.h
+++ b/lib/csapi/definitions/user_identifier.h
@@ -9,8 +9,9 @@
namespace Quotient {
/// Identification information for a user
struct UserIdentifier {
- /// The type of identification. See `Identifier types`_ for supported
- /// values and additional property descriptions.
+ /// The type of identification. See [Identifier
+ /// types](/client-server-api/#identifier-types) for supported values and
+ /// additional property descriptions.
QString type;
/// Identification information for a user
diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h
index 5cfaca24..b7db4182 100644
--- a/lib/csapi/definitions/wellknown/homeserver.h
+++ b/lib/csapi/definitions/wellknown/homeserver.h
@@ -10,7 +10,7 @@ namespace Quotient {
/// Used by clients to discover homeserver information.
struct HomeserverInformation {
/// The base URL for the homeserver for client-server connections.
- QString baseUrl;
+ QUrl baseUrl;
};
template <>
diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h
index 3bd07bd1..885e3d34 100644
--- a/lib/csapi/definitions/wellknown/identity_server.h
+++ b/lib/csapi/definitions/wellknown/identity_server.h
@@ -10,7 +10,7 @@ namespace Quotient {
/// Used by clients to discover identity server information.
struct IdentityServerInformation {
/// The base URL for the identity server for client-server connections.
- QString baseUrl;
+ QUrl baseUrl;
};
template <>
diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp
index eac9a545..da6dbc76 100644
--- a/lib/csapi/device_management.cpp
+++ b/lib/csapi/device_management.cpp
@@ -4,38 +4,35 @@
#include "device_management.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/devices");
+ makePath("/_matrix/client/r0", "/devices"));
}
GetDevicesJob::GetDevicesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices")
+ makePath("/_matrix/client/r0", "/devices"))
{}
QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/devices/" % deviceId);
+ makePath("/_matrix/client/r0", "/devices/",
+ deviceId));
}
GetDeviceJob::GetDeviceJob(const QString& deviceId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/r0", "/devices/", deviceId))
{}
UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId,
const QString& displayName)
: BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/r0", "/devices/", deviceId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("display_name"), displayName);
@@ -45,7 +42,7 @@ UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId,
DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/r0", "/devices/", deviceId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
@@ -55,7 +52,7 @@ DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId,
DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"),
- QStringLiteral("/_matrix/client/r0") % "/delete_devices")
+ makePath("/_matrix/client/r0", "/delete_devices"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("devices"), devices);
diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h
index 47dc7ec8..7fb69873 100644
--- a/lib/csapi/device_management.h
+++ b/lib/csapi/device_management.h
@@ -83,7 +83,8 @@ public:
/*! \brief Delete a device
*
- * This API endpoint uses the `User-Interactive Authentication API`_.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
*
* Deletes the given device, and invalidates any access token associated with it.
*/
@@ -104,7 +105,8 @@ public:
/*! \brief Bulk deletion of devices
*
- * This API endpoint uses the `User-Interactive Authentication API`_.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
*
* Deletes the given devices, and invalidates any access token associated with
* them.
diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp
index 25ea82e2..b351b4ef 100644
--- a/lib/csapi/directory.cpp
+++ b/lib/csapi/directory.cpp
@@ -4,14 +4,11 @@
#include "directory.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/room/"
- % roomAlias)
+ makePath("/_matrix/client/r0", "/directory/room/", roomAlias))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("room_id"), roomId);
@@ -21,41 +18,38 @@ SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId
QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/room/" % roomAlias);
+ makePath("/_matrix/client/r0",
+ "/directory/room/", roomAlias));
}
GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/room/"
- % roomAlias,
+ makePath("/_matrix/client/r0", "/directory/room/", roomAlias),
false)
{}
QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/room/" % roomAlias);
+ makePath("/_matrix/client/r0",
+ "/directory/room/", roomAlias));
}
DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/room/"
- % roomAlias)
+ makePath("/_matrix/client/r0", "/directory/room/", roomAlias))
{}
QUrl GetLocalAliasesJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/aliases");
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/aliases"));
}
GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/aliases")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/aliases"))
{
addExpectedKey("aliases");
}
diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h
index 9b109aad..93a31595 100644
--- a/lib/csapi/directory.h
+++ b/lib/csapi/directory.h
@@ -68,13 +68,13 @@ public:
* instance that room aliases can only be deleted by their creator or a server
* administrator.
*
- * .. Note::
- * Servers may choose to update the ``alt_aliases`` for the
- * ``m.room.canonical_alias`` state event in the room when an alias is removed.
+ * **Note:**
+ * Servers may choose to update the `alt_aliases` for the
+ * `m.room.canonical_alias` state event in the room when an alias is removed.
* Servers which choose to update the canonical alias event are recommended to,
* in addition to their other relevant permission checks, delete the alias and
* return a successful response even if the user does not have permission to
- * update the ``m.room.canonical_alias`` event.
+ * update the `m.room.canonical_alias` event.
*/
class DeleteRoomAliasJob : public BaseJob {
public:
@@ -99,18 +99,18 @@ public:
* given room.
*
* This endpoint can be called by users who are in the room (external
- * users receive an ``M_FORBIDDEN`` error response). If the room's
- * ``m.room.history_visibility`` maps to ``world_readable``, any
+ * users receive an `M_FORBIDDEN` error response). If the room's
+ * `m.room.history_visibility` maps to `world_readable`, any
* user can call this endpoint.
*
* Servers may choose to implement additional access control checks here,
* such as allowing server administrators to view aliases regardless of
* membership.
*
- * .. Note::
- * Clients are recommended not to display this list of aliases prominently
- * as they are not curated, unlike those listed in the
- * ``m.room.canonical_alias`` state event.
+ * **Note:**
+ * Clients are recommended not to display this list of aliases prominently
+ * as they are not curated, unlike those listed in the `m.room.canonical_alias`
+ * state event.
*/
class GetLocalAliasesJob : public BaseJob {
public:
diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp
index d2a5f522..877838e2 100644
--- a/lib/csapi/event_context.cpp
+++ b/lib/csapi/event_context.cpp
@@ -4,13 +4,11 @@
#include "event_context.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToGetEventContext(Omittable<int> limit, const QString& filter)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
addParam<IfNotEmpty>(_q, QStringLiteral("filter"), filter);
return _q;
@@ -22,9 +20,8 @@ QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& filter)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/context/"
- % eventId,
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/context/", eventId),
queryToGetEventContext(limit, filter));
}
@@ -33,7 +30,7 @@ GetEventContextJob::GetEventContextJob(const QString& roomId,
Omittable<int> limit,
const QString& filter)
: BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/context/" % eventId,
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/context/",
+ eventId),
queryToGetEventContext(limit, filter))
{}
diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h
index d82d16ab..4e50edf3 100644
--- a/lib/csapi/event_context.h
+++ b/lib/csapi/event_context.h
@@ -16,8 +16,8 @@ namespace Quotient {
* surrounding an event.
*
* *Note*: This endpoint supports lazy-loading of room member events. See
- * `Lazy-loading room members <#lazy-loading-room-members>`_ for more
- * information.
+ * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members)
+ * for more information.
*/
class GetEventContextJob : public BaseJob {
public:
@@ -33,13 +33,13 @@ public:
* The maximum number of events to return. Default: 10.
*
* \param filter
- * A JSON ``RoomEventFilter`` to filter the returned events with. The
- * filter is only applied to ``events_before``, ``events_after``, and
- * ``state``. It is not applied to the ``event`` itself. The filter may
- * be applied before or/and after the ``limit`` parameter - whichever the
+ * A JSON `RoomEventFilter` to filter the returned events with. The
+ * filter is only applied to `events_before`, `events_after`, and
+ * `state`. It is not applied to the `event` itself. The filter may
+ * be applied before or/and after the `limit` parameter - whichever the
* homeserver prefers.
*
- * See `Filtering <#filtering>`_ for more information.
+ * See [Filtering](/client-server-api/#filtering) for more information.
*/
explicit GetEventContextJob(const QString& roomId, const QString& eventId,
Omittable<int> limit = none,
diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp
index bb3a893f..38c68be7 100644
--- a/lib/csapi/filter.cpp
+++ b/lib/csapi/filter.cpp
@@ -4,16 +4,13 @@
#include "filter.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter)
: BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/filter")
+ makePath("/_matrix/client/r0", "/user/", userId, "/filter"))
{
- setRequestData(Data(toJson(filter)));
+ setRequestData(RequestData(toJson(filter)));
addExpectedKey("filter_id");
}
@@ -21,12 +18,12 @@ QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& filterId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/user/"
- % userId % "/filter/" % filterId);
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/filter/", filterId));
}
GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/filter/" % filterId)
+ makePath("/_matrix/client/r0", "/user/", userId, "/filter/",
+ filterId))
{}
diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h
index f07b489c..01bec36b 100644
--- a/lib/csapi/filter.h
+++ b/lib/csapi/filter.h
@@ -32,7 +32,7 @@ public:
// Result properties
/// The ID of the filter that was created. Cannot start
- /// with a ``{`` as this character is used to determine
+ /// with a `{` as this character is used to determine
/// if the filter provided is inline JSON or a previously
/// declared filter by homeservers on some APIs.
QString filterId() const { return loadFromJson<QString>("filter_id"_ls); }
diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp
index 01620f9e..39d24611 100644
--- a/lib/csapi/inviting.cpp
+++ b/lib/csapi/inviting.cpp
@@ -4,16 +4,15 @@
#include "inviting.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
-InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId)
+InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId,
+ const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/invite")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("user_id"), userId);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
}
diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h
index 59a61b89..eb13cc95 100644
--- a/lib/csapi/inviting.h
+++ b/lib/csapi/inviting.h
@@ -10,12 +10,11 @@ namespace Quotient {
/*! \brief Invite a user to participate in a particular room.
*
- * .. _invite-by-user-id-endpoint:
- *
* *Note that there are two forms of this API, which are documented separately.
* This version of the API requires that the inviter knows the Matrix
* identifier of the invitee. The other is documented in the*
- * `third party invites section`_.
+ * [third party invites
+ * section](/client-server-api/#post_matrixclientr0roomsroomidinvite-1).
*
* This API invites a user to participate in a particular room.
* They do not start participating in the room until they actually join the
@@ -25,9 +24,7 @@ namespace Quotient {
* join that room.
*
* If the user was invited to the room, the homeserver will append a
- * ``m.room.member`` event to the room.
- *
- * .. _third party invites section: `invite-by-third-party-id-endpoint`_
+ * `m.room.member` event to the room.
*/
class InviteUserJob : public BaseJob {
public:
@@ -38,8 +35,13 @@ public:
*
* \param userId
* The fully qualified user ID of the invitee.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
- explicit InviteUserJob(const QString& roomId, const QString& userId);
+ explicit InviteUserJob(const QString& roomId, const QString& userId,
+ const QString& reason = {});
};
} // namespace Quotient
diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp
index 4761e949..373c1c6a 100644
--- a/lib/csapi/joining.cpp
+++ b/lib/csapi/joining.cpp
@@ -4,39 +4,41 @@
#include "joining.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
JoinRoomByIdJob::JoinRoomByIdJob(
- const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned)
+ const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned,
+ const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/join")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/join"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"),
thirdPartySigned);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
addExpectedKey("room_id");
}
auto queryToJoinRoom(const QStringList& serverName)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("server_name"), serverName);
return _q;
}
JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias,
const QStringList& serverName,
- const Omittable<ThirdPartySigned>& thirdPartySigned)
+ const Omittable<ThirdPartySigned>& thirdPartySigned,
+ const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/join/" % roomIdOrAlias,
+ makePath("/_matrix/client/r0", "/join/", roomIdOrAlias),
queryToJoinRoom(serverName))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"),
thirdPartySigned);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
addExpectedKey("room_id");
}
diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h
index dd936f92..d0199b11 100644
--- a/lib/csapi/joining.h
+++ b/lib/csapi/joining.h
@@ -13,7 +13,7 @@ namespace Quotient {
/*! \brief Start the requesting user participating in a particular room.
*
* *Note that this API requires a room ID, not alias.*
- * ``/join/{roomIdOrAlias}`` *exists if you have a room alias.*
+ * `/join/{roomIdOrAlias}` *exists if you have a room alias.*
*
* This API starts a user participating in a particular room, if that user
* is allowed to participate in that room. After this call, the client is
@@ -21,7 +21,9 @@ namespace Quotient {
* events associated with the room until the user leaves the room.
*
* After a user has joined a room, the room will appear as an entry in the
- * response of the |/initialSync|_ and |/sync|_ APIs.
+ * response of the
+ * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs.
*/
class JoinRoomByIdJob : public BaseJob {
public:
@@ -32,12 +34,17 @@ public:
*
* \param thirdPartySigned
* If supplied, the homeserver must verify that it matches a pending
- * ``m.room.third_party_invite`` event in the room, and perform
+ * `m.room.third_party_invite` event in the room, and perform
* key validity checking if required by the event.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
explicit JoinRoomByIdJob(
const QString& roomId,
- const Omittable<ThirdPartySigned>& thirdPartySigned = none);
+ const Omittable<ThirdPartySigned>& thirdPartySigned = none,
+ const QString& reason = {});
// Result properties
@@ -48,7 +55,7 @@ public:
/*! \brief Start the requesting user participating in a particular room.
*
* *Note that this API takes either a room ID or alias, unlike*
- * ``/room/{roomId}/join``.
+ * `/room/{roomId}/join`.
*
* This API starts a user participating in a particular room, if that user
* is allowed to participate in that room. After this call, the client is
@@ -56,7 +63,9 @@ public:
* events associated with the room until the user leaves the room.
*
* After a user has joined a room, the room will appear as an entry in the
- * response of the |/initialSync|_ and |/sync|_ APIs.
+ * response of the
+ * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs.
*/
class JoinRoomJob : public BaseJob {
public:
@@ -70,13 +79,18 @@ public:
* must be participating in the room.
*
* \param thirdPartySigned
- * If a ``third_party_signed`` was supplied, the homeserver must verify
- * that it matches a pending ``m.room.third_party_invite`` event in the
+ * If a `third_party_signed` was supplied, the homeserver must verify
+ * that it matches a pending `m.room.third_party_invite` event in the
* room, and perform key validity checking if required by the event.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
explicit JoinRoomJob(
const QString& roomIdOrAlias, const QStringList& serverName = {},
- const Omittable<ThirdPartySigned>& thirdPartySigned = none);
+ const Omittable<ThirdPartySigned>& thirdPartySigned = none,
+ const QString& reason = {});
// Result properties
diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp
index 34ab47c9..d6bd2fab 100644
--- a/lib/csapi/keys.cpp
+++ b/lib/csapi/keys.cpp
@@ -4,14 +4,12 @@
#include "keys.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys,
const QHash<QString, QVariant>& oneTimeKeys)
: BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/upload")
+ makePath("/_matrix/client/r0", "/keys/upload"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("device_keys"), deviceKeys);
@@ -23,7 +21,7 @@ UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys,
QueryKeysJob::QueryKeysJob(const QHash<QString, QStringList>& deviceKeys,
Omittable<int> timeout, const QString& token)
: BaseJob(HttpVerb::Post, QStringLiteral("QueryKeysJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/query")
+ makePath("/_matrix/client/r0", "/keys/query"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout);
@@ -36,7 +34,7 @@ ClaimKeysJob::ClaimKeysJob(
const QHash<QString, QHash<QString, QString>>& oneTimeKeys,
Omittable<int> timeout)
: BaseJob(HttpVerb::Post, QStringLiteral("ClaimKeysJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/claim")
+ makePath("/_matrix/client/r0", "/keys/claim"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout);
@@ -47,7 +45,7 @@ ClaimKeysJob::ClaimKeysJob(
auto queryToGetKeysChanges(const QString& from, const QString& to)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("from"), from);
addParam<>(_q, QStringLiteral("to"), to);
return _q;
@@ -57,13 +55,13 @@ QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from,
const QString& to)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/keys/changes",
+ makePath("/_matrix/client/r0",
+ "/keys/changes"),
queryToGetKeysChanges(from, to));
}
GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to)
: BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/changes",
+ makePath("/_matrix/client/r0", "/keys/changes"),
queryToGetKeysChanges(from, to))
{}
diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h
index 8f6c8cc9..7db09e8d 100644
--- a/lib/csapi/keys.h
+++ b/lib/csapi/keys.h
@@ -4,6 +4,7 @@
#pragma once
+#include "csapi/definitions/cross_signing_key.h"
#include "csapi/definitions/device_keys.h"
#include "jobs/basejob.h"
@@ -25,8 +26,8 @@ public:
* \param oneTimeKeys
* One-time public keys for "pre-key" messages. The names of
* the properties should be in the format
- * ``<algorithm>:<key_id>``. The format of the key is determined
- * by the `key algorithm <#key-algorithms>`_.
+ * `<algorithm>:<key_id>`. The format of the key is determined
+ * by the [key algorithm](/client-server-api/#key-algorithms).
*
* May be absent if no new one-time keys are required.
*/
@@ -98,7 +99,7 @@ public:
///
/// If the homeserver could be reached, but the user or device
/// was unknown, no failure is recorded. Instead, the corresponding
- /// user or device is missing from the ``device_keys`` result.
+ /// user or device is missing from the `device_keys` result.
QHash<QString, QJsonObject> failures() const
{
return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls);
@@ -107,13 +108,45 @@ public:
/// Information on the queried devices. A map from user ID, to a
/// map from device ID to device information. For each device,
/// the information returned will be the same as uploaded via
- /// ``/keys/upload``, with the addition of an ``unsigned``
+ /// `/keys/upload`, with the addition of an `unsigned`
/// property.
QHash<QString, QHash<QString, DeviceInformation>> deviceKeys() const
{
return loadFromJson<QHash<QString, QHash<QString, DeviceInformation>>>(
"device_keys"_ls);
}
+
+ /// Information on the master cross-signing keys of the queried users.
+ /// A map from user ID, to master key information. For each key, the
+ /// information returned will be the same as uploaded via
+ /// `/keys/device_signing/upload`, along with the signatures
+ /// uploaded via `/keys/signatures/upload` that the requesting user
+ /// is allowed to see.
+ QHash<QString, CrossSigningKey> masterKeys() const
+ {
+ return loadFromJson<QHash<QString, CrossSigningKey>>("master_keys"_ls);
+ }
+
+ /// Information on the self-signing keys of the queried users. A map
+ /// from user ID, to self-signing key information. For each key, the
+ /// information returned will be the same as uploaded via
+ /// `/keys/device_signing/upload`.
+ QHash<QString, CrossSigningKey> selfSigningKeys() const
+ {
+ return loadFromJson<QHash<QString, CrossSigningKey>>(
+ "self_signing_keys"_ls);
+ }
+
+ /// Information on the user-signing key of the user making the
+ /// request, if they queried their own device information. A map
+ /// from user ID, to user-signing key information. The
+ /// information returned will be the same as uploaded via
+ /// `/keys/device_signing/upload`.
+ QHash<QString, CrossSigningKey> userSigningKeys() const
+ {
+ return loadFromJson<QHash<QString, CrossSigningKey>>(
+ "user_signing_keys"_ls);
+ }
};
template <>
@@ -163,18 +196,17 @@ public:
///
/// If the homeserver could be reached, but the user or device
/// was unknown, no failure is recorded. Instead, the corresponding
- /// user or device is missing from the ``one_time_keys`` result.
+ /// user or device is missing from the `one_time_keys` result.
QHash<QString, QJsonObject> failures() const
{
return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls);
}
/// One-time keys for the queried devices. A map from user ID, to a
- /// map from devices to a map from ``<algorithm>:<key_id>`` to the key
- /// object.
+ /// map from devices to a map from `<algorithm>:<key_id>` to the key object.
///
- /// See the `key algorithms <#key-algorithms>`_ section for information
- /// on the Key Object format.
+ /// See the [key algorithms](/client-server-api/#key-algorithms) section for
+ /// information on the Key Object format.
QHash<QString, QHash<QString, QVariant>> oneTimeKeys() const
{
return loadFromJson<QHash<QString, QHash<QString, QVariant>>>(
@@ -190,26 +222,28 @@ public:
* The server should include in the results any users who:
*
* * currently share a room with the calling user (ie, both users have
- * membership state ``join``); *and*
+ * membership state `join`); *and*
* * added new device identity keys or removed an existing device with
- * identity keys, between ``from`` and ``to``.
+ * identity keys, between `from` and `to`.
*/
class GetKeysChangesJob : public BaseJob {
public:
/*! \brief Query users with recent device key updates.
*
* \param from
- * The desired start point of the list. Should be the ``next_batch`` field
- * from a response to an earlier call to |/sync|. Users who have not
+ * The desired start point of the list. Should be the `next_batch` field
+ * from a response to an earlier call to
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync). Users who have not
* uploaded new device identity keys since this point, nor deleted
* existing devices with identity keys since then, will be excluded
* from the results.
*
* \param to
- * The desired end point of the list. Should be the ``next_batch``
- * field from a recent call to |/sync| - typically the most recent
- * such call. This may be used by the server as a hint to check its
- * caches are up to date.
+ * The desired end point of the list. Should be the `next_batch`
+ * field from a recent call to
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync) - typically the
+ * most recent such call. This may be used by the server as a hint to check
+ * its caches are up to date.
*/
explicit GetKeysChangesJob(const QString& from, const QString& to);
diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp
index 7de5ce01..433e592c 100644
--- a/lib/csapi/kicking.cpp
+++ b/lib/csapi/kicking.cpp
@@ -4,14 +4,12 @@
#include "kicking.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
KickJob::KickJob(const QString& roomId, const QString& userId,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("KickJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/kick")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/kick"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("user_id"), userId);
diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h
index 2645a54f..11018368 100644
--- a/lib/csapi/kicking.h
+++ b/lib/csapi/kicking.h
@@ -15,10 +15,10 @@ namespace Quotient {
* The caller must have the required power level in order to perform this
* operation.
*
- * Kicking a user adjusts the target member's membership state to be ``leave``
- * with an optional ``reason``. Like with other membership changes, a user can
+ * Kicking a user adjusts the target member's membership state to be `leave`
+ * with an optional `reason`. Like with other membership changes, a user can
* directly adjust the target member's state by making a request to
- * ``/rooms/<room id>/state/m.room.member/<user id>``.
+ * `/rooms/<room id>/state/m.room.member/<user id>`.
*/
class KickJob : public BaseJob {
public:
@@ -32,7 +32,8 @@ public:
*
* \param reason
* The reason the user has been kicked. This will be supplied as the
- * ``reason`` on the target's updated `m.room.member`_ event.
+ * `reason` on the target's updated
+ * [`m.room.member`](/client-server-api/#mroommember) event.
*/
explicit KickJob(const QString& roomId, const QString& userId,
const QString& reason = {});
diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp
new file mode 100644
index 00000000..73e13e6e
--- /dev/null
+++ b/lib/csapi/knocking.cpp
@@ -0,0 +1,26 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "knocking.h"
+
+using namespace Quotient;
+
+auto queryToKnockRoom(const QStringList& serverName)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("server_name"), serverName);
+ return _q;
+}
+
+KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias,
+ const QStringList& serverName, const QString& reason)
+ : BaseJob(HttpVerb::Post, QStringLiteral("KnockRoomJob"),
+ makePath("/_matrix/client/r0", "/knock/", roomIdOrAlias),
+ queryToKnockRoom(serverName))
+{
+ QJsonObject _data;
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
+ setRequestData(std::move(_data));
+ addExpectedKey("room_id");
+}
diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h
new file mode 100644
index 00000000..1108cb64
--- /dev/null
+++ b/lib/csapi/knocking.h
@@ -0,0 +1,55 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Knock on a room, requesting permission to join.
+ *
+ * *Note that this API takes either a room ID or alias, unlike other membership
+ * APIs.*
+ *
+ * This API "knocks" on the room to ask for permission to join, if the user
+ * is allowed to knock on the room. Acceptance of the knock happens out of
+ * band from this API, meaning that the client will have to watch for updates
+ * regarding the acceptance/rejection of the knock.
+ *
+ * If the room history settings allow, the user will still be able to see
+ * history of the room while being in the "knock" state. The user will have
+ * to accept the invitation to join the room (acceptance of knock) to see
+ * messages reliably. See the `/join` endpoints for more information about
+ * history visibility to the user.
+ *
+ * The knock will appear as an entry in the response of the
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync) API.
+ */
+class KnockRoomJob : public BaseJob {
+public:
+ /*! \brief Knock on a room, requesting permission to join.
+ *
+ * \param roomIdOrAlias
+ * The room identifier or alias to knock upon.
+ *
+ * \param serverName
+ * The servers to attempt to knock on the room through. One of the servers
+ * must be participating in the room.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
+ */
+ explicit KnockRoomJob(const QString& roomIdOrAlias,
+ const QStringList& serverName = {},
+ const QString& reason = {});
+
+ // Result properties
+
+ /// The knocked room ID.
+ QString roomId() const { return loadFromJson<QString>("room_id"_ls); }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp
index 8bd170bf..0e5386be 100644
--- a/lib/csapi/leaving.cpp
+++ b/lib/csapi/leaving.cpp
@@ -4,32 +4,25 @@
#include "leaving.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
-QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
+LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason)
+ : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"),
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/leave"))
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/leave");
+ QJsonObject _data;
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
+ setRequestData(std::move(_data));
}
-LeaveRoomJob::LeaveRoomJob(const QString& roomId)
- : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/leave")
-{}
-
QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/forget");
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/forget"));
}
ForgetRoomJob::ForgetRoomJob(const QString& roomId)
: BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/forget")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/forget"))
{}
diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h
index 1bea7e41..2e402d16 100644
--- a/lib/csapi/leaving.h
+++ b/lib/csapi/leaving.h
@@ -28,15 +28,12 @@ public:
*
* \param roomId
* The room identifier to leave.
- */
- explicit LeaveRoomJob(const QString& roomId);
-
- /*! \brief Construct a URL without creating a full-fledged job object
*
- * This function can be used when a URL for LeaveRoomJob
- * is necessary but the job itself isn't.
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId);
+ explicit LeaveRoomJob(const QString& roomId, const QString& reason = {});
};
/*! \brief Stop the requesting user remembering about a particular room.
diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp
index 8d7e267f..22ba04da 100644
--- a/lib/csapi/list_joined_rooms.cpp
+++ b/lib/csapi/list_joined_rooms.cpp
@@ -4,20 +4,17 @@
#include "list_joined_rooms.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/joined_rooms");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/joined_rooms"));
}
GetJoinedRoomsJob::GetJoinedRoomsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"),
- QStringLiteral("/_matrix/client/r0") % "/joined_rooms")
+ makePath("/_matrix/client/r0", "/joined_rooms"))
{
addExpectedKey("joined_rooms");
}
diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h
index 1034aa7b..59a24a49 100644
--- a/lib/csapi/list_joined_rooms.h
+++ b/lib/csapi/list_joined_rooms.h
@@ -26,7 +26,7 @@ public:
// Result properties
- /// The ID of each room in which the user has ``joined`` membership.
+ /// The ID of each room in which the user has `joined` membership.
QStringList joinedRooms() const
{
return loadFromJson<QStringList>("joined_rooms"_ls);
diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp
index 415d816c..25f8da5c 100644
--- a/lib/csapi/list_public_rooms.cpp
+++ b/lib/csapi/list_public_rooms.cpp
@@ -4,31 +4,27 @@
#include "list_public_rooms.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/list/room/" % roomId);
+ makePath("/_matrix/client/r0",
+ "/directory/list/room/", roomId));
}
GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob(
const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/list/room/"
- % roomId,
+ makePath("/_matrix/client/r0", "/directory/list/room/", roomId),
false)
{}
SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob(
const QString& roomId, const QString& visibility)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomVisibilityOnDirectoryJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/list/room/"
- % roomId)
+ makePath("/_matrix/client/r0", "/directory/list/room/", roomId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility);
@@ -38,7 +34,7 @@ SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob(
auto queryToGetPublicRooms(Omittable<int> limit, const QString& since,
const QString& server)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
addParam<IfNotEmpty>(_q, QStringLiteral("since"), since);
addParam<IfNotEmpty>(_q, QStringLiteral("server"), server);
@@ -50,15 +46,15 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit,
const QString& server)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/publicRooms",
+ makePath("/_matrix/client/r0",
+ "/publicRooms"),
queryToGetPublicRooms(limit, since, server));
}
GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since,
const QString& server)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"),
- QStringLiteral("/_matrix/client/r0") % "/publicRooms",
+ makePath("/_matrix/client/r0", "/publicRooms"),
queryToGetPublicRooms(limit, since, server), {}, false)
{
addExpectedKey("chunk");
@@ -66,7 +62,7 @@ GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since,
auto queryToQueryPublicRooms(const QString& server)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("server"), server);
return _q;
}
@@ -78,7 +74,7 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server,
Omittable<bool> includeAllNetworks,
const QString& thirdPartyInstanceId)
: BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"),
- QStringLiteral("/_matrix/client/r0") % "/publicRooms",
+ makePath("/_matrix/client/r0", "/publicRooms"),
queryToQueryPublicRooms(server))
{
QJsonObject _data;
diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h
index b0cb79d2..963c8b56 100644
--- a/lib/csapi/list_public_rooms.h
+++ b/lib/csapi/list_public_rooms.h
@@ -170,7 +170,7 @@ public:
*
* \param thirdPartyInstanceId
* The specific third party network/protocol to request from the
- * homeserver. Can only be used if ``include_all_networks`` is false.
+ * homeserver. Can only be used if `include_all_networks` is false.
*/
explicit QueryPublicRoomsJob(const QString& server = {},
Omittable<int> limit = none,
diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp
index a5bac9ea..71fd93c5 100644
--- a/lib/csapi/login.cpp
+++ b/lib/csapi/login.cpp
@@ -4,20 +4,17 @@
#include "login.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/login");
+ makePath("/_matrix/client/r0", "/login"));
}
GetLoginFlowsJob::GetLoginFlowsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"),
- QStringLiteral("/_matrix/client/r0") % "/login", false)
+ makePath("/_matrix/client/r0", "/login"), false)
{}
LoginJob::LoginJob(const QString& type,
@@ -26,7 +23,7 @@ LoginJob::LoginJob(const QString& type,
const QString& deviceId,
const QString& initialDeviceDisplayName)
: BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"),
- QStringLiteral("/_matrix/client/r0") % "/login", false)
+ makePath("/_matrix/client/r0", "/login"), false)
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("type"), type);
diff --git a/lib/csapi/login.h b/lib/csapi/login.h
index a406fc79..b35db1eb 100644
--- a/lib/csapi/login.h
+++ b/lib/csapi/login.h
@@ -14,17 +14,17 @@ namespace Quotient {
/*! \brief Get the supported login types to authenticate users
*
* 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.
+ * should pick one of these and supply it as the `type` when logging in.
*/
class GetLoginFlowsJob : public BaseJob {
public:
// Inner data structures
/// Gets the homeserver's supported login types to authenticate users.
- /// Clients should pick one of these and supply it as the ``type`` when
+ /// Clients should pick one of these and supply it as the `type` when
/// logging in.
struct LoginFlow {
- /// The login type. This is supplied as the ``type`` when
+ /// The login type. This is supplied as the `type` when
/// logging in.
QString type;
};
@@ -64,13 +64,14 @@ struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> {
* Authenticates the user, and issues an access token they can
* use to authorize themself in subsequent requests.
*
- * If the client does not supply a ``device_id``, the server must
+ * If the client does not supply a `device_id`, the server must
* auto-generate one.
*
- * The returned access token must be associated with the ``device_id``
+ * The returned access token must be associated with the `device_id`
* supplied by the client or generated by the server. The server may
* invalidate any access token previously associated with that device. See
- * `Relationship between access tokens and devices`_.
+ * [Relationship between access tokens and
+ * devices](/client-server-api/#relationship-between-access-tokens-and-devices).
*/
class LoginJob : public BaseJob {
public:
@@ -83,30 +84,33 @@ public:
* Authenticates the user, and issues an access token they can
* use to authorize themself in subsequent requests.
*
- * If the client does not supply a ``device_id``, the server must
+ * If the client does not supply a `device_id`, the server must
* auto-generate one.
*
- * The returned access token must be associated with the ``device_id``
+ * The returned access token must be associated with the `device_id`
* supplied by the client or generated by the server. The server may
* invalidate any access token previously associated with that device. See
- * `Relationship between access tokens and devices`_.
+ * [Relationship between access tokens and
+ * devices](/client-server-api/#relationship-between-access-tokens-and-devices).
*
* \param password
- * Required when ``type`` is ``m.login.password``. The user's
+ * Required when `type` is `m.login.password`. The user's
* password.
*
* \param token
- * Required when ``type`` is ``m.login.token``. Part of `Token-based`_
- * login.
+ * Required when `type` is `m.login.token`. Part of Token-based login.
*
* \param deviceId
* ID of the client device. If this does not correspond to a
- * known client device, a new device will be created. The server
- * will auto-generate a device_id if this is not specified.
+ * known client device, a new device will be created. The given
+ * device ID must not be the same as a
+ * [cross-signing](/client-server-api/#cross-signing) key ID.
+ * The server will auto-generate a device_id
+ * if this is not specified.
*
* \param initialDeviceDisplayName
* A display name to assign to the newly-created device. Ignored
- * if ``device_id`` corresponds to a known device.
+ * if `device_id` corresponds to a known device.
*/
explicit LoginJob(const QString& type,
const Omittable<UserIdentifier>& identifier = none,
@@ -130,8 +134,8 @@ public:
/// been registered.
///
/// **Deprecated**. Clients should extract the server_name from
- /// ``user_id`` (by splitting at the first colon) if they require
- /// it. Note also that ``homeserver`` is not spelt this way.
+ /// `user_id` (by splitting at the first colon) if they require
+ /// it. Note also that `homeserver` is not spelt this way.
QString homeServer() const
{
return loadFromJson<QString>("home_server"_ls);
diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp
index 9583b8ec..e8083e31 100644
--- a/lib/csapi/logout.cpp
+++ b/lib/csapi/logout.cpp
@@ -4,30 +4,26 @@
#include "logout.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl LogoutJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/logout");
+ makePath("/_matrix/client/r0", "/logout"));
}
LogoutJob::LogoutJob()
: BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"),
- QStringLiteral("/_matrix/client/r0") % "/logout")
+ makePath("/_matrix/client/r0", "/logout"))
{}
QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/logout/all");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/logout/all"));
}
LogoutAllJob::LogoutAllJob()
: BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"),
- QStringLiteral("/_matrix/client/r0") % "/logout/all")
+ makePath("/_matrix/client/r0", "/logout/all"))
{}
diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h
index 78f14e40..2e4c2692 100644
--- a/lib/csapi/logout.h
+++ b/lib/csapi/logout.h
@@ -12,7 +12,8 @@ namespace Quotient {
*
* Invalidates an existing access token, so that it can no longer be used for
* authorization. The device associated with the access token is also deleted.
- * `Device keys <#device-keys>`_ for the device are deleted alongside the device.
+ * [Device keys](/client-server-api/#device-keys) for the device are deleted
+ * alongside the device.
*/
class LogoutJob : public BaseJob {
public:
@@ -31,10 +32,12 @@ public:
*
* Invalidates all access tokens for a user, so that they can no longer be used
* for authorization. This includes the access token that made this request. All
- * devices for the user are also deleted. `Device keys <#device-keys>`_ for the
- * device are deleted alongside the device.
+ * devices for the user are also deleted. [Device
+ * keys](/client-server-api/#device-keys) for the device are deleted alongside
+ * the device.
*
- * This endpoint does not use the `User-Interactive Authentication API`_ because
+ * This endpoint does not use the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api) because
* User-Interactive Authentication is designed to protect against attacks where
* the someone gets hold of a single access token then takes over the account.
* This endpoint invalidates all access tokens for the user, including the token
diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp
index 855c051f..1a93b75b 100644
--- a/lib/csapi/message_pagination.cpp
+++ b/lib/csapi/message_pagination.cpp
@@ -4,15 +4,13 @@
#include "message_pagination.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToGetRoomEvents(const QString& from, const QString& to,
const QString& dir, Omittable<int> limit,
const QString& filter)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("from"), from);
addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
addParam<>(_q, QStringLiteral("dir"), dir);
@@ -28,7 +26,7 @@ QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/messages",
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"),
queryToGetRoomEvents(from, to, dir, limit, filter));
}
@@ -36,7 +34,6 @@ GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from,
const QString& dir, const QString& to,
Omittable<int> limit, const QString& filter)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomEventsJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/messages",
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"),
queryToGetRoomEvents(from, to, dir, limit, filter))
{}
diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h
index 286d4237..363e4d99 100644
--- a/lib/csapi/message_pagination.h
+++ b/lib/csapi/message_pagination.h
@@ -15,8 +15,8 @@ namespace Quotient {
* pagination query parameters to paginate history in the room.
*
* *Note*: This endpoint supports lazy-loading of room member events. See
- * `Lazy-loading room members <#lazy-loading-room-members>`_ for more
- * information.
+ * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members)
+ * for more information.
*/
class GetRoomEventsJob : public BaseJob {
public:
@@ -27,8 +27,8 @@ public:
*
* \param from
* The token to start returning events from. This token can be obtained
- * from a ``prev_batch`` token returned for each room by the sync API,
- * or from a ``start`` or ``end`` token returned by a previous request
+ * from a `prev_batch` token returned for each room by the sync API,
+ * or from a `start` or `end` token returned by a previous request
* to this endpoint.
*
* \param dir
@@ -36,8 +36,8 @@ public:
*
* \param to
* The token to stop returning events at. This token can be obtained from
- * a ``prev_batch`` token returned for each room by the sync endpoint,
- * or from a ``start`` or ``end`` token returned by a previous request to
+ * a `prev_batch` token returned for each room by the sync endpoint,
+ * or from a `start` or `end` token returned by a previous request to
* this endpoint.
*
* \param limit
@@ -64,25 +64,25 @@ public:
// Result properties
- /// The token the pagination starts from. If ``dir=b`` this will be
- /// the token supplied in ``from``.
+ /// The token the pagination starts from. If `dir=b` this will be
+ /// the token supplied in `from`.
QString begin() const { return loadFromJson<QString>("start"_ls); }
- /// The token the pagination ends at. If ``dir=b`` this token should
+ /// The token the pagination ends at. If `dir=b` this token should
/// be used again to request even earlier events.
QString end() const { return loadFromJson<QString>("end"_ls); }
- /// A list of room events. The order depends on the ``dir`` parameter.
- /// For ``dir=b`` events will be in reverse-chronological order,
- /// for ``dir=f`` in chronological order, so that events start
- /// at the ``from`` point.
+ /// A list of room events. The order depends on the `dir` parameter.
+ /// For `dir=b` events will be in reverse-chronological order,
+ /// for `dir=f` in chronological order, so that events start
+ /// at the `from` point.
RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); }
- /// A list of state events relevant to showing the ``chunk``. For example, if
- /// ``lazy_load_members`` is enabled in the filter then this may contain
- /// the membership events for the senders of events in the ``chunk``.
+ /// A list of state events relevant to showing the `chunk`. For example, if
+ /// `lazy_load_members` is enabled in the filter then this may contain
+ /// the membership events for the senders of events in the `chunk`.
///
- /// Unless ``include_redundant_members`` is ``true``, the server
+ /// Unless `include_redundant_members` is `true`, the server
/// may remove membership events which would have already been
/// sent to the client in prior calls to this endpoint, assuming
/// the membership of those members has not changed.
diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp
index a479d500..1e523c6f 100644
--- a/lib/csapi/notifications.cpp
+++ b/lib/csapi/notifications.cpp
@@ -4,14 +4,12 @@
#include "notifications.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToGetNotifications(const QString& from, Omittable<int> limit,
const QString& only)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
addParam<IfNotEmpty>(_q, QStringLiteral("only"), only);
@@ -23,8 +21,8 @@ QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from,
const QString& only)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/notifications",
+ makePath("/_matrix/client/r0",
+ "/notifications"),
queryToGetNotifications(from, limit, only));
}
@@ -32,7 +30,7 @@ GetNotificationsJob::GetNotificationsJob(const QString& from,
Omittable<int> limit,
const QString& only)
: BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"),
- QStringLiteral("/_matrix/client/r0") % "/notifications",
+ makePath("/_matrix/client/r0", "/notifications"),
queryToGetNotifications(from, limit, only))
{
addExpectedKey("notifications");
diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h
index ff499c7a..0c38fe6b 100644
--- a/lib/csapi/notifications.h
+++ b/lib/csapi/notifications.h
@@ -22,7 +22,7 @@ public:
/// user has been, or would have been notified about.
struct Notification {
/// The action(s) to perform when the conditions for this rule are met.
- /// See `Push Rules: API`_.
+ /// See [Push Rules: API](/client-server-api/#push-rules-api).
QVector<QVariant> actions;
/// The Event object for the event that triggered the notification.
EventPtr event;
@@ -35,7 +35,7 @@ public:
QString roomId;
/// The unix timestamp at which the event notification was sent,
/// in milliseconds.
- int ts;
+ qint64 ts;
};
// Construction/destruction
@@ -49,7 +49,7 @@ public:
* Limit on the number of events to return in this request.
*
* \param only
- * Allows basic filtering of events returned. Supply ``highlight``
+ * Allows basic filtering of events returned. Supply `highlight`
* to return only events where the notification had the highlight
* tweak set.
*/
@@ -68,8 +68,8 @@ public:
// Result properties
- /// The token to supply in the ``from`` param of the next
- /// ``/notifications`` request in order to request more
+ /// The token to supply in the `from` param of the next
+ /// `/notifications` request in order to request more
/// events. If this is absent, there are no more results.
QString nextToken() const { return loadFromJson<QString>("next_token"_ls); }
diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp
index 3941e9c0..5c93a2d7 100644
--- a/lib/csapi/openid.cpp
+++ b/lib/csapi/openid.cpp
@@ -4,15 +4,13 @@
#include "openid.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId,
const QJsonObject& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/openid/request_token")
+ makePath("/_matrix/client/r0", "/user/", userId,
+ "/openid/request_token"))
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h
index efb5f623..0be39c8c 100644
--- a/lib/csapi/openid.h
+++ b/lib/csapi/openid.h
@@ -18,7 +18,7 @@ namespace Quotient {
* OpenID.
*
* The access token generated is only valid for the OpenID API. It cannot
- * be used to request another OpenID access token or call ``/sync``, for
+ * be used to request another OpenID access token or call `/sync`, for
* example.
*/
class RequestOpenIdTokenJob : public BaseJob {
@@ -38,9 +38,10 @@ public:
// Result properties
/// OpenID token information. This response is nearly compatible with the
- /// response documented in the `OpenID Connect 1.0 Specification
- /// <http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse>`_
- /// with the only difference being the lack of an ``id_token``. Instead,
+ /// response documented in the
+ /// [OpenID Connect 1.0
+ /// Specification](http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse)
+ /// with the only difference being the lack of an `id_token`. Instead,
/// the Matrix homeserver's name is provided.
OpenidToken tokenData() const { return fromJson<OpenidToken>(jsonData()); }
};
diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp
index 70a5b6f3..eb5d22fa 100644
--- a/lib/csapi/peeking_events.cpp
+++ b/lib/csapi/peeking_events.cpp
@@ -4,14 +4,12 @@
#include "peeking_events.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToPeekEvents(const QString& from, Omittable<int> timeout,
const QString& roomId)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
addParam<IfNotEmpty>(_q, QStringLiteral("timeout"), timeout);
addParam<IfNotEmpty>(_q, QStringLiteral("room_id"), roomId);
@@ -22,14 +20,13 @@ QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from,
Omittable<int> timeout, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/events",
+ makePath("/_matrix/client/r0", "/events"),
queryToPeekEvents(from, timeout, roomId));
}
PeekEventsJob::PeekEventsJob(const QString& from, Omittable<int> timeout,
const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("PeekEventsJob"),
- QStringLiteral("/_matrix/client/r0") % "/events",
+ makePath("/_matrix/client/r0", "/events"),
queryToPeekEvents(from, timeout, roomId))
{}
diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h
index cecd9f2d..885ff340 100644
--- a/lib/csapi/peeking_events.h
+++ b/lib/csapi/peeking_events.h
@@ -13,12 +13,12 @@ namespace Quotient {
*
* This will listen for new events related to a particular room and return
* them to the caller. This will block until an event is received, or until
- * the ``timeout`` is reached.
+ * the `timeout` is reached.
*
- * This API is the same as the normal ``/events`` endpoint, but can be
+ * This API is the same as the normal `/events` endpoint, but can be
* called by users who have not joined the room.
*
- * Note that the normal ``/events`` endpoint has been deprecated. This
+ * Note that the normal `/events` endpoint has been deprecated. This
* API will also be deprecated at some point, but its replacement is not
* yet known.
*/
@@ -51,12 +51,12 @@ public:
// Result properties
- /// A token which correlates to the first value in ``chunk``. This
- /// is usually the same token supplied to ``from=``.
+ /// A token which correlates to the first value in `chunk`. This
+ /// is usually the same token supplied to `from=`.
QString begin() const { return loadFromJson<QString>("start"_ls); }
- /// A token which correlates to the last value in ``chunk``. This
- /// token should be used in the next request to ``/events``.
+ /// A token which correlates to the last value in `chunk`. This
+ /// token should be used in the next request to `/events`.
QString end() const { return loadFromJson<QString>("end"_ls); }
/// An array of events.
diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp
index 58d0d157..4f77c466 100644
--- a/lib/csapi/presence.cpp
+++ b/lib/csapi/presence.cpp
@@ -4,15 +4,12 @@
#include "presence.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence,
const QString& statusMsg)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"),
- QStringLiteral("/_matrix/client/r0") % "/presence/" % userId
- % "/status")
+ makePath("/_matrix/client/r0", "/presence/", userId, "/status"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("presence"), presence);
@@ -23,14 +20,13 @@ SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence,
QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/presence/" % userId % "/status");
+ makePath("/_matrix/client/r0", "/presence/",
+ userId, "/status"));
}
GetPresenceJob::GetPresenceJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"),
- QStringLiteral("/_matrix/client/r0") % "/presence/" % userId
- % "/status")
+ makePath("/_matrix/client/r0", "/presence/", userId, "/status"))
{
addExpectedKey("presence");
}
diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h
index a885bf4f..4ab50e25 100644
--- a/lib/csapi/presence.h
+++ b/lib/csapi/presence.h
@@ -12,7 +12,7 @@ namespace Quotient {
*
* This API sets the given user's presence state. When setting the status,
* the activity time is updated to reflect that activity; the client does
- * not need to specify the ``last_active_ago`` field. You cannot set the
+ * not need to specify the `last_active_ago` field. You cannot set the
* presence state of another user.
*/
class SetPresenceJob : public BaseJob {
diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp
index 8436b8e6..64ac84ca 100644
--- a/lib/csapi/profile.cpp
+++ b/lib/csapi/profile.cpp
@@ -4,15 +4,12 @@
#include "profile.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetDisplayNameJob::SetDisplayNameJob(const QString& userId,
const QString& displayname)
: BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/displayname")
+ makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("displayname"), displayname);
@@ -22,21 +19,19 @@ SetDisplayNameJob::SetDisplayNameJob(const QString& userId,
QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/profile/" % userId % "/displayname");
+ makePath("/_matrix/client/r0", "/profile/",
+ userId, "/displayname"));
}
GetDisplayNameJob::GetDisplayNameJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/displayname",
+ makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"),
false)
{}
-SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl)
+SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/avatar_url")
+ makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl);
@@ -46,25 +41,24 @@ SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl
QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/profile/" % userId % "/avatar_url");
+ makePath("/_matrix/client/r0", "/profile/",
+ userId, "/avatar_url"));
}
GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/avatar_url",
+ makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url"),
false)
{}
QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/profile/" % userId);
+ makePath("/_matrix/client/r0", "/profile/",
+ userId));
}
GetUserProfileJob::GetUserProfileJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId, false)
+ makePath("/_matrix/client/r0", "/profile/", userId), false)
{}
diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h
index 3858fab2..7f9c9e95 100644
--- a/lib/csapi/profile.h
+++ b/lib/csapi/profile.h
@@ -11,7 +11,7 @@ namespace Quotient {
/*! \brief Set the user's display name.
*
* 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``.
+ * set this user's display name, e.g. you need to have their `access_token`.
*/
class SetDisplayNameJob : public BaseJob {
public:
@@ -61,7 +61,7 @@ public:
/*! \brief Set the user's avatar URL.
*
* 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``.
+ * set this user's avatar URL, e.g. you need to have their `access_token`.
*/
class SetAvatarUrlJob : public BaseJob {
public:
@@ -73,7 +73,7 @@ public:
* \param avatarUrl
* The new avatar URL for this user.
*/
- explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl);
+ explicit SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl);
};
/*! \brief Get the user's avatar URL.
@@ -101,7 +101,7 @@ public:
// Result properties
/// The user's avatar URL if they have set one, otherwise not present.
- QString avatarUrl() const { return loadFromJson<QString>("avatar_url"_ls); }
+ QUrl avatarUrl() const { return loadFromJson<QUrl>("avatar_url"_ls); }
};
/*! \brief Get this user's profile information.
@@ -109,7 +109,7 @@ public:
* Get the combined profile information for this user. This API may be used
* to fetch the user's own profile information or other users; either
* locally or on remote homeservers. This API may return keys which are not
- * limited to ``displayname`` or ``avatar_url``.
+ * limited to `displayname` or `avatar_url`.
*/
class GetUserProfileJob : public BaseJob {
public:
@@ -130,7 +130,7 @@ public:
// Result properties
/// The user's avatar URL if they have set one, otherwise not present.
- QString avatarUrl() const { return loadFromJson<QString>("avatar_url"_ls); }
+ QUrl avatarUrl() const { return loadFromJson<QUrl>("avatar_url"_ls); }
/// The user's display name if they have set one, otherwise not present.
QString displayname() const
diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp
index 028022c5..ef4b3767 100644
--- a/lib/csapi/pusher.cpp
+++ b/lib/csapi/pusher.cpp
@@ -4,20 +4,17 @@
#include "pusher.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushers");
+ makePath("/_matrix/client/r0", "/pushers"));
}
GetPushersJob::GetPushersJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushers")
+ makePath("/_matrix/client/r0", "/pushers"))
{}
PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind,
@@ -26,7 +23,7 @@ PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind,
const QString& lang, const PusherData& data,
const QString& profileTag, Omittable<bool> append)
: BaseJob(HttpVerb::Post, QStringLiteral("PostPusherJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushers/set")
+ makePath("/_matrix/client/r0", "/pushers/set"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("pushkey"), pushkey);
diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h
index ae0050d2..622b0df6 100644
--- a/lib/csapi/pusher.h
+++ b/lib/csapi/pusher.h
@@ -19,9 +19,9 @@ public:
/// A dictionary of information for the pusher implementation
/// itself.
struct PusherData {
- /// Required if ``kind`` is ``http``. The URL to use to send
+ /// Required if `kind` is `http`. The URL to use to send
/// notifications to.
- QString url;
+ QUrl url;
/// The format to use when sending notifications to the Push
/// Gateway.
QString format;
@@ -29,11 +29,11 @@ public:
/// Gets all currently active pushers for the authenticated user.
struct Pusher {
- /// This is a unique identifier for this pusher. See ``/set`` for
+ /// This is a unique identifier for this pusher. See `/set` for
/// more detail.
/// Max length, 512 bytes.
QString pushkey;
- /// The kind of pusher. ``"http"`` is a pusher that
+ /// The kind of pusher. `"http"` is a pusher that
/// sends HTTP pokes.
QString kind;
/// This is a reverse-DNS style identifier for the application.
@@ -104,27 +104,27 @@ struct JsonObjectConverter<GetPushersJob::Pusher> {
/*! \brief Modify a pusher for this user on the homeserver.
*
- * This endpoint allows the creation, modification and deletion of `pushers`_
- * for this user ID. The behaviour of this endpoint varies depending on the
- * values in the JSON body.
+ * This endpoint allows the creation, modification and deletion of
+ * [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 {
public:
// Inner data structures
/// A dictionary of information for the pusher implementation
- /// itself. If ``kind`` is ``http``, this should contain ``url``
+ /// itself. If `kind` is `http`, this should contain `url`
/// which is the URL to use to send notifications to.
struct PusherData {
- /// Required if ``kind`` is ``http``. The URL to use to send
+ /// Required if `kind` is `http`. The URL to use to send
/// notifications to. MUST be an HTTPS URL with a path of
- /// ``/_matrix/push/v1/notify``.
- QString url;
+ /// `/_matrix/push/v1/notify`.
+ QUrl url;
/// The format to send notifications in to Push Gateways if the
- /// ``kind`` is ``http``. The details about what fields the
+ /// `kind` is `http`. The details about what fields the
/// homeserver should send to the push gateway are defined in the
- /// `Push Gateway Specification`_. Currently the only format
- /// available is 'event_id_only'.
+ /// [Push Gateway Specification](/push-gateway-api/). Currently the only
+ /// format available is 'event_id_only'.
QString format;
};
@@ -140,13 +140,13 @@ public:
* client has no such concept, use any unique identifier.
* Max length, 512 bytes.
*
- * If the ``kind`` is ``"email"``, this is the email address to
+ * If the `kind` is `"email"`, this is the email address to
* send notifications to.
*
* \param kind
- * The kind of pusher to configure. ``"http"`` makes a pusher that
- * sends HTTP pokes. ``"email"`` makes a pusher that emails the
- * user with unread notifications. ``null`` deletes the pusher.
+ * The kind of pusher to configure. `"http"` makes a pusher that
+ * sends HTTP pokes. `"email"` makes a pusher that emails the
+ * user with unread notifications. `null` deletes the pusher.
*
* \param appId
* This is a reverse-DNS style identifier for the application.
@@ -154,7 +154,7 @@ public:
* different platform versions get different app identifiers.
* Max length, 64 chars.
*
- * If the ``kind`` is ``"email"``, this is ``"m.email"``.
+ * If the `kind` is `"email"`, this is `"m.email"`.
*
* \param appDisplayName
* A string that will allow the user to identify what application
@@ -170,7 +170,7 @@ public:
*
* \param data
* A dictionary of information for the pusher implementation
- * itself. If ``kind`` is ``http``, this should contain ``url``
+ * itself. If `kind` is `http`, this should contain `url`
* which is the URL to use to send notifications to.
*
* \param profileTag
@@ -182,7 +182,7 @@ public:
* given pushkey and App ID in addition to any others with
* different user IDs. Otherwise, the homeserver must remove any
* other pushers with the same App ID and pushkey for different
- * users. The default is ``false``.
+ * users. The default is `false`.
*/
explicit PostPusherJob(const QString& pushkey, const QString& kind,
const QString& appId, const QString& appDisplayName,
diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp
index 86165744..0d840788 100644
--- a/lib/csapi/pushrules.cpp
+++ b/lib/csapi/pushrules.cpp
@@ -4,20 +4,17 @@
#include "pushrules.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/pushrules"));
}
GetPushRulesJob::GetPushRulesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules")
+ makePath("/_matrix/client/r0", "/pushrules"))
{
addExpectedKey("global");
}
@@ -26,16 +23,15 @@ QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& kind, const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules/" % scope % "/" % kind
- % "/" % ruleId);
+ makePath("/_matrix/client/r0", "/pushrules/",
+ scope, "/", kind, "/", ruleId));
}
GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId)
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId))
{}
QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
@@ -43,21 +39,20 @@ QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules/" % scope % "/" % kind
- % "/" % ruleId);
+ makePath("/_matrix/client/r0", "/pushrules/",
+ scope, "/", kind, "/", ruleId));
}
DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId)
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId))
{}
auto queryToSetPushRule(const QString& before, const QString& after)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("before"), before);
addParam<IfNotEmpty>(_q, QStringLiteral("after"), after);
return _q;
@@ -70,8 +65,8 @@ SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind,
const QVector<PushCondition>& conditions,
const QString& pattern)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId,
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId),
queryToSetPushRule(before, after))
{
QJsonObject _data;
@@ -86,17 +81,17 @@ QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules/" % scope % "/" % kind
- % "/" % ruleId % "/enabled");
+ makePath("/_matrix/client/r0", "/pushrules/",
+ scope, "/", kind, "/", ruleId,
+ "/enabled"));
}
IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope,
const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("IsPushRuleEnabledJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId % "/enabled")
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/enabled"))
{
addExpectedKey("enabled");
}
@@ -105,8 +100,8 @@ SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope,
const QString& kind,
const QString& ruleId, bool enabled)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleEnabledJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId % "/enabled")
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/enabled"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("enabled"), enabled);
@@ -118,17 +113,17 @@ QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules/" % scope % "/" % kind
- % "/" % ruleId % "/actions");
+ makePath("/_matrix/client/r0", "/pushrules/",
+ scope, "/", kind, "/", ruleId,
+ "/actions"));
}
GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope,
const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleActionsJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId % "/actions")
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/actions"))
{
addExpectedKey("actions");
}
@@ -138,8 +133,8 @@ SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope,
const QString& ruleId,
const QVector<QVariant>& actions)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId % "/actions")
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/actions"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("actions"), actions);
diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h
index 1c6d5c2d..a5eb48f0 100644
--- a/lib/csapi/pushrules.h
+++ b/lib/csapi/pushrules.h
@@ -15,9 +15,9 @@ namespace Quotient {
/*! \brief Retrieve all push rulesets.
*
* Retrieve all push rulesets for this user. Clients can "drill-down" on
- * the rulesets by suffixing a ``scope`` to this path e.g.
- * ``/pushrules/global/``. This will return a subset of this data under the
- * specified key e.g. the ``global`` key.
+ * the rulesets by suffixing a `scope` to this path e.g.
+ * `/pushrules/global/`. This will return a subset of this data under the
+ * specified key e.g. the `global` key.
*/
class GetPushRulesJob : public BaseJob {
public:
@@ -49,7 +49,7 @@ public:
/*! \brief Retrieve a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
@@ -71,7 +71,7 @@ public:
// Result properties
/// The specific push rule. This will also include keys specific to the
- /// rule itself such as the rule's ``actions`` and ``conditions`` if set.
+ /// rule itself such as the rule's `actions` and `conditions` if set.
PushRule pushRule() const { return fromJson<PushRule>(jsonData()); }
};
@@ -84,7 +84,7 @@ public:
/*! \brief Delete a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
@@ -117,7 +117,7 @@ public:
/*! \brief Add or change a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
@@ -129,7 +129,7 @@ public:
* The action(s) to perform when the conditions for this rule are met.
*
* \param before
- * Use 'before' with a ``rule_id`` as its value to make the new rule the
+ * Use 'before' with a `rule_id` as its value to make the new rule the
* next-most important rule with respect to the given user defined rule.
* It is not possible to add a rule relative to a predefined server rule.
*
@@ -141,10 +141,10 @@ public:
* \param conditions
* The conditions that must hold true for an event in order for a
* rule to be applied to an event. A rule with no conditions
- * always matches. Only applicable to ``underride`` and ``override`` rules.
+ * always matches. Only applicable to `underride` and `override` rules.
*
* \param pattern
- * Only applicable to ``content`` rules. The glob-style pattern to match
+ * Only applicable to `content` rules. The glob-style pattern to match
* against.
*/
explicit SetPushRuleJob(const QString& scope, const QString& kind,
@@ -165,8 +165,8 @@ public:
/*! \brief Get whether a push rule is enabled
*
* \param scope
- * Either ``global`` or ``device/<profile_tag>`` to specify global
- * rules or device rules for the given ``profile_tag``.
+ * Either `global` or `device/<profile_tag>` to specify global
+ * rules or device rules for the given `profile_tag`.
*
* \param kind
* The kind of rule
@@ -200,7 +200,7 @@ public:
/*! \brief Enable or disable a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
@@ -224,8 +224,8 @@ public:
/*! \brief The actions for a push rule
*
* \param scope
- * Either ``global`` or ``device/<profile_tag>`` to specify global
- * rules or device rules for the given ``profile_tag``.
+ * Either `global` or `device/<profile_tag>` to specify global
+ * rules or device rules for the given `profile_tag`.
*
* \param kind
* The kind of rule
@@ -263,7 +263,7 @@ public:
/*! \brief Set the actions for a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp
index 39e4d148..f2edb71e 100644
--- a/lib/csapi/read_markers.cpp
+++ b/lib/csapi/read_markers.cpp
@@ -4,16 +4,13 @@
#include "read_markers.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetReadMarkerJob::SetReadMarkerJob(const QString& roomId,
const QString& mFullyRead,
const QString& mRead)
: BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/read_markers")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/read_markers"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead);
diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h
index 0e122c63..00a2aa0d 100644
--- a/lib/csapi/read_markers.h
+++ b/lib/csapi/read_markers.h
@@ -26,7 +26,7 @@ public:
*
* \param mRead
* The event ID to set the read receipt location at. This is
- * equivalent to calling ``/receipt/m.read/$elsewhere:example.org``
+ * equivalent to calling `/receipt/m.read/$elsewhere:example.org`
* and is provided here to save that extra call.
*/
explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead,
diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp
index 00d1c28a..401c3bfe 100644
--- a/lib/csapi/receipts.cpp
+++ b/lib/csapi/receipts.cpp
@@ -4,16 +4,14 @@
#include "receipts.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType,
const QString& eventId,
const QJsonObject& receipt)
: BaseJob(HttpVerb::Post, QStringLiteral("PostReceiptJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/receipt/" % receiptType % "/" % eventId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/receipt/",
+ receiptType, "/", eventId))
{
- setRequestData(Data(toJson(receipt)));
+ setRequestData(RequestData(toJson(receipt)));
}
diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h
index 1fac0acf..7ac093cd 100644
--- a/lib/csapi/receipts.h
+++ b/lib/csapi/receipts.h
@@ -27,8 +27,8 @@ public:
* The event ID to acknowledge up to.
*
* \param receipt
- * Extra receipt information to attach to ``content`` if any. The
- * server will automatically set the ``ts`` field.
+ * Extra receipt information to attach to `content` if any. The
+ * server will automatically set the `ts` field.
*/
explicit PostReceiptJob(const QString& roomId, const QString& receiptType,
const QString& eventId,
diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp
index 91497064..acf1b0e4 100644
--- a/lib/csapi/redaction.cpp
+++ b/lib/csapi/redaction.cpp
@@ -4,15 +4,13 @@
#include "redaction.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId,
const QString& txnId, const QString& reason)
: BaseJob(HttpVerb::Put, QStringLiteral("RedactEventJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/redact/" % eventId % "/" % txnId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/redact/",
+ eventId, "/", txnId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h
index 541e433a..f0db9f9f 100644
--- a/lib/csapi/redaction.h
+++ b/lib/csapi/redaction.h
@@ -15,9 +15,12 @@ namespace Quotient {
*
* This cannot be undone.
*
- * Users may redact their own events, and any user with a power level
- * greater than or equal to the ``redact`` power level of the room may
- * redact events there.
+ * Any user with a power level greater than or equal to the `m.room.redaction`
+ * event power level may send redaction events in the room. If the user's power
+ * level greater is also greater than or equal to the `redact` power level
+ * of the room, the user may redact events sent by other users.
+ *
+ * Server administrators may redact events sent by users on their server.
*/
class RedactEventJob : public BaseJob {
public:
diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp
index 33f61265..153abcee 100644
--- a/lib/csapi/registration.cpp
+++ b/lib/csapi/registration.cpp
@@ -4,13 +4,11 @@
#include "registration.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToRegister(const QString& kind)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("kind"), kind);
return _q;
}
@@ -22,7 +20,7 @@ RegisterJob::RegisterJob(const QString& kind,
const QString& initialDeviceDisplayName,
Omittable<bool> inhibitLogin)
: BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"),
- QStringLiteral("/_matrix/client/r0") % "/register",
+ makePath("/_matrix/client/r0", "/register"),
queryToRegister(kind), {}, false)
{
QJsonObject _data;
@@ -40,28 +38,26 @@ RegisterJob::RegisterJob(const QString& kind,
RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/register/email/requestToken",
+ makePath("/_matrix/client/r0", "/register/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/register/msisdn/requestToken",
+ makePath("/_matrix/client/r0", "/register/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
ChangePasswordJob::ChangePasswordJob(const QString& newPassword,
bool logoutDevices,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/password")
+ makePath("/_matrix/client/r0", "/account/password"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
@@ -74,28 +70,28 @@ RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post,
QStringLiteral("RequestTokenToResetPasswordEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/password/email/requestToken",
+ makePath("/_matrix/client/r0",
+ "/account/password/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post,
QStringLiteral("RequestTokenToResetPasswordMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/password/msisdn/requestToken",
+ makePath("/_matrix/client/r0",
+ "/account/password/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
DeactivateAccountJob::DeactivateAccountJob(
const Omittable<AuthenticationData>& auth, const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/deactivate")
+ makePath("/_matrix/client/r0", "/account/deactivate"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
@@ -106,7 +102,7 @@ DeactivateAccountJob::DeactivateAccountJob(
auto queryToCheckUsernameAvailability(const QString& username)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("username"), username);
return _q;
}
@@ -115,13 +111,13 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl,
const QString& username)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/register/available",
+ makePath("/_matrix/client/r0",
+ "/register/available"),
queryToCheckUsernameAvailability(username));
}
CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username)
: BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"),
- QStringLiteral("/_matrix/client/r0") % "/register/available",
+ makePath("/_matrix/client/r0", "/register/available"),
queryToCheckUsernameAvailability(username), {}, false)
{}
diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h
index 6864fd47..c1614f20 100644
--- a/lib/csapi/registration.h
+++ b/lib/csapi/registration.h
@@ -15,8 +15,9 @@ namespace Quotient {
/*! \brief Register for an account on this homeserver.
*
- * This API endpoint uses the `User-Interactive Authentication API`_, except in
- * the cases where a guest account is being registered.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api), except in the
+ * cases where a guest account is being registered.
*
* Register for an account on this homeserver.
*
@@ -31,45 +32,46 @@ namespace Quotient {
* If registration is successful, this endpoint will issue an access token
* the client can use to authorize itself in subsequent requests.
*
- * If the client does not supply a ``device_id``, the server must
+ * If the client does not supply a `device_id`, the server must
* auto-generate one.
*
* The server SHOULD register an account with a User ID based on the
- * ``username`` provided, if any. Note that the grammar of Matrix User ID
+ * `username` provided, if any. Note that the grammar of Matrix User ID
* localparts is restricted, so the server MUST either map the provided
- * ``username`` onto a ``user_id`` in a logical manner, or reject
- * ``username``\s which do not comply to the grammar, with
- * ``M_INVALID_USERNAME``.
+ * `username` onto a `user_id` in a logical manner, or reject
+ * `username`\s which do not comply to the grammar, with
+ * `M_INVALID_USERNAME`.
*
* Matrix clients MUST NOT assume that localpart of the registered
- * ``user_id`` matches the provided ``username``.
+ * `user_id` matches the provided `username`.
*
- * The returned access token must be associated with the ``device_id``
+ * The returned access token must be associated with the `device_id`
* supplied by the client or generated by the server. The server may
* invalidate any access token previously associated with that device. See
- * `Relationship between access tokens and devices`_.
+ * [Relationship between access tokens and
+ * devices](/client-server-api/#relationship-between-access-tokens-and-devices).
*
* When registering a guest account, all parameters in the request body
- * with the exception of ``initial_device_display_name`` MUST BE ignored
- * by the server. The server MUST pick a ``device_id`` for the account
+ * with the exception of `initial_device_display_name` MUST BE ignored
+ * by the server. The server MUST pick a `device_id` for the account
* regardless of input.
*
* Any user ID returned by this API must conform to the grammar given in the
- * `Matrix specification <../appendices.html#user-identifiers>`_.
+ * [Matrix specification](/appendices/#user-identifiers).
*/
class RegisterJob : public BaseJob {
public:
/*! \brief Register for an account on this homeserver.
*
* \param kind
- * The kind of account to register. Defaults to ``user``.
+ * The kind of account to register. Defaults to `user`.
*
* \param auth
* Additional authentication information for the
* user-interactive authentication API. Note that this
* information is *not* used to define how the registered user
* should be authenticated, but is instead used to
- * authenticate the ``register`` call itself.
+ * authenticate the `register` call itself.
*
* \param username
* The basis for the localpart of the desired Matrix ID. If omitted,
@@ -85,10 +87,10 @@ public:
*
* \param initialDeviceDisplayName
* A display name to assign to the newly-created device. Ignored
- * if ``device_id`` corresponds to a known device.
+ * if `device_id` corresponds to a known device.
*
* \param inhibitLogin
- * If true, an ``access_token`` and ``device_id`` should not be
+ * If true, an `access_token` and `device_id` should not be
* returned from this call, therefore preventing an automatic
* login. Defaults to false.
*/
@@ -105,12 +107,12 @@ public:
/// The fully-qualified Matrix user ID (MXID) that has been registered.
///
/// Any user ID returned by this API must conform to the grammar given in
- /// the `Matrix specification <../appendices.html#user-identifiers>`_.
+ /// the [Matrix specification](/appendices/#user-identifiers).
QString userId() const { return loadFromJson<QString>("user_id"_ls); }
/// An access token for the account.
/// This access token can then be used to authorize other requests.
- /// Required if the ``inhibit_login`` option is false.
+ /// Required if the `inhibit_login` option is false.
QString accessToken() const
{
return loadFromJson<QString>("access_token"_ls);
@@ -120,8 +122,8 @@ public:
/// been registered.
///
/// **Deprecated**. Clients should extract the server_name from
- /// ``user_id`` (by splitting at the first colon) if they require
- /// it. Note also that ``homeserver`` is not spelt this way.
+ /// `user_id` (by splitting at the first colon) if they require
+ /// it. Note also that `homeserver` is not spelt this way.
QString homeServer() const
{
return loadFromJson<QString>("home_server"_ls);
@@ -129,7 +131,7 @@ public:
/// ID of the registered device. Will be the same as the
/// corresponding parameter in the request, if one was specified.
- /// Required if the ``inhibit_login`` option is false.
+ /// Required if the `inhibit_login` option is false.
QString deviceId() const { return loadFromJson<QString>("device_id"_ls); }
};
@@ -201,9 +203,9 @@ public:
*
* Changes the password for an account on this homeserver.
*
- * This API endpoint uses the `User-Interactive Authentication API`_ to
- * ensure the user changing the password is actually the owner of the
- * account.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api) to ensure the
+ * user changing the password is actually the owner of the account.
*
* An access token should be submitted to this endpoint if the client has
* an active session.
@@ -224,8 +226,8 @@ public:
* Whether the user's other access tokens, and their associated devices,
* should be revoked if the request succeeds.
*
- * When ``false``, the server can still take advantage of `the soft logout
- * method <#soft-logout>`_ for the user's remaining devices.
+ * When `false`, the server can still take advantage of the [soft logout
+ * method](/client-server-api/#soft-logout) for the user's remaining devices.
*
* \param auth
* Additional authentication information for the user-interactive
@@ -242,23 +244,18 @@ public:
* The homeserver must check that the given email address **is
* associated** with an account on this homeserver. This API should be
* used to request validation tokens when authenticating for the
- * ``/account/password`` endpoint.
+ * `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * |/register/email/requestToken|_ endpoint, except that
- * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * endpoint, except that
+ * `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given email address could be found. The server may instead send an
* email to the given address prompting the user to create an account.
- * ``M_THREEPID_IN_USE`` may not be returned.
+ * `M_THREEPID_IN_USE` may not be returned.
*
* The homeserver should validate the email itself, either by sending a
* validation email itself or by using a service it has control over.
- *
- *
- * .. |/register/email/requestToken| replace:: ``/register/email/requestToken``
- *
- * .. _/register/email/requestToken:
- * #post-matrix-client-r0-register-email-requesttoken
*/
class RequestTokenToResetPasswordEmailJob : public BaseJob {
public:
@@ -269,24 +266,18 @@ public:
* The homeserver must check that the given email address **is
* associated** with an account on this homeserver. This API should be
* used to request validation tokens when authenticating for the
- * ``/account/password`` endpoint.
+ * `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * |/register/email/requestToken|_ endpoint, except that
- * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * endpoint, except that
+ * `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given email address could be found. The server may instead send an
* email to the given address prompting the user to create an account.
- * ``M_THREEPID_IN_USE`` may not be returned.
+ * `M_THREEPID_IN_USE` may not be returned.
*
* The homeserver should validate the email itself, either by sending a
* validation email itself or by using a service it has control over.
- *
- *
- * .. |/register/email/requestToken| replace::
- * ``/register/email/requestToken``
- *
- * .. _/register/email/requestToken:
- * #post-matrix-client-r0-register-email-requesttoken
*/
explicit RequestTokenToResetPasswordEmailJob(const EmailValidationData& body);
@@ -305,22 +296,18 @@ public:
* The homeserver must check that the given phone number **is
* associated** with an account on this homeserver. This API should be
* used to request validation tokens when authenticating for the
- * ``/account/password`` endpoint.
+ * `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * |/register/msisdn/requestToken|_ endpoint, except that
- * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * endpoint, except that
+ * `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given phone number could be found. The server may instead send the SMS
* to the given phone number prompting the user to create an account.
- * ``M_THREEPID_IN_USE`` may not be returned.
+ * `M_THREEPID_IN_USE` may not be returned.
*
* The homeserver should validate the phone number itself, either by sending a
* validation message itself or by using a service it has control over.
- *
- * .. |/register/msisdn/requestToken| replace:: ``/register/msisdn/requestToken``
- *
- * .. _/register/msisdn/requestToken:
- * #post-matrix-client-r0-register-email-requesttoken
*/
class RequestTokenToResetPasswordMSISDNJob : public BaseJob {
public:
@@ -331,23 +318,18 @@ public:
* The homeserver must check that the given phone number **is
* associated** with an account on this homeserver. This API should be
* used to request validation tokens when authenticating for the
- * ``/account/password`` endpoint.
+ * `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * |/register/msisdn/requestToken|_ endpoint, except that
- * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * endpoint, except that
+ * `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given phone number could be found. The server may instead send the SMS
* to the given phone number prompting the user to create an account.
- * ``M_THREEPID_IN_USE`` may not be returned.
+ * `M_THREEPID_IN_USE` may not be returned.
*
* The homeserver should validate the phone number itself, either by sending
* a validation message itself or by using a service it has control over.
- *
- * .. |/register/msisdn/requestToken| replace::
- * ``/register/msisdn/requestToken``
- *
- * .. _/register/msisdn/requestToken:
- * #post-matrix-client-r0-register-email-requesttoken
*/
explicit RequestTokenToResetPasswordMSISDNJob(
const MsisdnValidationData& body);
@@ -366,7 +348,8 @@ public:
* Deactivate the user's account, removing all ability for the user to
* login again.
*
- * This API endpoint uses the `User-Interactive Authentication API`_.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
*
* An access token should be submitted to this endpoint if the client has
* an active session.
@@ -374,7 +357,7 @@ public:
* The homeserver may change the flows available depending on whether a
* valid access token is provided.
*
- * Unlike other endpoints, this endpoint does not take an ``id_access_token``
+ * Unlike other endpoints, this endpoint does not take an `id_access_token`
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
@@ -388,11 +371,11 @@ public:
*
* \param idServer
* The identity server to unbind all of the user's 3PIDs from.
- * If not provided, the homeserver MUST use the ``id_server``
+ * If not provided, the homeserver MUST use the `id_server`
* that was originally use to bind each identifier. If the
- * homeserver does not know which ``id_server`` that was,
- * it must return an ``id_server_unbind_result`` of
- * ``no-support``.
+ * homeserver does not know which `id_server` that was,
+ * it must return an `id_server_unbind_result` of
+ * `no-support`.
*/
explicit DeactivateAccountJob(const Omittable<AuthenticationData>& auth = none,
const QString& idServer = {});
@@ -400,12 +383,12 @@ public:
// Result properties
/// An indicator as to whether or not the homeserver was able to unbind
- /// the user's 3PIDs from the identity server(s). ``success`` indicates
+ /// the user's 3PIDs from the identity server(s). `success` indicates
/// that all identifiers have been unbound from the identity server while
- /// ``no-support`` indicates that one or more identifiers failed to unbind
+ /// `no-support` indicates that one or more identifiers failed to unbind
/// due to the identity server refusing the request or the homeserver
/// being unable to determine an identity server to unbind from. This
- /// must be ``success`` if the homeserver has no identifiers to unbind
+ /// must be `success` if the homeserver has no identifiers to unbind
/// for the user.
QString idServerUnbindResult() const
{
@@ -447,7 +430,7 @@ public:
// Result properties
/// A flag to indicate that the username is available. This should always
- /// be ``true`` when the server replies with 200 OK.
+ /// be `true` when the server replies with 200 OK.
Omittable<bool> available() const
{
return loadFromJson<Omittable<bool>>("available"_ls);
diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp
index 0a41625f..0a76d5b8 100644
--- a/lib/csapi/report_content.cpp
+++ b/lib/csapi/report_content.cpp
@@ -4,18 +4,16 @@
#include "report_content.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId,
- int score, const QString& reason)
+ Omittable<int> score, const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/report/" % eventId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/report/",
+ eventId))
{
QJsonObject _data;
- addParam<>(_data, QStringLiteral("score"), score);
- addParam<>(_data, QStringLiteral("reason"), reason);
+ addParam<IfNotEmpty>(_data, QStringLiteral("score"), score);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
}
diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h
index 375e1829..e401c2e1 100644
--- a/lib/csapi/report_content.h
+++ b/lib/csapi/report_content.h
@@ -31,7 +31,8 @@ public:
* The reason the content is being reported. May be blank.
*/
explicit ReportContentJob(const QString& roomId, const QString& eventId,
- int score, const QString& reason);
+ Omittable<int> score = none,
+ const QString& reason = {});
};
} // namespace Quotient
diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp
index 63986c56..f80f9300 100644
--- a/lib/csapi/room_send.cpp
+++ b/lib/csapi/room_send.cpp
@@ -4,16 +4,14 @@
#include "room_send.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType,
const QString& txnId, const QJsonObject& body)
: BaseJob(HttpVerb::Put, QStringLiteral("SendMessageJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/send/" % eventType % "/" % txnId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/send/",
+ eventType, "/", txnId))
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
addExpectedKey("event_id");
}
diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h
index 39460aca..96f5beca 100644
--- a/lib/csapi/room_send.h
+++ b/lib/csapi/room_send.h
@@ -16,7 +16,7 @@ namespace Quotient {
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * `Room Events`_ for the m. event specification.
+ * [Room Events](/client-server-api/#room-events) for the m. event specification.
*/
class SendMessageJob : public BaseJob {
public:
@@ -40,7 +40,8 @@ public:
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * `Room Events`_ for the m. event specification.
+ * [Room Events](/client-server-api/#room-events) for the m. event
+ * specification.
*/
explicit SendMessageJob(const QString& roomId, const QString& eventType,
const QString& txnId, const QJsonObject& body = {});
diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp
index e18108ac..f6d2e6ec 100644
--- a/lib/csapi/room_state.cpp
+++ b/lib/csapi/room_state.cpp
@@ -4,8 +4,6 @@
#include "room_state.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId,
@@ -13,9 +11,9 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId,
const QString& stateKey,
const QJsonObject& body)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/state/" % eventType % "/" % stateKey)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/",
+ eventType, "/", stateKey))
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
addExpectedKey("event_id");
}
diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h
index 447605ff..f95af223 100644
--- a/lib/csapi/room_state.h
+++ b/lib/csapi/room_state.h
@@ -10,22 +10,20 @@ namespace Quotient {
/*! \brief Send a state event to the given room.
*
- * .. For backwards compatibility with older links...
- * .. _`put-matrix-client-r0-rooms-roomid-state-eventtype`:
- *
* State events can be sent using this endpoint. These events will be
- * overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all
+ * overwritten if `<room id>`, `<event type>` and `<state key>` all
* match.
*
* Requests to this endpoint **cannot use transaction IDs**
- * like other ``PUT`` paths because they cannot be differentiated from the
- * ``state_key``. Furthermore, ``POST`` is unsupported on state paths.
+ * like other `PUT` paths because they cannot be differentiated from the
+ * `state_key`. Furthermore, `POST` is unsupported on state paths.
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * `Room Events`_ for the ``m.`` event specification.
+ * [Room Events](/client-server-api/#room-events) for the `m.` event
+ * specification.
*
- * If the event type being sent is ``m.room.canonical_alias`` servers
+ * If the event type being sent is `m.room.canonical_alias` servers
* SHOULD ensure that any new aliases being listed in the event are valid
* per their grammar/syntax and that they point to the room ID where the
* state event is to be sent. Servers do not validate aliases which are
@@ -46,22 +44,20 @@ public:
* an empty string, the trailing slash on this endpoint is optional.
*
* \param body
- * .. For backwards compatibility with older links...
- * .. _`put-matrix-client-r0-rooms-roomid-state-eventtype`:
- *
* State events can be sent using this endpoint. These events will be
- * overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all
+ * overwritten if `<room id>`, `<event type>` and `<state key>` all
* match.
*
* Requests to this endpoint **cannot use transaction IDs**
- * like other ``PUT`` paths because they cannot be differentiated from the
- * ``state_key``. Furthermore, ``POST`` is unsupported on state paths.
+ * like other `PUT` paths because they cannot be differentiated from the
+ * `state_key`. Furthermore, `POST` is unsupported on state paths.
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * `Room Events`_ for the ``m.`` event specification.
+ * [Room Events](/client-server-api/#room-events) for the `m.` event
+ * specification.
*
- * If the event type being sent is ``m.room.canonical_alias`` servers
+ * If the event type being sent is `m.room.canonical_alias` servers
* SHOULD ensure that any new aliases being listed in the event are valid
* per their grammar/syntax and that they point to the room ID where the
* state event is to be sent. Servers do not validate aliases which are
diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp
index e3791b08..d4129cfb 100644
--- a/lib/csapi/room_upgrades.cpp
+++ b/lib/csapi/room_upgrades.cpp
@@ -4,14 +4,11 @@
#include "room_upgrades.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion)
: BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/upgrade")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/upgrade"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_version"), newVersion);
diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp
index 724d941f..5310aa32 100644
--- a/lib/csapi/rooms.cpp
+++ b/lib/csapi/rooms.cpp
@@ -4,24 +4,21 @@
#include "rooms.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& eventId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/event/"
- % eventId);
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/event/", eventId));
}
GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId,
const QString& eventId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/event/" % eventId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/event/",
+ eventId))
{}
QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
@@ -29,36 +26,35 @@ QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& stateKey)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/state/"
- % eventType % "/" % stateKey);
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/state/", eventType, "/",
+ stateKey));
}
GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId,
const QString& eventType,
const QString& stateKey)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateWithKeyJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/state/" % eventType % "/" % stateKey)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/",
+ eventType, "/", stateKey))
{}
QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/state");
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/state"));
}
GetRoomStateJob::GetRoomStateJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/state")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/state"))
{}
auto queryToGetMembersByRoom(const QString& at, const QString& membership,
const QString& notMembership)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("at"), at);
addParam<IfNotEmpty>(_q, QStringLiteral("membership"), membership);
addParam<IfNotEmpty>(_q, QStringLiteral("not_membership"), notMembership);
@@ -72,7 +68,7 @@ QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/members",
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"),
queryToGetMembersByRoom(at, membership, notMembership));
}
@@ -81,8 +77,7 @@ GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId,
const QString& membership,
const QString& notMembership)
: BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/members",
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"),
queryToGetMembersByRoom(at, membership, notMembership))
{}
@@ -90,12 +85,12 @@ QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/joined_members");
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/joined_members"));
}
GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/joined_members")
+ makePath("/_matrix/client/r0", "/rooms/", roomId,
+ "/joined_members"))
{}
diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h
index f0bfa349..2620582b 100644
--- a/lib/csapi/rooms.h
+++ b/lib/csapi/rooms.h
@@ -12,7 +12,7 @@ namespace Quotient {
/*! \brief Get a single event by event ID.
*
- * Get a single event based on ``roomId/eventId``. You must have permission to
+ * 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 {
@@ -43,9 +43,6 @@ public:
/*! \brief Get the state identified by the type and key.
*
- * .. For backwards compatibility with older links...
- * .. _`get-matrix-client-r0-rooms-roomid-state-eventtype`:
- *
* Looks up the contents of a state event in a room. If the user is
* joined to the room then the state is taken from the current
* state of the room. If the user has left the room then the state is
@@ -118,16 +115,15 @@ public:
*
* \param at
* The point in time (pagination token) to return members for in the room.
- * This token can be obtained from a ``prev_batch`` token returned for
+ * This token can be obtained from a `prev_batch` token returned for
* each room by the sync API. Defaults to the current state of the room,
* as determined by the server.
*
* \param membership
* The kind of membership to filter for. Defaults to no filtering if
- * unspecified. When specified alongside ``not_membership``, the two
+ * unspecified. When specified alongside `not_membership`, the two
* parameters create an 'or' condition: either the membership *is*
- * the same as ``membership`` **or** *is not* the same as
- * ``not_membership``.
+ * the same as `membership` **or** *is not* the same as `not_membership`.
*
* \param notMembership
* The kind of membership to exclude from the results. Defaults to no
@@ -162,7 +158,7 @@ public:
* room. The current user must be in the room for it to work, unless it is an
* Application Service in which case any of the AS's users must be in the room.
* This API is primarily for Application Services and should be faster to
- * respond than ``/members`` as it can be implemented more efficiently on the
+ * respond than `/members` as it can be implemented more efficiently on the
* server.
*/
class GetJoinedMembersByRoomJob : public BaseJob {
@@ -173,13 +169,13 @@ public:
/// the room. The current user must be in the room for it to work, unless it
/// is an Application Service in which case any of the AS's users must be in
/// the room. This API is primarily for Application Services and should be
- /// faster to respond than ``/members`` as it can be implemented more
+ /// faster to respond than `/members` as it can be implemented more
/// efficiently on the server.
struct RoomMember {
/// The display name of the user this object is representing.
QString displayName;
/// The mxc avatar url of the user this object is representing.
- QString avatarUrl;
+ QUrl avatarUrl;
};
// Construction/destruction
diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp
index 5649d52a..295dd1cc 100644
--- a/lib/csapi/search.cpp
+++ b/lib/csapi/search.cpp
@@ -4,13 +4,11 @@
#include "search.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToSearch(const QString& nextBatch)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("next_batch"), nextBatch);
return _q;
}
@@ -18,7 +16,7 @@ auto queryToSearch(const QString& nextBatch)
SearchJob::SearchJob(const Categories& searchCategories,
const QString& nextBatch)
: BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"),
- QStringLiteral("/_matrix/client/r0") % "/search",
+ makePath("/_matrix/client/r0", "/search"),
queryToSearch(nextBatch))
{
QJsonObject _data;
diff --git a/lib/csapi/search.h b/lib/csapi/search.h
index c009ded6..3d02752a 100644
--- a/lib/csapi/search.h
+++ b/lib/csapi/search.h
@@ -23,15 +23,15 @@ public:
/// returned are included in the response.
struct IncludeEventContext {
/// How many events before the result are
- /// returned. By default, this is ``5``.
+ /// returned. By default, this is `5`.
Omittable<int> beforeLimit;
/// How many events after the result are
- /// returned. By default, this is ``5``.
+ /// returned. By default, this is `5`.
Omittable<int> afterLimit;
/// Requests that the server returns the
/// historic profile information for the users
/// that sent the events that were returned.
- /// By default, this is ``false``.
+ /// By default, this is `false`.
Omittable<bool> includeProfile;
};
@@ -54,10 +54,10 @@ public:
QString searchTerm;
/// The keys to search. Defaults to all.
QStringList keys;
- /// This takes a `filter`_.
+ /// This takes a [filter](/client-server-api/#filtering).
RoomEventFilter filter;
/// The order in which to search for results.
- /// By default, this is ``"rank"``.
+ /// By default, this is `"rank"`.
QString orderBy;
/// Configures whether any context for the events
/// returned are included in the response.
@@ -81,7 +81,7 @@ public:
/// Performs a full text search across different categories.
QString displayname;
/// Performs a full text search across different categories.
- QString avatarUrl;
+ QUrl avatarUrl;
};
/// Context for result, if requested.
@@ -93,7 +93,7 @@ public:
/// The historic profile information of the
/// users that sent the events returned.
///
- /// The ``string`` key is the user ID for which
+ /// The `string` key is the user ID for which
/// the profile belongs to.
QHash<QString, UserProfile> profileInfo;
/// Events just before the result.
@@ -139,15 +139,15 @@ public:
std::vector<Result> results;
/// The current state for every room in the results.
/// This is included if the request had the
- /// ``include_state`` key set with a value of ``true``.
+ /// `include_state` key set with a value of `true`.
///
- /// The ``string`` key is the room ID for which the ``State
- /// Event`` array belongs to.
+ /// The `string` key is the room ID for which the `State
+ /// Event` array belongs to.
UnorderedMap<QString, StateEvents> state;
/// Any groups that were requested.
///
- /// The outer ``string`` key is the group key requested (eg: ``room_id``
- /// or ``sender``). The inner ``string`` key is the grouped value (eg:
+ /// The outer `string` key is the group key requested (eg: `room_id`
+ /// or `sender`). The inner `string` key is the grouped value (eg:
/// a room's ID or a user's ID).
QHash<QString, QHash<QString, GroupValue>> groups;
/// Token that can be used to get the next batch of
@@ -172,7 +172,7 @@ public:
*
* \param nextBatch
* The point to return events from. If given, this should be a
- * ``next_batch`` result from a previous call to this endpoint.
+ * `next_batch` result from a previous call to this endpoint.
*/
explicit SearchJob(const Categories& searchCategories,
const QString& nextBatch = {});
diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp
index 85a18560..871d6ff6 100644
--- a/lib/csapi/sso_login_redirect.cpp
+++ b/lib/csapi/sso_login_redirect.cpp
@@ -4,13 +4,11 @@
#include "sso_login_redirect.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToRedirectToSSO(const QString& redirectUrl)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl);
return _q;
}
@@ -18,13 +16,36 @@ auto queryToRedirectToSSO(const QString& redirectUrl)
QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/login/sso/redirect",
+ makePath("/_matrix/client/r0",
+ "/login/sso/redirect"),
queryToRedirectToSSO(redirectUrl));
}
RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl)
: BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"),
- QStringLiteral("/_matrix/client/r0") % "/login/sso/redirect",
+ makePath("/_matrix/client/r0", "/login/sso/redirect"),
queryToRedirectToSSO(redirectUrl), {}, false)
{}
+
+auto queryToRedirectToIdP(const QString& redirectUrl)
+{
+ QUrlQuery _q;
+ addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl);
+ return _q;
+}
+
+QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId,
+ const QString& redirectUrl)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ makePath("/_matrix/client/r0",
+ "/login/sso/redirect/", idpId),
+ queryToRedirectToIdP(redirectUrl));
+}
+
+RedirectToIdPJob::RedirectToIdPJob(const QString& idpId,
+ const QString& redirectUrl)
+ : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"),
+ makePath("/_matrix/client/r0", "/login/sso/redirect/", idpId),
+ queryToRedirectToIdP(redirectUrl), {}, false)
+{}
diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h
index d6330e38..ade1eb7d 100644
--- a/lib/csapi/sso_login_redirect.h
+++ b/lib/csapi/sso_login_redirect.h
@@ -13,7 +13,9 @@ namespace Quotient {
* A web-based Matrix client should instruct the user's browser to
* navigate to this endpoint in order to log in via SSO.
*
- * The server MUST respond with an HTTP redirect to the SSO interface.
+ * The server MUST respond with an HTTP redirect to the SSO interface,
+ * 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 {
public:
@@ -33,4 +35,36 @@ public:
static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl);
};
+/*! \brief Redirect the user's browser to the SSO interface for an IdP.
+ *
+ * This endpoint is the same as `/login/sso/redirect`, though with an
+ * IdP ID from the original `identity_providers` array to inform the
+ * server of which IdP the client/user would like to continue with.
+ *
+ * The server MUST respond with an HTTP redirect to the SSO interface
+ * for that IdP.
+ */
+class RedirectToIdPJob : public BaseJob {
+public:
+ /*! \brief Redirect the user's browser to the SSO interface for an IdP.
+ *
+ * \param idpId
+ * The `id` of the IdP from the `m.login.sso` `identity_providers`
+ * array denoting the user's selection.
+ *
+ * \param redirectUrl
+ * URI to which the user will be redirected after the homeserver has
+ * authenticated the user with SSO.
+ */
+ explicit RedirectToIdPJob(const QString& idpId, const QString& redirectUrl);
+
+ /*! \brief Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for RedirectToIdPJob
+ * is necessary but the job itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& idpId,
+ const QString& redirectUrl);
+};
+
} // namespace Quotient
diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp
index dc22dc18..f717de6e 100644
--- a/lib/csapi/tags.cpp
+++ b/lib/csapi/tags.cpp
@@ -4,30 +4,28 @@
#include "tags.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/user/"
- % userId % "/rooms/" % roomId % "/tags");
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/rooms/", roomId, "/tags"));
}
GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/tags")
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/tags"))
{}
SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId,
const QString& tag, Omittable<float> order,
const QVariantHash& additionalProperties)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomTagJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/tags/" % tag)
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/tags/", tag))
{
QJsonObject _data;
fillJson(_data, additionalProperties);
@@ -39,14 +37,14 @@ QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& roomId, const QString& tag)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/user/" % userId % "/rooms/" % roomId
- % "/tags/" % tag);
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/rooms/", roomId, "/tags/",
+ tag));
}
DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId,
const QString& tag)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomTagJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/tags/" % tag)
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/tags/", tag))
{}
diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h
index a815d9b3..a854531a 100644
--- a/lib/csapi/tags.h
+++ b/lib/csapi/tags.h
@@ -18,7 +18,7 @@ public:
/// List the tags set by a user on a room.
struct Tag {
- /// A number in a range ``[0,1]`` describing a relative
+ /// A number in a range `[0,1]` describing a relative
/// position of the room under the given tag.
Omittable<float> order;
/// List the tags set by a user on a room.
@@ -83,7 +83,7 @@ public:
* The tag to add.
*
* \param order
- * A number in a range ``[0,1]`` describing a relative
+ * A number in a range `[0,1]` describing a relative
* position of the room under the given tag.
*
* \param additionalProperties
diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp
index baf1fab5..4c930668 100644
--- a/lib/csapi/third_party_lookup.cpp
+++ b/lib/csapi/third_party_lookup.cpp
@@ -4,39 +4,36 @@
#include "third_party_lookup.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/protocols");
+ makePath("/_matrix/client/r0",
+ "/thirdparty/protocols"));
}
GetProtocolsJob::GetProtocolsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocols")
+ makePath("/_matrix/client/r0", "/thirdparty/protocols"))
{}
QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl,
const QString& protocol)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/protocol/" % protocol);
+ makePath("/_matrix/client/r0",
+ "/thirdparty/protocol/", protocol));
}
GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol)
: BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocol/"
- % protocol)
+ makePath("/_matrix/client/r0", "/thirdparty/protocol/", protocol))
{}
auto queryToQueryLocationByProtocol(const QString& searchFields)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("searchFields"), searchFields);
return _q;
}
@@ -46,22 +43,21 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl,
const QString& searchFields)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/location/" % protocol,
+ makePath("/_matrix/client/r0",
+ "/thirdparty/location/", protocol),
queryToQueryLocationByProtocol(searchFields));
}
QueryLocationByProtocolJob::QueryLocationByProtocolJob(
const QString& protocol, const QString& searchFields)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/location/"
- % protocol,
+ makePath("/_matrix/client/r0", "/thirdparty/location/", protocol),
queryToQueryLocationByProtocol(searchFields))
{}
auto queryToQueryUserByProtocol(const QString& fields)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("fields..."), fields);
return _q;
}
@@ -71,22 +67,21 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl,
const QString& fields)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/user/" % protocol,
+ makePath("/_matrix/client/r0",
+ "/thirdparty/user/", protocol),
queryToQueryUserByProtocol(fields));
}
QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol,
const QString& fields)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/user/"
- % protocol,
+ makePath("/_matrix/client/r0", "/thirdparty/user/", protocol),
queryToQueryUserByProtocol(fields))
{}
auto queryToQueryLocationByAlias(const QString& alias)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("alias"), alias);
return _q;
}
@@ -94,20 +89,20 @@ auto queryToQueryLocationByAlias(const QString& alias)
QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/location",
+ makePath("/_matrix/client/r0",
+ "/thirdparty/location"),
queryToQueryLocationByAlias(alias));
}
QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/location",
+ makePath("/_matrix/client/r0", "/thirdparty/location"),
queryToQueryLocationByAlias(alias))
{}
auto queryToQueryUserByID(const QString& userid)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("userid"), userid);
return _q;
}
@@ -115,13 +110,13 @@ auto queryToQueryUserByID(const QString& userid)
QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/user",
+ makePath("/_matrix/client/r0",
+ "/thirdparty/user"),
queryToQueryUserByID(userid));
}
QueryUserByIDJob::QueryUserByIDJob(const QString& userid)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/user",
+ makePath("/_matrix/client/r0", "/thirdparty/user"),
queryToQueryUserByID(userid))
{}
diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp
index fda772d2..59275e41 100644
--- a/lib/csapi/third_party_membership.cpp
+++ b/lib/csapi/third_party_membership.cpp
@@ -4,16 +4,13 @@
#include "third_party_membership.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer,
const QString& idAccessToken,
const QString& medium, const QString& address)
: BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/invite")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("id_server"), idServer);
diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h
index 55cab370..a424678f 100644
--- a/lib/csapi/third_party_membership.h
+++ b/lib/csapi/third_party_membership.h
@@ -10,14 +10,13 @@ namespace Quotient {
/*! \brief Invite a user to participate in a particular room.
*
- * .. _invite-by-third-party-id-endpoint:
- *
* *Note that there are two forms of this API, which are documented separately.
* This version of the API does not require that the inviter know the Matrix
* identifier of the invitee, and instead relies on third party identifiers.
* The homeserver uses an identity server to perform the mapping from
* third party identifier to a Matrix identifier. The other is documented in
- * the* `joining rooms section`_.
+ * the* [joining rooms
+ * section](/client-server-api/#post_matrixclientr0roomsroomidinvite).
*
* This API invites a user to participate in a particular room.
* They do not start participating in the room until they actually join the
@@ -27,7 +26,7 @@ namespace Quotient {
* join that room.
*
* If the identity server did know the Matrix user identifier for the
- * third party identifier, the homeserver will append a ``m.room.member``
+ * third party identifier, the homeserver will append a `m.room.member`
* event to the room.
*
* If the identity server does not know a Matrix user identifier for the
@@ -35,7 +34,7 @@ namespace Quotient {
* which can be accepted upon providing proof of ownership of the third
* party identifier. This is achieved by the identity server generating a
* token, which it gives to the inviting homeserver. The homeserver will
- * add an ``m.room.third_party_invite`` event into the graph for the room,
+ * add an `m.room.third_party_invite` event into the graph for the room,
* containing that token.
*
* When the invitee binds the invited third party identifier to a Matrix
@@ -51,9 +50,7 @@ namespace Quotient {
* - The matrix user ID who invited them to the room
*
* If a token is requested from the identity server, the homeserver will
- * append a ``m.room.third_party_invite`` event to the room.
- *
- * .. _joining rooms section: `invite-by-user-id-endpoint`_
+ * append a `m.room.third_party_invite` event to the room.
*/
class InviteBy3PIDJob : public BaseJob {
public:
@@ -73,7 +70,7 @@ public:
*
* \param medium
* The kind of address being passed in the address field, for example
- * ``email``.
+ * `email`.
*
* \param address
* The invitee's third party identifier.
diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp
index 28c4115a..628e8314 100644
--- a/lib/csapi/to_device.cpp
+++ b/lib/csapi/to_device.cpp
@@ -4,18 +4,16 @@
#include "to_device.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SendToDeviceJob::SendToDeviceJob(
const QString& eventType, const QString& txnId,
const QHash<QString, QHash<QString, QJsonObject>>& messages)
: BaseJob(HttpVerb::Put, QStringLiteral("SendToDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/sendToDevice/"
- % eventType % "/" % txnId)
+ makePath("/_matrix/client/r0", "/sendToDevice/", eventType, "/",
+ txnId))
{
QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("messages"), messages);
+ addParam<>(_data, QStringLiteral("messages"), messages);
setRequestData(std::move(_data));
}
diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h
index f5d69d65..7a237195 100644
--- a/lib/csapi/to_device.h
+++ b/lib/csapi/to_device.h
@@ -32,7 +32,7 @@ public:
*/
explicit SendToDeviceJob(
const QString& eventType, const QString& txnId,
- const QHash<QString, QHash<QString, QJsonObject>>& messages = {});
+ const QHash<QString, QHash<QString, QJsonObject>>& messages);
};
} // namespace Quotient
diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp
index 8e214053..c9673118 100644
--- a/lib/csapi/typing.cpp
+++ b/lib/csapi/typing.cpp
@@ -4,15 +4,13 @@
#include "typing.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId,
bool typing, Omittable<int> timeout)
: BaseJob(HttpVerb::Put, QStringLiteral("SetTypingJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/typing/" % userId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/typing/",
+ userId))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("typing"), typing);
diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h
index 2c953949..64a310d0 100644
--- a/lib/csapi/typing.h
+++ b/lib/csapi/typing.h
@@ -11,8 +11,8 @@ namespace Quotient {
/*! \brief Informs the server that the user has started or stopped typing.
*
* This tells the server that the user is typing for the next N
- * milliseconds where N is the value specified in the ``timeout`` key.
- * Alternatively, if ``typing`` is ``false``, it tells the server that the
+ * milliseconds where N is the value specified in the `timeout` key.
+ * Alternatively, if `typing` is `false`, it tells the server that the
* user has stopped typing.
*/
class SetTypingJob : public BaseJob {
@@ -26,7 +26,7 @@ public:
* The room in which the user is typing.
*
* \param typing
- * Whether the user is typing or not. If ``false``, the ``timeout``
+ * Whether the user is typing or not. If `false`, the `timeout`
* key can be omitted.
*
* \param timeout
diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp
index a0279d7e..48b727f0 100644
--- a/lib/csapi/users.cpp
+++ b/lib/csapi/users.cpp
@@ -4,14 +4,12 @@
#include "users.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm,
Omittable<int> limit)
: BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"),
- QStringLiteral("/_matrix/client/r0") % "/user_directory/search")
+ makePath("/_matrix/client/r0", "/user_directory/search"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("search_term"), searchTerm);
diff --git a/lib/csapi/users.h b/lib/csapi/users.h
index 6fc26f57..ec186592 100644
--- a/lib/csapi/users.h
+++ b/lib/csapi/users.h
@@ -19,7 +19,7 @@ namespace Quotient {
*
* The search is performed case-insensitively on user IDs and display
* names preferably using a collation determined based upon the
- * ``Accept-Language`` header provided in the request, if present.
+ * `Accept-Language` header provided in the request, if present.
*/
class SearchUserDirectoryJob : public BaseJob {
public:
@@ -34,14 +34,14 @@ public:
///
/// The search is performed case-insensitively on user IDs and display
/// names preferably using a collation determined based upon the
- /// ``Accept-Language`` header provided in the request, if present.
+ /// `Accept-Language` header provided in the request, if present.
struct User {
/// The user's matrix user ID.
QString userId;
/// The display name of the user, if one exists.
QString displayName;
/// The avatar url, as an MXC, if one exists.
- QString avatarUrl;
+ QUrl avatarUrl;
};
// Construction/destruction
diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp
index 9003e27f..a1efc33e 100644
--- a/lib/csapi/versions.cpp
+++ b/lib/csapi/versions.cpp
@@ -4,20 +4,17 @@
#include "versions.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client")
- % "/versions");
+ makePath("/_matrix/client", "/versions"));
}
GetVersionsJob::GetVersionsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetVersionsJob"),
- QStringLiteral("/_matrix/client") % "/versions", false)
+ makePath("/_matrix/client", "/versions"), false)
{
addExpectedKey("versions");
}
diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h
index 828a7eb9..896e2ea9 100644
--- a/lib/csapi/versions.h
+++ b/lib/csapi/versions.h
@@ -12,14 +12,14 @@ namespace Quotient {
*
* Gets the versions of the specification supported by the server.
*
- * Values will take the form ``rX.Y.Z``.
+ * Values will take the form `rX.Y.Z`.
*
- * Only the latest ``Z`` value will be reported for each supported ``X.Y``
- * value. i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``,
- * it will report ``r0.0.1`` and ``r1.2.0``.
+ * Only the latest `Z` value will be reported for each supported `X.Y` value.
+ * i.e. if the server implements `r0.0.0`, `r0.0.1`, and `r1.2.0`, it will
+ * report `r0.0.1` and `r1.2.0`.
*
* The server may additionally advertise experimental features it supports
- * through ``unstable_features``. These features should be namespaced and
+ * through `unstable_features`. These features should be namespaced and
* may optionally include version information within their name if desired.
* Features listed here are not for optionally toggling parts of the Matrix
* specification and should only be used to advertise support for a feature
diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp
index 43170057..c748ad94 100644
--- a/lib/csapi/voip.cpp
+++ b/lib/csapi/voip.cpp
@@ -4,18 +4,15 @@
#include "voip.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/voip/turnServer");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/voip/turnServer"));
}
GetTurnServerJob::GetTurnServerJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"),
- QStringLiteral("/_matrix/client/r0") % "/voip/turnServer")
+ makePath("/_matrix/client/r0", "/voip/turnServer"))
{}
diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp
index 1aa0a90b..0b441279 100644
--- a/lib/csapi/wellknown.cpp
+++ b/lib/csapi/wellknown.cpp
@@ -4,18 +4,15 @@
#include "wellknown.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/.well-known")
- % "/matrix/client");
+ makePath("/.well-known", "/matrix/client"));
}
GetWellknownJob::GetWellknownJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetWellknownJob"),
- QStringLiteral("/.well-known") % "/matrix/client", false)
+ makePath("/.well-known", "/matrix/client"), false)
{}
diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h
index b21d9fc7..c707d232 100644
--- a/lib/csapi/wellknown.h
+++ b/lib/csapi/wellknown.h
@@ -14,7 +14,7 @@ namespace Quotient {
*
* Gets discovery information about the domain. The file may include
* additional keys, which MUST follow the Java package naming convention,
- * e.g. ``com.example.myapp.property``. This ensures property names are
+ * e.g. `com.example.myapp.property`. This ensures property names are
* suitably namespaced for each application and reduces the risk of
* clashes.
*
diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp
index 73f0298e..ed8a9817 100644
--- a/lib/csapi/whoami.cpp
+++ b/lib/csapi/whoami.cpp
@@ -4,20 +4,17 @@
#include "whoami.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/account/whoami");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/account/whoami"));
}
GetTokenOwnerJob::GetTokenOwnerJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/whoami")
+ makePath("/_matrix/client/r0", "/account/whoami"))
{
addExpectedKey("user_id");
}
diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h
index af8f1e8a..319f82c5 100644
--- a/lib/csapi/whoami.h
+++ b/lib/csapi/whoami.h
@@ -14,8 +14,8 @@ namespace Quotient {
*
* Note that, as with the rest of the Client-Server API,
* Application Services may masquerade as users within their
- * namespace by giving a ``user_id`` query parameter. In this
- * situation, the server should verify that the given ``user_id``
+ * namespace by giving a `user_id` query parameter. In this
+ * situation, the server should verify that the given `user_id`
* is registered by the appservice, and return it in the response
* body.
*/
@@ -33,8 +33,14 @@ public:
// Result properties
- /// The user id that owns the access token.
+ /// The user ID that owns the access token.
QString userId() const { return loadFromJson<QString>("user_id"_ls); }
+
+ /// Device ID associated with the access token. If no device
+ /// is associated with the access token (such as in the case
+ /// of application services) then this field can be omitted.
+ /// Otherwise this is required.
+ QString deviceId() const { return loadFromJson<QString>("device_id"_ls); }
};
} // namespace Quotient
diff --git a/lib/eventitem.h b/lib/eventitem.h
index 1986ba77..0ab1a01d 100644
--- a/lib/eventitem.h
+++ b/lib/eventitem.h
@@ -9,11 +9,10 @@
#include <utility>
namespace Quotient {
-class StateEventBase;
-class EventStatus {
- Q_GADGET
-public:
+namespace EventStatus {
+ Q_NAMESPACE
+
/** Special marks an event can assume
*
* This is used to hint at a special status of some events in UI.
@@ -31,9 +30,8 @@ public:
Replaced = 0x10, //< The event has been replaced
Hidden = 0x100, //< The event should not be shown in the timeline
};
- Q_DECLARE_FLAGS(Status, Code)
- Q_FLAG(Status)
-};
+ Q_ENUM_NS(Code)
+} // namespace EventStatus
class EventItemBase {
public:
@@ -106,7 +104,6 @@ inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const
}
class PendingEventItem : public EventItemBase {
- Q_GADGET
public:
using EventItemBase::EventItemBase;
@@ -148,4 +145,3 @@ inline QDebug& operator<<(QDebug& d, const TimelineItem& ti)
return d;
}
} // namespace Quotient
-Q_DECLARE_METATYPE(Quotient::EventStatus)
diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h
index 8cea0ec8..9cf77be3 100644
--- a/lib/events/accountdataevents.h
+++ b/lib/events/accountdataevents.h
@@ -4,7 +4,6 @@
#pragma once
#include "event.h"
-#include "eventcontent.h"
namespace Quotient {
constexpr const char* FavouriteTag = "m.favourite";
@@ -16,12 +15,12 @@ struct TagRecord {
order_type order;
- TagRecord(order_type order = none) : order(std::move(order)) {}
+ TagRecord(order_type order = none) : order(order) {}
bool operator<(const TagRecord& other) const
{
// Per The Spec, rooms with no order should be after those with order,
- // against optional<>::operator<() convention.
+ // against std::optional<>::operator<() convention.
return order && (!other.order || *order < *other.order);
}
};
@@ -55,15 +54,15 @@ using TagsMap = QHash<QString, TagRecord>;
public: \
using content_type = _ContentType; \
DEFINE_EVENT_TYPEID(_TypeId, _Name) \
- explicit _Name(QJsonObject obj) : Event(typeId(), std::move(obj)) {} \
- explicit _Name(_ContentType content) \
+ explicit _Name(const QJsonObject& obj) : Event(typeId(), obj) {} \
+ explicit _Name(const content_type& content) \
: Event(typeId(), matrixTypeId(), \
- QJsonObject { { QStringLiteral(#_ContentKey), \
- toJson(std::move(content)) } }) \
+ QJsonObject { \
+ { QStringLiteral(#_ContentKey), toJson(content) } }) \
{} \
auto _ContentKey() const \
{ \
- return content<content_type>(#_ContentKey##_ls); \
+ return contentPart<content_type>(#_ContentKey##_ls); \
} \
}; \
REGISTER_EVENT_TYPE(_Name) \
diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h
index 6132cb44..4c01c941 100644
--- a/lib/events/callanswerevent.h
+++ b/lib/events/callanswerevent.h
@@ -19,11 +19,11 @@ public:
int lifetime() const
{
- return content<int>("lifetime"_ls);
+ return contentPart<int>("lifetime"_ls);
} // FIXME: Omittable<>?
QString sdp() const
{
- return contentJson()["answer"_ls].toObject().value("sdp"_ls).toString();
+ return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString();
}
};
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/callcandidatesevent.h b/lib/events/callcandidatesevent.h
index c2ccac3b..74c38f2c 100644
--- a/lib/events/callcandidatesevent.h
+++ b/lib/events/callcandidatesevent.h
@@ -25,17 +25,17 @@ public:
QJsonArray candidates() const
{
- return content<QJsonArray>("candidates"_ls);
+ return contentPart<QJsonArray>("candidates"_ls);
}
QString callId() const
{
- return content<QString>("call_id");
+ return contentPart<QString>("call_id");
}
int version() const
{
- return content<int>("version");
+ return contentPart<int>("version");
}
};
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..f3f82833 100644
--- a/lib/events/callhangupevent.h
+++ b/lib/events/callhangupevent.h
@@ -11,8 +11,12 @@ class 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 d3454c4f..80b7d651 100644
--- a/lib/events/callinviteevent.h
+++ b/lib/events/callinviteevent.h
@@ -18,11 +18,11 @@ public:
int lifetime() const
{
- return content<int>("lifetime"_ls);
+ return contentPart<int>("lifetime"_ls);
} // FIXME: Omittable<>?
QString sdp() const
{
- return contentJson()["offer"_ls].toObject().value("sdp"_ls).toString();
+ return contentPart<QJsonObject>("offer"_ls).value("sdp"_ls).toString();
}
};
diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h
index eb7123eb..de89a7c6 100644
--- a/lib/events/encryptedevent.h
+++ b/lib/events/encryptedevent.h
@@ -7,7 +7,6 @@
#include "roomevent.h"
namespace Quotient {
-class Room;
/*
* While the specification states:
*
@@ -27,7 +26,6 @@ class Room;
* one and doesn't add new restrictions, just provides additional features.
*/
class EncryptedEvent : public RoomEvent {
- Q_GADGET
public:
DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent)
@@ -43,7 +41,7 @@ public:
QString algorithm() const
{
- QString algo = content<QString>(AlgorithmKeyL);
+ QString algo = contentPart<QString>(AlgorithmKeyL);
if (!SupportedAlgorithms.contains(algo)) {
qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo
<< "is not supported";
@@ -52,17 +50,17 @@ public:
}
QByteArray ciphertext() const
{
- return content<QString>(CiphertextKeyL).toLatin1();
+ return contentPart<QString>(CiphertextKeyL).toLatin1();
}
QJsonObject ciphertext(const QString& identityKey) const
{
- return content<QJsonObject>(CiphertextKeyL).value(identityKey).toObject();
+ return contentPart<QJsonObject>(CiphertextKeyL).value(identityKey).toObject();
}
- QString senderKey() const { return content<QString>(SenderKeyKeyL); }
+ QString senderKey() const { return contentPart<QString>(SenderKeyKeyL); }
/* device_id and session_id are required with Megolm */
- QString deviceId() const { return content<QString>(DeviceIdKeyL); }
- QString sessionId() const { return content<QString>(SessionIdKeyL); }
+ QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); }
+ QString sessionId() const { return contentPart<QString>(SessionIdKeyL); }
};
REGISTER_EVENT_TYPE(EncryptedEvent)
diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h
new file mode 100644
index 00000000..24ac9de1
--- /dev/null
+++ b/lib/events/encryptedfile.h
@@ -0,0 +1,88 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPl-2.1-or-later
+
+#pragma once
+
+#include "converters.h"
+
+namespace Quotient {
+/**
+ * JSON Web Key object as specified in
+ * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes
+ * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec.
+ */
+struct JWK
+{
+ Q_GADGET
+ Q_PROPERTY(QString kty MEMBER kty CONSTANT)
+ Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT)
+ Q_PROPERTY(QString alg MEMBER alg CONSTANT)
+ Q_PROPERTY(QString k MEMBER k CONSTANT)
+ Q_PROPERTY(bool ext MEMBER ext CONSTANT)
+
+public:
+ QString kty;
+ QStringList keyOps;
+ QString alg;
+ QString k;
+ bool ext;
+};
+
+struct EncryptedFile
+{
+ Q_GADGET
+ Q_PROPERTY(QUrl url MEMBER url CONSTANT)
+ Q_PROPERTY(JWK key MEMBER key CONSTANT)
+ Q_PROPERTY(QString iv MEMBER iv CONSTANT)
+ Q_PROPERTY(QHash<QString, QString> hashes MEMBER hashes CONSTANT)
+ Q_PROPERTY(QString v MEMBER v CONSTANT)
+
+public:
+ QUrl url;
+ JWK key;
+ QString iv;
+ QHash<QString, QString> hashes;
+ QString v;
+};
+
+template <>
+struct JsonObjectConverter<EncryptedFile> {
+ static void dumpTo(QJsonObject& jo, const EncryptedFile& pod)
+ {
+ addParam<>(jo, QStringLiteral("url"), pod.url);
+ addParam<>(jo, QStringLiteral("key"), pod.key);
+ addParam<>(jo, QStringLiteral("iv"), pod.iv);
+ addParam<>(jo, QStringLiteral("hashes"), pod.hashes);
+ addParam<>(jo, QStringLiteral("v"), pod.v);
+ }
+ static void fillFrom(const QJsonObject& jo, EncryptedFile& pod)
+ {
+ fromJson(jo.value("url"_ls), pod.url);
+ fromJson(jo.value("key"_ls), pod.key);
+ fromJson(jo.value("iv"_ls), pod.iv);
+ fromJson(jo.value("hashes"_ls), pod.hashes);
+ fromJson(jo.value("v"_ls), pod.v);
+ }
+};
+
+template <>
+struct JsonObjectConverter<JWK> {
+ static void dumpTo(QJsonObject& jo, const JWK& pod)
+ {
+ addParam<>(jo, QStringLiteral("kty"), pod.kty);
+ addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps);
+ addParam<>(jo, QStringLiteral("alg"), pod.alg);
+ addParam<>(jo, QStringLiteral("k"), pod.k);
+ addParam<>(jo, QStringLiteral("ext"), pod.ext);
+ }
+ static void fillFrom(const QJsonObject& jo, JWK& pod)
+ {
+ fromJson(jo.value("kty"_ls), pod.kty);
+ fromJson(jo.value("key_ops"_ls), pod.keyOps);
+ fromJson(jo.value("alg"_ls), pod.alg);
+ fromJson(jo.value("k"_ls), pod.k);
+ fromJson(jo.value("ext"_ls), pod.ext);
+ }
+};
+} // namespace Quotient
diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp
index 490a5e8a..aa05a96e 100644
--- a/lib/events/encryptionevent.cpp
+++ b/lib/events/encryptionevent.cpp
@@ -39,6 +39,14 @@ EncryptionEventContent::EncryptionEventContent(const QJsonObject& json)
, rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100))
{}
+EncryptionEventContent::EncryptionEventContent(EncryptionType et)
+ : encryption(et)
+{
+ if(encryption != Undefined) {
+ algorithm = encryptionStrings[encryption];
+ }
+}
+
void EncryptionEventContent::fillJson(QJsonObject* o) const
{
Q_ASSERT(o);
diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h
index f9bbab12..14439fcc 100644
--- a/lib/events/encryptionevent.h
+++ b/lib/events/encryptionevent.h
@@ -12,9 +12,7 @@ class EncryptionEventContent : public EventContent::Base {
public:
enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined };
- explicit EncryptionEventContent(EncryptionType et = Undefined)
- : encryption(et)
- {}
+ explicit EncryptionEventContent(EncryptionType et = Undefined);
explicit EncryptionEventContent(const QJsonObject& json);
EncryptionType encryption;
@@ -40,6 +38,7 @@ public:
// default value
: StateEvent(typeId(), obj)
{}
+ EncryptionEvent(EncryptionEvent&&) = delete;
template <typename... ArgTs>
EncryptionEvent(ArgTs&&... contentArgs)
: StateEvent(typeId(), matrixTypeId(), QString(),
diff --git a/lib/events/event.cpp b/lib/events/event.cpp
index 3d66ab55..96be717c 100644
--- a/lib/events/event.cpp
+++ b/lib/events/event.cpp
@@ -46,14 +46,11 @@ QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); }
QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); }
-// On const below: this is to catch accidental attempts to change event JSON
-// NOLINTNEXTLINE(readability-const-return-type)
const QJsonObject Event::contentJson() const
{
return fullJson()[ContentKeyL].toObject();
}
-// NOLINTNEXTLINE(readability-const-return-type)
const QJsonObject Event::unsignedJson() const
{
return fullJson()[UnsignedKeyL].toObject();
diff --git a/lib/events/event.h b/lib/events/event.h
index f8f8311d..8f62872d 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,31 +29,30 @@ 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
-template <typename StrT>
-inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content)
+inline QJsonObject basicEventJson(const QString& matrixType,
+ const QJsonObject& content)
{
- return { { TypeKey, std::forward<StrT>(matrixType) },
- { ContentKey, content } };
+ return { { TypeKey, matrixType }, { ContentKey, content } };
}
// === Event types and event types registry ===
@@ -76,8 +76,7 @@ public:
private:
EventTypeRegistry() = default;
- Q_DISABLE_COPY(EventTypeRegistry)
- DISABLE_MOVE(EventTypeRegistry)
+ Q_DISABLE_COPY_MOVE(EventTypeRegistry)
static EventTypeRegistry& get()
{
@@ -111,97 +110,90 @@ inline event_type_t typeId()
inline event_type_t unknownEventTypeId() { return typeId<void>(); }
-// === 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)...);
}
-template <typename BaseEventT>
-class EventFactory {
-public:
- template <typename FnT>
- static auto addMethod(FnT&& method)
- {
- factories().emplace_back(std::forward<FnT>(method));
- return 0;
- }
-
- /** 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()
- {
- return addMethod(&EventT::factory_t::make);
- }
-
- static event_ptr_tt<BaseEventT> make(const QJsonObject& json,
- const QString& matrixType)
- {
- for (const auto& f : factories())
- if (auto e = f(json, matrixType))
- return e;
- return nullptr;
- }
-
-private:
- static auto& factories()
+namespace _impl {
+ template <class EventT, class BaseEventT>
+ event_ptr_tt<BaseEventT> makeIfMatches(const QJsonObject& json,
+ const QString& matrixType)
{
- using inner_factory_tt = std::function<event_ptr_tt<BaseEventT>(
- const QJsonObject&, const QString&)>;
- static std::vector<inner_factory_tt> _factories {};
- return _factories;
+ return QLatin1String(EventT::matrixTypeId()) == matrixType
+ ? makeEvent<EventT>(json)
+ : nullptr;
}
-};
-/** 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()
-{
- // 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
-}
+ //! \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
+ : private std::vector<event_ptr_tt<BaseEventT> (*)(const QJsonObject&,
+ const QString&)> {
+ // Actual makeIfMatches specialisations will differ in the first
+ // template parameter but that doesn't affect the function type
+ public:
+ explicit EventFactory(const char* name = "")
+ : name(name)
+ {
+ static auto yetToBeConstructed = true;
+ Q_ASSERT(yetToBeConstructed);
+ if (!yetToBeConstructed) // For Release builds that pass Q_ASSERT
+ qCritical(EVENTS)
+ << "Another EventFactory for the same base type is being "
+ "created - event creation logic will be splintered";
+ yetToBeConstructed = false;
+ }
+ EventFactory(const EventFactory&) = delete;
+
+ //! \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 makeIfMatches, loadEvent, Quotient::loadEvent
+ template <class EventT>
+ bool addMethod()
+ {
+ this->emplace_back(&makeIfMatches<EventT, BaseEventT>);
+ qDebug(EVENTS) << "Added factory method for"
+ << EventT::matrixTypeId() << "events;" << this->size()
+ << "methods in the" << name << "chain by now";
+ return true;
+ }
+
+ auto loadEvent(const QJsonObject& json, const QString& matrixType)
+ {
+ for (const auto& f : *this)
+ if (auto e = f(json, matrixType))
+ return e;
+ return makeEvent<BaseEventT>(unknownEventTypeId(), json);
+ }
+
+ const char* const name;
+ };
+} // namespace _impl
// === Event ===
class Event {
- Q_GADGET
- Q_PROPERTY(Type type READ type CONSTANT)
- Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT)
public:
using Type = event_type_t;
- using factory_t = EventFactory<Event>;
+ static inline _impl::EventFactory<Event> factory { "Event" };
explicit Event(Type type, const QJsonObject& json);
explicit Event(Type type, event_mtype_t matrixType,
@@ -213,7 +205,10 @@ public:
Type type() const { return _type; }
QString matrixType() const;
+ [[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() "
+ "or by other means")]]
QByteArray originalJson() const;
+ [[deprecated("Use fullJson() instead")]] //
QJsonObject originalJsonObject() const { return fullJson(); }
const QJsonObject& fullJson() const { return _json; }
@@ -222,19 +217,30 @@ public:
// a "content" object; but since its structure is different for
// different types, we're implementing it per-event type.
+ // NB: const return types below are meant to catch accidental attempts
+ // to change event JSON (e.g., consider contentJson()["inexistentKey"]).
+
const QJsonObject contentJson() const;
- const QJsonObject unsignedJson() const;
+
+ template <typename T = QJsonValue, typename KeyT>
+ const T contentPart(KeyT&& key) const
+ {
+ return fromJson<T>(contentJson()[std::forward<KeyT>(key)]);
+ }
template <typename T>
+ [[deprecated("Use contentPart() to get a part of the event content")]] //
T content(const QString& key) const
{
- return fromJson<T>(contentJson()[key]);
+ return contentPart<T>(key);
}
- template <typename T>
- T content(QLatin1String key) const
+ const QJsonObject unsignedJson() const;
+
+ template <typename T = QJsonValue, typename KeyT>
+ const T unsignedPart(KeyT&& key) const
{
- return fromJson<T>(contentJson()[key]);
+ return fromJson<T>(unsignedJson()[std::forward<KeyT>(key)]);
}
friend QDebug operator<<(QDebug dbg, const Event& e)
@@ -262,7 +268,7 @@ 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().
@@ -274,14 +280,28 @@ using Events = EventsArray<Event>;
// 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 _factoryAdded##_Type = \
+ _Type::factory.addMethod<_Type>(); \
// End of macro
-// === is<>(), eventCast<>() and visit<>() ===
+// === Event loading ===
+// (see also event_loader.h)
+
+//! \brief Point of customisation to dynamically load events
+//!
+//! The default specialisation of this calls BaseEventT::factory 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, and so on
+template <class BaseEventT>
+event_ptr_tt<BaseEventT> doLoadEvent(const QJsonObject& json,
+ const QString& matrixType)
+{
+ return BaseEventT::factory.loadEvent(json, matrixType);
+}
+
+// === is<>(), eventCast<>() and switchOnType<>() ===
template <class EventT>
inline bool is(const Event& e)
@@ -303,12 +323,12 @@ 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 {
@@ -319,52 +339,60 @@ namespace _impl {
&& !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 std::common_type_t<fn_return_t<FnTs>...>
+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 b249b160..22878d4c 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -5,10 +5,13 @@
#include "converters.h"
#include "util.h"
+#include "logging.h"
#include <QtCore/QMimeDatabase>
+#include <QtCore/QFileInfo>
using namespace Quotient::EventContent;
+using std::move;
QJsonObject Base::toJson() const
{
@@ -17,23 +20,44 @@ QJsonObject Base::toJson() const
return o;
}
-FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType,
- const QString& originalFilename)
+FileInfo::FileInfo(const QFileInfo &fi)
+ : mimeType(QMimeDatabase().mimeTypeForFile(fi))
+ , url(QUrl::fromLocalFile(fi.filePath()))
+ , payloadSize(fi.size())
+ , originalName(fi.fileName())
+{
+ Q_ASSERT(fi.isFile());
+}
+
+FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType,
+ Omittable<EncryptedFile> file, QString originalFilename)
: mimeType(mimeType)
- , url(u)
+ , url(move(u))
, payloadSize(payloadSize)
- , originalName(originalFilename)
-{}
+ , originalName(move(originalFilename))
+ , file(file)
+{
+ if (!isValid())
+ qCWarning(MESSAGES)
+ << "To client developers: using FileInfo(QUrl, qint64, ...) "
+ "constructor for non-mxc resources is deprecated since Quotient "
+ "0.7; for local resources, use FileInfo(QFileInfo) instead";
+}
-FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
- const QString& originalFilename)
+FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson,
+ const Omittable<EncryptedFile> &file,
+ QString originalFilename)
: originalInfoJson(infoJson)
, mimeType(
QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString()))
- , url(u)
+ , url(move(mxcUrl))
, payloadSize(fromJson<qint64>(infoJson["size"_ls]))
- , originalName(originalFilename)
+ , originalName(move(originalFilename))
+ , file(file)
{
+ if(url.isEmpty() && file.has_value()) {
+ url = file->url;
+ }
if (!mimeType.isValid())
mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
}
@@ -53,14 +77,20 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const
infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
}
-ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType,
- const QSize& imageSize, const QString& originalFilename)
- : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize)
+ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize)
+ : FileInfo(fi), imageSize(imageSize)
+{}
+
+ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type,
+ QSize imageSize, const Omittable<EncryptedFile> &file, const QString& originalFilename)
+ : FileInfo(mxcUrl, fileSize, type, file, originalFilename)
+ , imageSize(imageSize)
{}
-ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson,
+ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson,
+ const Omittable<EncryptedFile> &file,
const QString& originalFilename)
- : FileInfo(u, infoJson, originalFilename)
+ : FileInfo(mxcUrl, infoJson, file, originalFilename)
, imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt())
{}
@@ -73,9 +103,10 @@ void ImageInfo::fillInfoJson(QJsonObject* infoJson) const
infoJson->insert(QStringLiteral("h"), imageSize.height());
}
-Thumbnail::Thumbnail(const QJsonObject& infoJson)
+Thumbnail::Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file)
: ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()),
- infoJson["thumbnail_info"_ls].toObject())
+ infoJson["thumbnail_info"_ls].toObject(),
+ file)
{}
void Thumbnail::fillInfoJson(QJsonObject* infoJson) const
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
index 60d1f7b7..f609a603 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -12,6 +12,10 @@
#include <QtCore/QUrl>
#include <QtCore/QMetaType>
+#include "encryptedfile.h"
+
+class QFileInfo;
+
namespace Quotient {
namespace EventContent {
/**
@@ -47,13 +51,14 @@ namespace EventContent {
// but specific aggregation structure is altered. See doc comments to
// each type for the list of available attributes.
- // A quick classes inheritance structure follows:
+ // A quick classes inheritance structure follows (the definitions are
+ // spread across eventcontent.h and roommessageevent.h):
// FileInfo
- // FileContent : UrlBasedContent<FileInfo, Thumbnail>
- // AudioContent : UrlBasedContent<FileInfo, Duration>
+ // FileContent : UrlWithThumbnailContent<FileInfo>
+ // AudioContent : PlayableContent<UrlBasedContent<FileInfo>>
// ImageInfo : FileInfo + imageSize attribute
- // ImageContent : UrlBasedContent<ImageInfo, Thumbnail>
- // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration>
+ // ImageContent : UrlWithThumbnailContent<ImageInfo>
+ // VideoContent : PlayableContent<UrlWithThumbnailContent<ImageInfo>>
/**
* A base/mixin class for structures representing an "info" object for
@@ -73,11 +78,15 @@ namespace EventContent {
*/
class FileInfo {
public:
- explicit FileInfo(const QUrl& u, qint64 payloadSize = -1,
+ FileInfo() = default;
+ explicit FileInfo(const QFileInfo& fi);
+ explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1,
const QMimeType& mimeType = {},
- const QString& originalFilename = {});
- FileInfo(const QUrl& u, const QJsonObject& infoJson,
- const QString& originalFilename = {});
+ Omittable<EncryptedFile> file = none,
+ QString originalFilename = {});
+ FileInfo(QUrl mxcUrl, const QJsonObject& infoJson,
+ const Omittable<EncryptedFile> &file,
+ QString originalFilename = {});
bool isValid() const;
@@ -98,6 +107,7 @@ namespace EventContent {
QUrl url;
qint64 payloadSize;
QString originalName;
+ Omittable<EncryptedFile> file = none;
};
template <typename InfoT>
@@ -113,10 +123,14 @@ namespace EventContent {
*/
class ImageInfo : public FileInfo {
public:
- explicit ImageInfo(const QUrl& u, qint64 fileSize = -1,
- QMimeType mimeType = {}, const QSize& imageSize = {},
+ ImageInfo() = default;
+ explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {});
+ explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1,
+ const QMimeType& type = {}, QSize imageSize = {},
+ const Omittable<EncryptedFile> &file = none,
const QString& originalFilename = {});
- ImageInfo(const QUrl& u, const QJsonObject& infoJson,
+ ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson,
+ const Omittable<EncryptedFile> &encryptedFile,
const QString& originalFilename = {});
void fillInfoJson(QJsonObject* infoJson) const;
@@ -134,8 +148,8 @@ namespace EventContent {
*/
class Thumbnail : public ImageInfo {
public:
- Thumbnail() : ImageInfo(QUrl()) {} // To allow empty thumbnails
- Thumbnail(const QJsonObject& infoJson);
+ Thumbnail() = default; // Allow empty thumbnails
+ Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file = none);
Thumbnail(const ImageInfo& info) : ImageInfo(info) {}
using ImageInfo::ImageInfo;
@@ -148,13 +162,13 @@ namespace EventContent {
class TypedBase : public Base {
public:
- explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {}
virtual QMimeType type() const = 0;
virtual const FileInfo* fileInfo() const { return nullptr; }
virtual FileInfo* fileInfo() { return nullptr; }
virtual const Thumbnail* thumbnailInfo() const { return nullptr; }
protected:
+ explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {}
using Base::Base;
};
@@ -175,7 +189,7 @@ namespace EventContent {
explicit UrlBasedContent(const QJsonObject& json)
: TypedBase(json)
, InfoT(QUrl(json["url"].toString()), json["info"].toObject(),
- json["filename"].toString())
+ fromJson<Omittable<EncryptedFile>>(json["file"]), json["filename"].toString())
{
// A small hack to facilitate links creation in QML.
originalJson.insert("mediaId", InfoT::mediaId());
@@ -189,7 +203,11 @@ namespace EventContent {
void fillJson(QJsonObject* json) const override
{
Q_ASSERT(json);
- json->insert("url", InfoT::url.toString());
+ if (!InfoT::file.has_value()) {
+ json->insert("url", InfoT::url.toString());
+ } else {
+ json->insert("file", Quotient::toJson(*InfoT::file));
+ }
if (!InfoT::originalName.isEmpty())
json->insert("filename", InfoT::originalName);
json->insert("info", toInfoJson<InfoT>(*this));
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/reactionevent.h b/lib/events/reactionevent.h
index 777905f2..5a2b98c4 100644
--- a/lib/events/reactionevent.h
+++ b/lib/events/reactionevent.h
@@ -47,7 +47,7 @@ public:
explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {}
EventRelation relation() const
{
- return content<EventRelation>(QStringLiteral("m.relates_to"));
+ return contentPart<EventRelation>("m.relates_to"_ls);
}
};
REGISTER_EVENT_TYPE(ReactionEvent)
diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp
index 4185d92d..72dbf2e3 100644
--- a/lib/events/receiptevent.cpp
+++ b/lib/events/receiptevent.cpp
@@ -25,6 +25,27 @@ Example of a Receipt Event:
using namespace Quotient;
+// The library loads the event-ids-to-receipts JSON map into a vector because
+// map lookups are not used and vectors are massively faster. Same goes for
+// de-/serialization of ReceiptsForEvent::receipts.
+// (XXX: would this be generally preferred across CS API JSON maps?..)
+QJsonObject toJson(const EventsWithReceipts& ewrs)
+{
+ QJsonObject json;
+ for (const auto& e : ewrs) {
+ QJsonObject receiptsJson;
+ for (const auto& r : e.receipts)
+ receiptsJson.insert(r.userId,
+ QJsonObject { { "ts"_ls, toJson(r.timestamp) } });
+ json.insert(e.evtId, QJsonObject { { "m.read"_ls, receiptsJson } });
+ }
+ return json;
+}
+
+ReceiptEvent::ReceiptEvent(const EventsWithReceipts &ewrs)
+ : Event(typeId(), matrixTypeId(), toJson(ewrs))
+{}
+
EventsWithReceipts ReceiptEvent::eventsWithReceipts() const
{
EventsWithReceipts result;
@@ -39,14 +60,14 @@ EventsWithReceipts ReceiptEvent::eventsWithReceipts() const
}
const auto reads =
eventIt.value().toObject().value("m.read"_ls).toObject();
- QVector<Receipt> receipts;
- receipts.reserve(reads.size());
+ QVector<UserTimestamp> usersAtEvent;
+ usersAtEvent.reserve(reads.size());
for (auto userIt = reads.begin(); userIt != reads.end(); ++userIt) {
const auto user = userIt.value().toObject();
- receipts.push_back(
+ usersAtEvent.push_back(
{ userIt.key(), fromJson<QDateTime>(user["ts"_ls]) });
}
- result.push_back({ eventIt.key(), std::move(receipts) });
+ result.push_back({ eventIt.key(), std::move(usersAtEvent) });
}
return result;
}
diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h
index 4feec9ea..9683deef 100644
--- a/lib/events/receiptevent.h
+++ b/lib/events/receiptevent.h
@@ -9,19 +9,20 @@
#include <QtCore/QVector>
namespace Quotient {
-struct Receipt {
+struct UserTimestamp {
QString userId;
QDateTime timestamp;
};
struct ReceiptsForEvent {
QString evtId;
- QVector<Receipt> receipts;
+ QVector<UserTimestamp> receipts;
};
using EventsWithReceipts = QVector<ReceiptsForEvent>;
class ReceiptEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent)
+ explicit ReceiptEvent(const EventsWithReceipts& ewrs);
explicit ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) {}
EventsWithReceipts eventsWithReceipts() const;
diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h
index ed560331..be20bf52 100644
--- a/lib/events/redactionevent.h
+++ b/lib/events/redactionevent.h
@@ -17,7 +17,7 @@ public:
{
return fullJson()["redacts"_ls].toString();
}
- QString reason() const { return contentJson()["reason"_ls].toString(); }
+ QString reason() const { return contentPart<QString>("reason"_ls); }
};
REGISTER_EVENT_TYPE(RedactionEvent)
} // namespace Quotient
diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h
index a4257895..8618ba31 100644
--- a/lib/events/roomavatarevent.h
+++ b/lib/events/roomavatarevent.h
@@ -20,12 +20,12 @@ public:
: StateEvent(typeId(), matrixTypeId(), QString(), avatar)
{}
// A replica of EventContent::ImageInfo constructor
- explicit RoomAvatarEvent(const QUrl& u, qint64 fileSize = -1,
+ explicit RoomAvatarEvent(const QUrl& mxcUrl, qint64 fileSize = -1,
QMimeType mimeType = {},
const QSize& imageSize = {},
const QString& originalFilename = {})
: RoomAvatarEvent(EventContent::ImageContent {
- u, fileSize, mimeType, imageSize, originalFilename })
+ mxcUrl, fileSize, mimeType, imageSize, none, originalFilename })
{}
QUrl url() const { return content().url; }
diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp
index 6558bade..bb6de648 100644
--- a/lib/events/roomcreateevent.cpp
+++ b/lib/events/roomcreateevent.cpp
@@ -5,19 +5,35 @@
using namespace Quotient;
+template <>
+struct Quotient::JsonConverter<RoomType> {
+ static RoomType load(const QJsonValue& jv)
+ {
+ const auto& roomTypeString = jv.toString();
+ for (auto it = RoomTypeStrings.begin(); it != RoomTypeStrings.end();
+ ++it)
+ if (roomTypeString == *it)
+ return RoomType(it - RoomTypeStrings.begin());
+
+ if (!roomTypeString.isEmpty())
+ qCWarning(EVENTS) << "Unknown Room Type: " << roomTypeString;
+ return RoomType::Undefined;
+ }
+};
+
bool RoomCreateEvent::isFederated() const
{
- return fromJson<bool>(contentJson()["m.federate"_ls]);
+ return contentPart<bool>("m.federate"_ls);
}
QString RoomCreateEvent::version() const
{
- return fromJson<QString>(contentJson()["room_version"_ls]);
+ return contentPart<QString>("room_version"_ls);
}
RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const
{
- const auto predJson = contentJson()["predecessor"_ls].toObject();
+ const auto predJson = contentPart<QJsonObject>("predecessor"_ls);
return { fromJson<QString>(predJson[RoomIdKeyL]),
fromJson<QString>(predJson[EventIdKeyL]) };
}
@@ -26,3 +42,8 @@ bool RoomCreateEvent::isUpgrade() const
{
return contentJson().contains("predecessor"_ls);
}
+
+RoomType RoomCreateEvent::roomType() const
+{
+ return contentPart<RoomType>("type"_ls);
+}
diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h
index 05e623ed..b3ad287c 100644
--- a/lib/events/roomcreateevent.h
+++ b/lib/events/roomcreateevent.h
@@ -4,6 +4,7 @@
#pragma once
#include "stateevent.h"
+#include "quotient_common.h"
namespace Quotient {
class RoomCreateEvent : public StateEventBase {
@@ -24,6 +25,7 @@ public:
QString version() const;
Predecessor predecessor() const;
bool isUpgrade() const;
+ RoomType roomType() const;
};
REGISTER_EVENT_TYPE(RoomCreateEvent)
} // namespace Quotient
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 4fec9d2b..b728e0bf 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -9,9 +9,6 @@
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)
@@ -19,7 +16,7 @@ RoomEvent::RoomEvent(Type type, event_mtype_t matrixType,
RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json)
{
- if (const auto redaction = unsignedJson()[RedactedCauseKeyL];
+ if (const auto redaction = unsignedPart(RedactedCauseKeyL);
redaction.isObject())
_redactedBecause = makeEvent<RedactionEvent>(redaction.toObject());
}
@@ -45,14 +42,14 @@ QString RoomEvent::senderId() const
bool RoomEvent::isReplaced() const
{
- return unsignedJson()["m.relations"_ls].toObject().contains("m.replace");
+ return unsignedPart<QJsonObject>("m.relations"_ls).contains("m.replace");
}
QString RoomEvent::replacedBy() const
{
// clang-format off
- return unsignedJson()["m.relations"_ls].toObject()
- .value("m.replace").toObject()
+ return unsignedPart<QJsonObject>("m.relations"_ls)
+ .value("m.replace"_ls).toObject()
.value(EventIdKeyL).toString();
// clang-format on
}
@@ -64,7 +61,7 @@ QString RoomEvent::redactionReason() const
QString RoomEvent::transactionId() const
{
- return unsignedJson()["transaction_id"_ls].toString();
+ return unsignedPart<QString>("transaction_id"_ls);
}
QString RoomEvent::stateKey() const
diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h
index fea509c0..8be58481 100644
--- a/lib/events/roomevent.h
+++ b/lib/events/roomevent.h
@@ -12,16 +12,8 @@ class RedactionEvent;
/** This class corresponds to m.room.* events */
class RoomEvent : public Event {
- Q_GADGET
- Q_PROPERTY(QString id READ id)
- Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT)
- Q_PROPERTY(QString roomId READ roomId CONSTANT)
- Q_PROPERTY(QString senderId READ senderId CONSTANT)
- Q_PROPERTY(QString redactionReason READ redactionReason)
- Q_PROPERTY(bool isRedacted READ isRedacted)
- Q_PROPERTY(QString transactionId READ transactionId WRITE setTransactionId)
public:
- using factory_t = EventFactory<RoomEvent>;
+ static inline _impl::EventFactory<RoomEvent> factory { "RoomEvent" };
// RedactionEvent is an incomplete type here so we cannot inline
// constructors and destructors and we cannot use 'using'.
@@ -32,11 +24,12 @@ public:
QString id() const;
QDateTime originTimestamp() const;
- [[deprecated("Use originTimestamp()")]] QDateTime timestamp() const {
- return originTimestamp();
- }
QString roomId() const;
QString senderId() const;
+ //! \brief Determine whether the event has been replaced
+ //!
+ //! \return true if this event has been overridden by another event
+ //! with `"rel_type": "m.replace"`; false otherwise
bool isReplaced() const;
QString replacedBy() const;
bool isRedacted() const { return bool(_redactedBecause); }
@@ -48,28 +41,23 @@ public:
QString transactionId() const;
QString stateKey() const;
+ //! \brief Fill the pending event object with the room id
void setRoomId(const QString& roomId);
+ //! \brief Fill the pending event object with the sender id
void setSender(const QString& senderId);
-
- /**
- * Sets the transaction id for locally created events. This should be
- * done before the event is exposed to any code using the respective
- * Q_PROPERTY.
- *
- * \param txnId - transaction id, normally obtained from
- * Connection::generateTxnId()
- */
+ //! \brief Fill the pending event object with the transaction id
+ //! \param txnId - transaction id, normally obtained from
+ //! Connection::generateTxnId()
void setTransactionId(const QString& txnId);
- /**
- * Sets event id for locally created events
- *
- * When a new event is created locally, it has no server id yet.
- * This function allows to add the id once the confirmation from
- * the server is received. There should be no id set previously
- * in the event. It's the responsibility of the code calling addId()
- * to notify clients that use Q_PROPERTY(id) about its change
- */
+ //! \brief Add an event id to locally created events after they are sent
+ //!
+ //! When a new event is created locally, it has no id; the homeserver
+ //! assigns it once the event is sent. This function allows to add the id
+ //! once the confirmation from the server is received. There should be no id
+ //! set previously in the event. It's the responsibility of the code calling
+ //! addId() to notify clients about the change; there's no signal or
+ //! callback for that in RoomEvent.
void addId(const QString& newId);
protected:
@@ -90,8 +78,8 @@ public:
~CallEventBase() override = default;
bool isCallEvent() const override { return true; }
- QString callId() const { return content<QString>("call_id"_ls); }
- int version() const { return content<int>("version"_ls); }
+ QString callId() const { return contentPart<QString>("call_id"_ls); }
+ int version() const { return contentPart<int>("version"_ls); }
};
} // namespace Quotient
Q_DECLARE_METATYPE(Quotient::RoomEvent*)
diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h
index 14e80324..d021fbec 100644
--- a/lib/events/roomkeyevent.h
+++ b/lib/events/roomkeyevent.h
@@ -13,10 +13,10 @@ public:
explicit RoomKeyEvent(const QJsonObject& obj);
- QString algorithm() const { return content<QString>("algorithm"_ls); }
- QString roomId() const { return content<QString>(RoomIdKeyL); }
- QString sessionId() const { return content<QString>("session_id"_ls); }
- QString sessionKey() const { return content<QString>("session_key"_ls); }
+ QString algorithm() const { return contentPart<QString>("algorithm"_ls); }
+ QString roomId() const { return contentPart<QString>(RoomIdKeyL); }
+ QString sessionId() const { return contentPart<QString>("session_id"_ls); }
+ QString sessionKey() const { return contentPart<QString>("session_key"_ls); }
};
REGISTER_EVENT_TYPE(RoomKeyEvent)
} // namespace Quotient
diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp
index 9634ca3a..3141f6b5 100644
--- a/lib/events/roommemberevent.cpp
+++ b/lib/events/roommemberevent.cpp
@@ -7,27 +7,26 @@
#include "converters.h"
#include "logging.h"
-#include <array>
-
-static const std::array<QString, 5> membershipStrings = {
- { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"),
- QStringLiteral("leave"), QStringLiteral("ban") }
-};
+#include <QtCore/QtAlgorithms>
namespace Quotient {
template <>
-struct JsonConverter<MembershipType> {
- static MembershipType load(const QJsonValue& jv)
+struct JsonConverter<Membership> {
+ static Membership load(const QJsonValue& jv)
{
- const auto& membershipString = jv.toString();
- for (auto it = membershipStrings.begin(); it != membershipStrings.end();
- ++it)
- if (membershipString == *it)
- return MembershipType(it - membershipStrings.begin());
-
- if (!membershipString.isEmpty())
- qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString;
- return MembershipType::Undefined;
+ const auto& ms = jv.toString();
+ if (ms.isEmpty())
+ {
+ qCWarning(EVENTS) << "Empty membership state";
+ return Membership::Invalid;
+ }
+ const auto it =
+ std::find(MembershipStrings.begin(), MembershipStrings.end(), ms);
+ if (it != MembershipStrings.end())
+ return Membership(1U << (it - MembershipStrings.begin()));
+
+ qCWarning(EVENTS) << "Unknown Membership value: " << ms;
+ return Membership::Invalid;
}
};
} // namespace Quotient
@@ -35,7 +34,7 @@ struct JsonConverter<MembershipType> {
using namespace Quotient;
MemberEventContent::MemberEventContent(const QJsonObject& json)
- : membership(fromJson<MembershipType>(json["membership"_ls]))
+ : membership(fromJson<Membership>(json["membership"_ls]))
, isDirect(json["is_direct"_ls].toBool())
, displayName(fromJson<Omittable<QString>>(json["displayname"_ls]))
, avatarUrl(fromJson<Omittable<QString>>(json["avatar_url"_ls]))
@@ -48,10 +47,10 @@ MemberEventContent::MemberEventContent(const QJsonObject& json)
void MemberEventContent::fillJson(QJsonObject* o) const
{
Q_ASSERT(o);
- Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__,
- "The key 'membership' must be explicit in MemberEventContent");
- if (membership != MembershipType::Undefined)
- o->insert(QStringLiteral("membership"), membershipStrings[membership]);
+ if (membership != Membership::Invalid)
+ o->insert(QStringLiteral("membership"),
+ MembershipStrings[qCountTrailingZeroBits(
+ std::underlying_type_t<Membership>(membership))]);
if (displayName)
o->insert(QStringLiteral("displayname"), *displayName);
if (avatarUrl && avatarUrl->isValid())
@@ -67,51 +66,49 @@ bool RoomMemberEvent::changesMembership() const
bool RoomMemberEvent::isInvite() const
{
- return membership() == MembershipType::Invite && changesMembership();
+ return membership() == Membership::Invite && changesMembership();
}
bool RoomMemberEvent::isRejectedInvite() const
{
- return membership() == MembershipType::Leave && prevContent()
- && prevContent()->membership == MembershipType::Invite;
+ return membership() == Membership::Leave && prevContent()
+ && prevContent()->membership == Membership::Invite;
}
bool RoomMemberEvent::isJoin() const
{
- return membership() == MembershipType::Join && changesMembership();
+ return membership() == Membership::Join && changesMembership();
}
bool RoomMemberEvent::isLeave() const
{
- return membership() == MembershipType::Leave && prevContent()
+ return membership() == Membership::Leave && prevContent()
&& prevContent()->membership != membership()
- && prevContent()->membership != MembershipType::Ban
- && prevContent()->membership != MembershipType::Invite;
+ && prevContent()->membership != Membership::Ban
+ && prevContent()->membership != Membership::Invite;
}
bool RoomMemberEvent::isBan() const
{
- return membership() == MembershipType::Ban && changesMembership();
+ return membership() == Membership::Ban && changesMembership();
}
bool RoomMemberEvent::isUnban() const
{
- return membership() == MembershipType::Leave && prevContent()
- && prevContent()->membership == MembershipType::Ban;
+ return membership() == Membership::Leave && prevContent()
+ && prevContent()->membership == Membership::Ban;
}
bool RoomMemberEvent::isRename() const
{
- auto prevName = prevContent() && prevContent()->displayName
- ? *prevContent()->displayName
- : QString();
- return newDisplayName() != prevName;
+ return prevContent() && prevContent()->displayName
+ ? newDisplayName() != *prevContent()->displayName
+ : newDisplayName().has_value();
}
bool RoomMemberEvent::isAvatarUpdate() const
{
- auto prevAvatarUrl = prevContent() && prevContent()->avatarUrl
- ? *prevContent()->avatarUrl
- : QUrl();
- return newAvatarUrl() != prevAvatarUrl;
+ return prevContent() && prevContent()->avatarUrl
+ ? newAvatarUrl() != *prevContent()->avatarUrl
+ : newAvatarUrl().has_value();
}
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
index f2fbe689..0fb464d4 100644
--- a/lib/events/roommemberevent.h
+++ b/lib/events/roommemberevent.h
@@ -7,23 +7,21 @@
#include "eventcontent.h"
#include "stateevent.h"
+#include "quotient_common.h"
namespace Quotient {
class MemberEventContent : public EventContent::Base {
public:
- enum MembershipType : unsigned char {
- Invite = 0,
- Join,
- Knock,
- Leave,
- Ban,
- Undefined
- };
+ using MembershipType
+ [[deprecated("Use Quotient::Membership instead")]] = Membership;
- explicit MemberEventContent(MembershipType mt = Join) : membership(mt) {}
+ explicit MemberEventContent(Membership ms = Membership::Join)
+ : membership(ms)
+ {}
explicit MemberEventContent(const QJsonObject& json);
- MembershipType membership;
+ Membership membership;
+ /// (Only for invites) Whether the invite is to a direct chat
bool isDirect = false;
Omittable<QString> displayName;
Omittable<QUrl> avatarUrl;
@@ -33,15 +31,15 @@ protected:
void fillJson(QJsonObject* o) const override;
};
-using MembershipType = MemberEventContent::MembershipType;
+using MembershipType [[deprecated("Use Membership instead")]] = Membership;
class RoomMemberEvent : public StateEvent<MemberEventContent> {
Q_GADGET
public:
DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent)
- using MembershipType = MemberEventContent::MembershipType;
- Q_ENUM(MembershipType)
+ using MembershipType
+ [[deprecated("Use Quotient::Membership instead")]] = Membership;
explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj)
{}
@@ -51,21 +49,20 @@ public:
std::forward<ArgTs>(contentArgs)...)
{}
- /// 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)
{}
- MembershipType membership() const { return content().membership; }
+ Membership membership() const { return content().membership; }
QString userId() const { return stateKey(); }
bool isDirect() const { return content().isDirect; }
Omittable<QString> newDisplayName() const { return content().displayName; }
@@ -91,14 +88,13 @@ 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 31c0fd9e..2b7b4166 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -10,7 +10,9 @@
#include <QtCore/QFileInfo>
#include <QtCore/QMimeDatabase>
#include <QtGui/QImageReader>
-#include <QtMultimedia/QMediaResource>
+#if QT_VERSION_MAJOR < 6
+# include <QtMultimedia/QMediaResource>
+#endif
using namespace Quotient;
using namespace EventContent;
@@ -133,6 +135,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType,
: RoomMessageEvent(plainBody, msgTypeToJson(msgType), content)
{}
+#if QT_VERSION_MAJOR < 6
TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
{
auto filePath = file.absoluteFilePath();
@@ -142,21 +145,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
auto mimeTypeName = mimeType.name();
if (mimeTypeName.startsWith("image/"))
return new ImageContent(localUrl, file.size(), mimeType,
- QImageReader(filePath).size(),
+ QImageReader(filePath).size(), none,
file.fileName());
// duration can only be obtained asynchronously and can only be reliably
// done by starting to play the file. Left for a future implementation.
if (mimeTypeName.startsWith("video/"))
return new VideoContent(localUrl, file.size(), mimeType,
- QMediaResource(localUrl).resolution(),
+ QMediaResource(localUrl).resolution(), none,
file.fileName());
if (mimeTypeName.startsWith("audio/"))
- return new AudioContent(localUrl, file.size(), mimeType,
+ return new AudioContent(localUrl, file.size(), mimeType, none,
file.fileName());
}
- return new FileContent(localUrl, file.size(), mimeType, file.fileName());
+ return new FileContent(localUrl, file.size(), mimeType, none, file.fileName());
}
RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
@@ -166,6 +169,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
: rawMsgTypeForFile(file),
contentFromFile(file, asGenericFile))
{}
+#endif
RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
: RoomEvent(typeId(), obj), _content(nullptr)
@@ -200,12 +204,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const
QString RoomMessageEvent::rawMsgtype() const
{
- return contentJson()[MsgTypeKeyL].toString();
+ return contentPart<QString>(MsgTypeKeyL);
}
QString RoomMessageEvent::plainBody() const
{
- return contentJson()[BodyKeyL].toString();
+ return contentPart<QString>(BodyKeyL);
}
QMimeType RoomMessageEvent::mimeType() const
diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h
index 8303ce4e..56597ddc 100644
--- a/lib/events/roommessageevent.h
+++ b/lib/events/roommessageevent.h
@@ -18,10 +18,6 @@ namespace MessageEventContent = EventContent; // Back-compatibility
*/
class RoomMessageEvent : public RoomEvent {
Q_GADGET
- Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT)
- Q_PROPERTY(QString plainBody READ plainBody CONSTANT)
- Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT)
- Q_PROPERTY(const EventContent::TypedBase* content READ content CONSTANT)
public:
DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent)
@@ -42,8 +38,12 @@ public:
explicit RoomMessageEvent(const QString& plainBody,
MsgType msgType = MsgType::Text,
EventContent::TypedBase* content = nullptr);
+#if QT_VERSION_MAJOR < 6
+ [[deprecated("Create an EventContent object on the client side"
+ " and pass it to other constructors")]] //
explicit RoomMessageEvent(const QString& plainBody, const QFileInfo& file,
bool asGenericFile = false);
+#endif
explicit RoomMessageEvent(const QJsonObject& obj);
MsgType msgtype() const;
@@ -58,9 +58,26 @@ public:
_content.data());
}
QMimeType mimeType() const;
+ //! \brief Determine whether the message has text content
+ //!
+ //! \return true, if the message type is one of m.text, m.notice, m.emote,
+ //! or the message type is unspecified (in which case plainBody()
+ //! can still be examined); false otherwise
bool hasTextContent() const;
+ //! \brief Determine whether the message has a file/attachment
+ //!
+ //! \return true, if the message has a data structure corresponding to
+ //! a file (such as m.file or m.audio); false otherwise
bool hasFileContent() const;
+ //! \brief Determine whether the message has a thumbnail
+ //!
+ //! \return true, if the message has a data structure corresponding to
+ //! a thumbnail (the message type may be one for visual content,
+ //! such as m.image, or generic binary content, i.e. m.file);
+ //! false otherwise
bool hasThumbnail() const;
+ //! \brief Obtain id of an event replaced by the current one
+ //! \sa RoomEvent::isReplaced, RoomEvent::replacedBy
QString replacedEvent() const;
static QString rawMsgTypeForUrl(const QUrl& url);
diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp
index 080d269c..2c3492d6 100644
--- a/lib/events/roomtombstoneevent.cpp
+++ b/lib/events/roomtombstoneevent.cpp
@@ -7,10 +7,10 @@ using namespace Quotient;
QString RoomTombstoneEvent::serverMessage() const
{
- return fromJson<QString>(contentJson()["body"_ls]);
+ return contentPart<QString>("body"_ls);
}
QString RoomTombstoneEvent::successorRoomId() const
{
- return fromJson<QString>(contentJson()["replacement_room"_ls]);
+ return contentPart<QString>("replacement_room"_ls);
}
diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h
index c977cb6e..13597979 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,11 +24,8 @@ namespace EventContent {
return { { key, Quotient::toJson(value) } };
}
- public:
T value;
-
- protected:
- QString key;
+ const QString key;
};
} // namespace EventContent
@@ -57,19 +53,16 @@ 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 RoomAliasesEvent
- : public StateEvent<EventContent::SimpleContent<QStringList>> {
+class [[deprecated(
+ "m.room.aliases events are deprecated by the Matrix spec; use"
+ " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] //
+RoomAliasesEvent : public StateEvent<EventContent::SimpleContent<QStringList>> {
public:
DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent)
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; }
};
-REGISTER_EVENT_TYPE(RoomAliasesEvent)
} // namespace Quotient
diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp
index 42fc9054..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,
@@ -28,22 +21,22 @@ StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType,
bool StateEventBase::repeatsState() const
{
- const auto prevContentJson = unsignedJson().value(PrevContentKeyL);
+ const auto prevContentJson = unsignedPart(PrevContentKeyL);
return fullJson().value(ContentKeyL) == prevContentJson;
}
QString StateEventBase::replacedState() const
{
- return unsignedJson().value("replaces_state"_ls).toString();
+ return unsignedPart<QString>("replaces_state"_ls);
}
void StateEventBase::dumpTo(QDebug dbg) const
{
if (!stateKey().isEmpty())
dbg << '<' << stateKey() << "> ";
- if (unsignedJson().contains(PrevContentKeyL))
- dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject())
- .toJson(QJsonDocument::Compact)
+ if (const auto prevContentJson = unsignedPart<QJsonObject>(PrevContentKeyL);
+ !prevContentJson.isEmpty())
+ dbg << QJsonDocument(prevContentJson).toJson(QJsonDocument::Compact)
<< " -> ";
RoomEvent::dumpTo(dbg);
}
diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h
index 1415f709..c37965aa 100644
--- a/lib/events/stateevent.h
+++ b/lib/events/stateevent.h
@@ -18,13 +18,10 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId,
}
class StateEventBase : public RoomEvent {
- Q_GADGET
- Q_PROPERTY(QString stateKey READ stateKey CONSTANT)
public:
- using factory_t = EventFactory<StateEventBase>;
+ static inline _impl::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 = {});
@@ -39,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)
{
@@ -100,10 +113,6 @@ public:
visitor(_content);
editJson()[ContentKeyL] = _content.toJson();
}
- [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const
- {
- return prevContent();
- }
const ContentT* prevContent() const
{
return _prev ? &_prev->content : nullptr;
diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp
index ea4dff3f..628fd154 100644
--- a/lib/events/stickerevent.cpp
+++ b/lib/events/stickerevent.cpp
@@ -12,7 +12,7 @@ StickerEvent::StickerEvent(const QJsonObject &obj)
QString StickerEvent::body() const
{
- return content<QString>("body"_ls);
+ return contentPart<QString>("body"_ls);
}
const EventContent::ImageContent &StickerEvent::image() const
diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp
index e97e978f..7e5d7ee6 100644
--- a/lib/events/typingevent.cpp
+++ b/lib/events/typingevent.cpp
@@ -7,5 +7,5 @@ using namespace Quotient;
QStringList TypingEvent::users() const
{
- return fromJson<QStringList>(contentJson()["user_ids"_ls]);
+ return contentPart<QStringList>("user_ids"_ls);
}
diff --git a/lib/eventstats.cpp b/lib/eventstats.cpp
new file mode 100644
index 00000000..9fa7f5ff
--- /dev/null
+++ b/lib/eventstats.cpp
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: 2021 Quotient contributors
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "eventstats.h"
+
+using namespace Quotient;
+
+EventStats EventStats::fromRange(const Room* room, const Room::rev_iter_t& from,
+ const Room::rev_iter_t& to,
+ const EventStats& init)
+{
+ Q_ASSERT(to <= room->historyEdge());
+ Q_ASSERT(from >= Room::rev_iter_t(room->syncEdge()));
+ Q_ASSERT(from <= to);
+ QElapsedTimer et;
+ et.start();
+ const auto result =
+ accumulate(from, to, init,
+ [room](EventStats acc, const TimelineItem& ti) {
+ acc.notableCount += room->isEventNotable(ti);
+ acc.highlightCount += room->notificationFor(ti).type
+ == Notification::Highlight;
+ return acc;
+ });
+ if (et.nsecsElapsed() > profilerMinNsecs() / 10)
+ qCDebug(PROFILER).nospace()
+ << "Event statistics collection over index range [" << from->index()
+ << "," << (to - 1)->index() << "] took " << et;
+ return result;
+}
+
+EventStats EventStats::fromMarker(const Room* room,
+ const EventStats::marker_t& marker)
+{
+ const auto s = fromRange(room, marker_t(room->syncEdge()), marker,
+ { 0, 0, marker == room->historyEdge() });
+ Q_ASSERT(s.isValidFor(room, marker));
+ return s;
+}
+
+EventStats EventStats::fromCachedCounters(Omittable<int> notableCount,
+ Omittable<int> highlightCount)
+{
+ const auto hCount = std::max(0, highlightCount.value_or(0));
+ if (!notableCount.has_value())
+ return { 0, hCount, true };
+ auto nCount = notableCount.value_or(0);
+ return { std::max(0, nCount), hCount, nCount != -1 };
+}
+
+bool EventStats::updateOnMarkerMove(const Room* room, const marker_t& oldMarker,
+ const marker_t& newMarker)
+{
+ if (newMarker == oldMarker)
+ return false;
+
+ // Double-check consistency between the old marker and the old stats
+ Q_ASSERT(isValidFor(room, oldMarker));
+ Q_ASSERT(oldMarker > newMarker);
+
+ // A bit of optimisation: only calculate the difference if the marker moved
+ // less than half the remaining timeline ahead; otherwise, recalculation
+ // over the remaining timeline will very likely be faster.
+ if (oldMarker != room->historyEdge()
+ && oldMarker - newMarker < newMarker - marker_t(room->syncEdge())) {
+ const auto removedStats = fromRange(room, newMarker, oldMarker);
+ Q_ASSERT(notableCount >= removedStats.notableCount
+ && highlightCount >= removedStats.highlightCount);
+ notableCount -= removedStats.notableCount;
+ highlightCount -= removedStats.highlightCount;
+ return removedStats.notableCount > 0 || removedStats.highlightCount > 0;
+ }
+
+ const auto newStats = EventStats::fromMarker(room, newMarker);
+ if (!isEstimate && newStats == *this)
+ return false;
+ *this = newStats;
+ return true;
+}
+
+bool EventStats::isValidFor(const Room* room, const marker_t& marker) const
+{
+ const auto markerAtHistoryEdge = marker == room->historyEdge();
+ // Either markerAtHistoryEdge and isEstimate are in the same state, or it's
+ // a special case of no notable events and the marker at history edge
+ // (then isEstimate can assume any value).
+ return markerAtHistoryEdge == isEstimate
+ || (markerAtHistoryEdge && notableCount == 0);
+}
+
+QDebug Quotient::operator<<(QDebug dbg, const EventStats& es)
+{
+ QDebugStateSaver _(dbg);
+ dbg.nospace() << es.notableCount << '/' << es.highlightCount;
+ if (es.isEstimate)
+ dbg << " (estimated)";
+ return dbg;
+}
diff --git a/lib/eventstats.h b/lib/eventstats.h
new file mode 100644
index 00000000..77c661a7
--- /dev/null
+++ b/lib/eventstats.h
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: 2021 Quotient contributors
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "room.h"
+
+namespace Quotient {
+
+//! \brief Counters of unread events and highlights with a precision flag
+//!
+//! This structure contains a static snapshot with values of unread counters
+//! returned by Room::partiallyReadStats and Room::unreadStats (properties
+//! or methods).
+//!
+//! \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 {
+ Q_GADGET
+ Q_PROPERTY(qsizetype notableCount MEMBER notableCount CONSTANT)
+ Q_PROPERTY(qsizetype highlightCount MEMBER highlightCount CONSTANT)
+ Q_PROPERTY(bool isEstimate MEMBER isEstimate CONSTANT)
+public:
+ //! The number of "notable" events in an events range
+ //! \sa Room::isEventNotable
+ qsizetype notableCount = 0;
+ qsizetype highlightCount = 0;
+ //! \brief Whether the counter values above are exact
+ //!
+ //! This is false when the end marker (m.read receipt or m.fully_read) used
+ //! to collect the stats points to an event loaded locally and the counters
+ //! can therefore be calculated exactly using the locally available segment
+ //! of the timeline; true when the marker points to an event outside of
+ //! the local timeline (in which case the estimation is made basing on
+ //! the data supplied by the homeserver as well as counters saved from
+ //! the previous run of the client).
+ bool isEstimate = true;
+
+ // TODO: replace with = default once C++20 becomes a requirement on clients
+ bool operator==(const EventStats& rhs) const
+ {
+ return notableCount == rhs.notableCount
+ && highlightCount == rhs.highlightCount
+ && isEstimate == rhs.isEstimate;
+ }
+ bool operator!=(const EventStats& rhs) const { return !operator==(rhs); }
+
+ //! \brief Check whether the event statistics are empty
+ //!
+ //! Empty statistics have notable and highlight counters of zero and
+ //! isEstimate set to false.
+ Q_INVOKABLE bool empty() const
+ {
+ return notableCount == 0 && !isEstimate && highlightCount == 0;
+ }
+
+ using marker_t = Room::rev_iter_t;
+
+ //! \brief Build event statistics on a range of events
+ //!
+ //! This is a factory that returns an EventStats instance with counts of
+ //! notable and highlighted events between \p from and \p to reverse
+ //! timeline iterators; the \p init parameter allows to override
+ //! the initial statistics object and start from other values.
+ static EventStats fromRange(const Room* room, const marker_t& from,
+ const marker_t& to,
+ const EventStats& init = { 0, 0, false });
+
+ //! \brief Build event statistics on a range from sync edge to marker
+ //!
+ //! This is mainly a shortcut for \code
+ //! <tt>fromRange(room, marker_t(room->syncEdge()), marker)</tt>
+ //! \endcode except that it also sets isEstimate to true if (and only if)
+ //! <tt>to == room->historyEdge()</tt>.
+ static EventStats fromMarker(const Room* room, const marker_t& marker);
+
+ //! \brief Loads a statistics object from the cached counters
+ //!
+ //! Sets isEstimate to `true` unless both notableCount and highlightCount
+ //! are equal to -1.
+ static EventStats fromCachedCounters(Omittable<int> notableCount,
+ Omittable<int> highlightCount = none);
+
+ //! \brief Update statistics when a read marker moves down the timeline
+ //!
+ //! Removes events between oldMarker and newMarker from statistics
+ //! calculation if \p oldMarker points to an existing event in the timeline,
+ //! or recalculates the statistics entirely if \p oldMarker points
+ //! to <tt>room->historyEdge()</tt>. Always results in exact statistics
+ //! (<tt>isEstimate == false</tt>.
+ //! \param oldMarker Must point correspond to the _current_ statistics
+ //! isEstimate state, i.e. it should point to
+ //! <tt>room->historyEdge()</tt> if <tt>isEstimate == true</tt>, or
+ //! to a valid position within the timeline otherwise
+ //! \param newMarker Must point to a valid position in the timeline (not to
+ //! <tt>room->historyEdge()</tt> that is equal to or closer to
+ //! the sync edge than \p oldMarker
+ //! \return true if either notableCount or highlightCount changed, or if
+ //! the statistics was completely recalculated; false otherwise
+ bool updateOnMarkerMove(const Room* room, const marker_t& oldMarker,
+ const marker_t& newMarker);
+
+ //! \brief Validate the statistics object against the given marker
+ //!
+ //! Checks whether the statistics object data are valid for a given marker.
+ //! No stats recalculation takes place, only isEstimate and zero-ness
+ //! of notableCount are checked.
+ bool isValidFor(const Room* room, const marker_t& marker) const;
+};
+
+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..20bcf30e
--- /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>
+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/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h
index 079da953..87549505 100644
--- a/lib/identity/definitions/request_email_validation.h
+++ b/lib/identity/definitions/request_email_validation.h
@@ -11,16 +11,16 @@ namespace Quotient {
struct RequestEmailValidation {
/// A unique string generated by the client, and used to identify the
/// validation attempt. It must be a string consisting of the characters
- /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it
+ /// `[0-9a-zA-Z.=_-]`. Its length must not exceed 255 characters and it
/// must not be empty.
QString clientSecret;
/// The email address to validate.
QString email;
- /// The server will only send an email if the ``send_attempt``
+ /// The server will only send an email if the `send_attempt`
/// is a number greater than the most recent one which it has seen,
- /// scoped to that ``email`` + ``client_secret`` pair. This is to
+ /// scoped to that `email` + `client_secret` pair. This is to
/// avoid repeatedly sending the same email in the case of request
/// retries between the POSTing user and the identity server.
/// The client should increment this value if they desire a new
diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h
index a29fd0de..d2ea463f 100644
--- a/lib/identity/definitions/request_msisdn_validation.h
+++ b/lib/identity/definitions/request_msisdn_validation.h
@@ -11,20 +11,20 @@ namespace Quotient {
struct RequestMsisdnValidation {
/// A unique string generated by the client, and used to identify the
/// validation attempt. It must be a string consisting of the characters
- /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it
+ /// `[0-9a-zA-Z.=_-]`. Its length must not exceed 255 characters and it
/// must not be empty.
QString clientSecret;
/// The two-letter uppercase ISO-3166-1 alpha-2 country code that the
- /// number in ``phone_number`` should be parsed as if it were dialled from.
+ /// number in `phone_number` should be parsed as if it were dialled from.
QString country;
/// The phone number to validate.
QString phoneNumber;
- /// The server will only send an SMS if the ``send_attempt`` is a
+ /// The server will only send an SMS if the `send_attempt` is a
/// number greater than the most recent one which it has seen,
- /// scoped to that ``country`` + ``phone_number`` + ``client_secret``
+ /// scoped to that `country` + `phone_number` + `client_secret`
/// triple. This is to avoid repeatedly sending the same SMS in
/// the case of request retries between the POSTing user and the
/// identity server. The client should increment this value if
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index 48c2996d..971fea7b 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -5,6 +5,7 @@
#include "basejob.h"
#include "connectiondata.h"
+#include "quotient_common.h"
#include <QtCore/QRegularExpression>
#include <QtCore/QTimer>
@@ -15,8 +16,6 @@
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
-#include <array>
-
using namespace Quotient;
using std::chrono::seconds, std::chrono::milliseconds;
using namespace std::chrono_literals;
@@ -25,7 +24,7 @@ BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode)
{
// Based on https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
if (httpCode / 10 == 41) // 41x errors
- return httpCode == 410 ? IncorrectRequestError : NotFoundError;
+ return httpCode == 410 ? IncorrectRequest : NotFound;
switch (httpCode) {
case 401:
return Unauthorised;
@@ -33,19 +32,19 @@ BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode)
case 403: case 407: // clang-format on
return ContentAccessError;
case 404:
- return NotFoundError;
+ return NotFound;
// clang-format off
case 400: case 405: case 406: case 426: case 428: case 505: // clang-format on
case 494: // Unofficial nginx "Request header too large"
case 497: // Unofficial nginx "HTTP request sent to HTTPS port"
- return IncorrectRequestError;
+ return IncorrectRequest;
case 429:
- return TooManyRequestsError;
+ return TooManyRequests;
case 501:
case 510:
- return RequestNotImplementedError;
+ return RequestNotImplemented;
case 511:
- return NetworkAuthRequiredError;
+ return NetworkAuthRequired;
default:
return NetworkError;
}
@@ -63,12 +62,6 @@ QDebug BaseJob::Status::dumpToLog(QDebug dbg) const
return dbg << ": " << message;
}
-template <typename... Ts>
-constexpr auto make_array(Ts&&... items)
-{
- return std::array<std::common_type_t<Ts...>, sizeof...(Ts)>({items...});
-}
-
class BaseJob::Private {
public:
struct JobTimeoutConfig {
@@ -78,8 +71,8 @@ public:
// Using an idiom from clang-tidy:
// http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html
- Private(HttpVerb v, QString endpoint, const QUrlQuery& q, Data&& data,
- bool nt)
+ Private(HttpVerb v, QByteArray endpoint, const QUrlQuery& q,
+ RequestData&& data, bool nt)
: verb(v)
, apiEndpoint(std::move(endpoint))
, requestQuery(q)
@@ -113,10 +106,10 @@ public:
// Contents for the network request
HttpVerb verb;
- QString apiEndpoint;
+ QByteArray apiEndpoint;
QHash<QByteArray, QByteArray> requestHeaders;
QUrlQuery requestQuery;
- Data requestData;
+ RequestData requestData;
bool needsToken;
bool inBackground = false;
@@ -173,14 +166,36 @@ public:
}
};
-BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
+inline bool isHex(QChar c)
+{
+ return c.isDigit() || (c >= u'A' && c <= u'F') || (c >= u'a' && c <= u'f');
+}
+
+QByteArray BaseJob::encodeIfParam(const QString& paramPart)
+{
+ const auto percentIndex = paramPart.indexOf('%');
+ if (percentIndex != -1 && paramPart.size() > percentIndex + 2
+ && isHex(paramPart[percentIndex + 1])
+ && isHex(paramPart[percentIndex + 2])) {
+ qCWarning(JOBS)
+ << "Developers, upfront percent-encoding of job parameters is "
+ "deprecated since libQuotient 0.7; the string involved is"
+ << paramPart;
+ return QUrl(paramPart, QUrl::TolerantMode).toEncoded();
+ }
+ return QUrl::toPercentEncoding(paramPart);
+}
+
+BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
bool needsToken)
- : BaseJob(verb, name, endpoint, Query {}, Data {}, needsToken)
+ : BaseJob(verb, name, std::move(endpoint), QUrlQuery {}, RequestData {},
+ needsToken)
{}
-BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
- const Query& query, Data&& data, bool needsToken)
- : d(new Private(verb, endpoint, query, std::move(data), needsToken))
+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))
{
setObjectName(name);
connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
@@ -201,13 +216,6 @@ QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); }
bool BaseJob::isBackground() const { return d->inBackground; }
-const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; }
-
-void BaseJob::setApiEndpoint(const QString& apiEndpoint)
-{
- d->apiEndpoint = apiEndpoint;
-}
-
const BaseJob::headers_t& BaseJob::requestHeaders() const
{
return d->requestHeaders;
@@ -224,16 +232,19 @@ void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers)
d->requestHeaders = headers;
}
-const QUrlQuery& BaseJob::query() const { return d->requestQuery; }
+QUrlQuery BaseJob::query() const { return d->requestQuery; }
void BaseJob::setRequestQuery(const QUrlQuery& query)
{
d->requestQuery = query;
}
-const BaseJob::Data& BaseJob::requestData() const { return d->requestData; }
+const RequestData& BaseJob::requestData() const { return d->requestData; }
-void BaseJob::setRequestData(Data&& data) { std::swap(d->requestData, data); }
+void BaseJob::setRequestData(RequestData&& data)
+{
+ std::swap(d->requestData, data);
+}
const QByteArrayList& BaseJob::expectedContentTypes() const
{
@@ -263,17 +274,17 @@ const QNetworkReply* BaseJob::reply() const { return d->reply.data(); }
QNetworkReply* BaseJob::reply() { return d->reply.data(); }
-QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path,
+QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QByteArray& encodedPath,
const QUrlQuery& query)
{
- auto pathBase = baseUrl.path();
- // QUrl::adjusted(QUrl::StripTrailingSlashes) doesn't help with root '/'
- while (pathBase.endsWith('/'))
- pathBase.chop(1);
- if (!path.startsWith('/')) // Normally API files do start with '/'
- pathBase.push_back('/'); // so this shouldn't be needed these days
-
- baseUrl.setPath(pathBase + path, QUrl::TolerantMode);
+ // Make sure the added path is relative even if it's not (the official
+ // API definitions have the leading slash though it's not really correct).
+ const auto pathUrl =
+ QUrl::fromEncoded(encodedPath.mid(encodedPath.startsWith('/')),
+ QUrl::StrictMode);
+ Q_ASSERT_X(pathUrl.isValid(), __FUNCTION__,
+ qPrintable(pathUrl.errorString()));
+ baseUrl = baseUrl.resolved(pathUrl);
baseUrl.setQuery(query);
return baseUrl;
}
@@ -288,7 +299,8 @@ void BaseJob::Private::sendRequest()
req.setRawHeader("Authorization",
QByteArray("Bearer ") + connection->accessToken());
req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground);
- req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
+ QNetworkRequest::NoLessSafeRedirectPolicy);
req.setMaximumRedirectsAllowed(10);
req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
req.setAttribute(
@@ -353,7 +365,7 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground)
qCCritical(d->logCat)
<< "Developers, ensure the Connection is valid before using it";
Q_ASSERT(false);
- setStatus(IncorrectRequestError, tr("Invalid server connection"));
+ setStatus(IncorrectRequest, tr("Invalid server connection"));
}
// The status is no good, finalise
QTimer::singleShot(0, this, &BaseJob::finishJob);
@@ -403,42 +415,42 @@ BaseJob::Status BaseJob::Private::parseJson()
void BaseJob::gotReply()
{
- setStatus(checkReply(reply()));
-
- if (status().good()
- && d->expectedContentTypes == QByteArrayList { "application/json" }) {
+ // Defer actually updating the status until it's finalised
+ auto statusSoFar = checkReply(reply());
+ if (statusSoFar.good()
+ && d->expectedContentTypes == QByteArrayList { "application/json" }) //
+ {
d->rawResponse = reply()->readAll();
- setStatus(d->parseJson());
- if (status().good() && !expectedKeys().empty()) {
+ statusSoFar = d->parseJson();
+ if (statusSoFar.good() && !expectedKeys().empty()) {
const auto& responseObject = jsonData();
QByteArrayList missingKeys;
for (const auto& k: expectedKeys())
if (!responseObject.contains(k))
missingKeys.push_back(k);
if (!missingKeys.empty())
- setStatus(IncorrectResponse, tr("Required JSON keys missing: ")
- + missingKeys.join());
+ statusSoFar = { IncorrectResponse,
+ tr("Required JSON keys missing: ")
+ + missingKeys.join() };
}
+ setStatus(statusSoFar);
if (!status().good()) // Bad JSON in a "good" reply: bail out
return;
- } // else {
+ }
// If the endpoint expects anything else than just (API-related) JSON
// reply()->readAll() is not performed and the whole reply processing
// is left to derived job classes: they may read it piecemeal or customise
// per content type in prepareResult(), or even have read it already
// (see, e.g., DownloadFileJob).
- // }
-
- if (status().good())
+ if (statusSoFar.good()) {
setStatus(prepareResult());
- else {
- d->rawResponse = reply()->readAll();
- qCDebug(d->logCat).noquote()
- << "Error body (truncated if long):" << rawDataSample(500);
- // Parse the error payload and update the status if needed
- if (const auto newStatus = prepareError(); !newStatus.good())
- setStatus(newStatus);
+ return;
}
+
+ d->rawResponse = reply()->readAll();
+ qCDebug(d->logCat).noquote()
+ << "Error body (truncated if long):" << rawDataSample(500);
+ setStatus(prepareError(statusSoFar));
}
bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
@@ -503,7 +515,7 @@ BaseJob::Status BaseJob::checkReply(const QNetworkReply* reply) const
BaseJob::Status BaseJob::prepareResult() { return Success; }
-BaseJob::Status BaseJob::prepareError()
+BaseJob::Status BaseJob::prepareError(Status currentStatus)
{
// Try to make sense of the error payload but be prepared for all kinds
// of unexpected stuff (raw HTML, plain text, foreign JSON among those)
@@ -513,10 +525,10 @@ BaseJob::Status BaseJob::prepareError()
// By now, if d->parseJson() above succeeded then jsonData() will return
// a valid JSON object - or an empty object otherwise (in which case most
- // of if's below will fall through to `return NoError` at the end
+ // of if's below will fall through retaining the current status)
const auto& errorJson = jsonData();
const auto errCode = errorJson.value("errcode"_ls).toString();
- if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") {
+ if (error() == TooManyRequests || errCode == "M_LIMIT_EXCEEDED") {
QString msg = tr("Too many requests");
int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1);
if (retryAfterMs >= 0)
@@ -526,16 +538,16 @@ BaseJob::Status BaseJob::prepareError()
d->connection->limitRate(milliseconds(retryAfterMs));
- return { TooManyRequestsError, msg };
+ return { TooManyRequests, msg };
}
if (errCode == "M_CONSENT_NOT_GIVEN") {
d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString());
- return { UserConsentRequiredError };
+ return { UserConsentRequired };
}
if (errCode == "M_UNSUPPORTED_ROOM_VERSION"
|| errCode == "M_INCOMPATIBLE_ROOM_VERSION")
- return { UnsupportedRoomVersionError,
+ return { UnsupportedRoomVersion,
errorJson.contains("room_version"_ls)
? tr("Requested room version: %1")
.arg(errorJson.value("room_version"_ls).toString())
@@ -548,9 +560,9 @@ BaseJob::Status BaseJob::prepareError()
// Not localisable on the client side
if (errorJson.contains("error"_ls)) // Keep the code, update the message
- return { d->status.code, errorJson.value("error"_ls).toString() };
+ return { currentStatus.code, errorJson.value("error"_ls).toString() };
- return NoError; // Retain the status if the error payload is not recognised
+ return currentStatus; // The error payload is not recognised
}
QJsonValue BaseJob::takeValueFromJson(const QString& key)
@@ -717,27 +729,27 @@ QString BaseJob::statusCaption() const
return tr("Request was abandoned");
case NetworkError:
return tr("Network problems");
- case TimeoutError:
+ case Timeout:
return tr("Request timed out");
case Unauthorised:
return tr("Unauthorised request");
case ContentAccessError:
return tr("Access error");
- case NotFoundError:
+ case NotFound:
return tr("Not found");
- case IncorrectRequestError:
+ case IncorrectRequest:
return tr("Invalid request");
- case IncorrectResponseError:
+ case IncorrectResponse:
return tr("Response could not be parsed");
- case TooManyRequestsError:
+ case TooManyRequests:
return tr("Too many requests");
- case RequestNotImplementedError:
+ case RequestNotImplemented:
return tr("Function not implemented by the server");
- case NetworkAuthRequiredError:
+ case NetworkAuthRequired:
return tr("Network authentication required");
- case UserConsentRequiredError:
+ case UserConsentRequired:
return tr("User consent required");
- case UnsupportedRoomVersionError:
+ case UnsupportedRoomVersion:
return tr("The server does not support the needed room version");
default:
return tr("Request failed");
@@ -799,7 +811,7 @@ void BaseJob::abandon()
void BaseJob::timeout()
{
- setStatus(TimeoutError, "The job has timed out");
+ setStatus(Timeout, "The job has timed out");
finishJob();
}
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index ca91a781..ddf243ed 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -7,8 +7,10 @@
#include "requestdata.h"
#include "../logging.h"
#include "../converters.h"
+#include "../quotient_common.h"
#include <QtCore/QObject>
+#include <QtCore/QStringBuilder>
class QNetworkReply;
class QSslError;
@@ -23,7 +25,18 @@ class BaseJob : public QObject {
Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
Q_PROPERTY(int statusCode READ error NOTIFY statusChanged)
+
+ static QByteArray encodeIfParam(const QString& paramPart);
+ template <int N>
+ static inline auto encodeIfParam(const char (&constPart)[N])
+ {
+ return constPart;
+ }
+
public:
+#define WITH_DEPRECATED_ERROR_VERSION(Recommended) \
+ Recommended, DECL_DEPRECATED_ENUMERATOR(Recommended##Error, Recommended)
+
/*! The status code of a job
*
* Every job is created in Unprepared status; upon calling prepare()
@@ -34,7 +47,7 @@ public:
*/
enum StatusCode {
Success = 0,
- NoError = Success, // To be compatible with Qt conventions
+ NoError = Success,
Pending = 1,
WarningLevel = 20, //< Warnings have codes starting from this
UnexpectedResponseType = 21,
@@ -43,28 +56,18 @@ public:
Abandoned = 50, //< A tiny period between abandoning and object deletion
ErrorLevel = 100, //< Errors have codes starting from this
NetworkError = 101,
- Timeout,
- TimeoutError = Timeout,
+ WITH_DEPRECATED_ERROR_VERSION(Timeout),
Unauthorised,
ContentAccessError,
- NotFoundError,
- IncorrectRequest,
- IncorrectRequestError = IncorrectRequest,
- IncorrectResponse,
- IncorrectResponseError = IncorrectResponse,
- JsonParseError //< \deprecated Use IncorrectResponse instead
- = IncorrectResponse,
- TooManyRequests,
- TooManyRequestsError = TooManyRequests,
+ WITH_DEPRECATED_ERROR_VERSION(NotFound),
+ WITH_DEPRECATED_ERROR_VERSION(IncorrectRequest),
+ WITH_DEPRECATED_ERROR_VERSION(IncorrectResponse),
+ WITH_DEPRECATED_ERROR_VERSION(TooManyRequests),
RateLimited = TooManyRequests,
- RequestNotImplemented,
- RequestNotImplementedError = RequestNotImplemented,
- UnsupportedRoomVersion,
- UnsupportedRoomVersionError = UnsupportedRoomVersion,
- NetworkAuthRequired,
- NetworkAuthRequiredError = NetworkAuthRequired,
- UserConsentRequired,
- UserConsentRequiredError = UserConsentRequired,
+ WITH_DEPRECATED_ERROR_VERSION(RequestNotImplemented),
+ WITH_DEPRECATED_ERROR_VERSION(UnsupportedRoomVersion),
+ WITH_DEPRECATED_ERROR_VERSION(NetworkAuthRequired),
+ WITH_DEPRECATED_ERROR_VERSION(UserConsentRequired),
CannotLeaveRoom,
UserDeactivated,
FileError,
@@ -72,21 +75,19 @@ public:
};
Q_ENUM(StatusCode)
- /**
- * A simple wrapper around QUrlQuery that allows its creation from
- * a list of string pairs
- */
- class Query : public QUrlQuery {
- public:
- using QUrlQuery::QUrlQuery;
- Query() = default;
- Query(const std::initializer_list<QPair<QString, QString>>& l)
- {
- setQueryItems(l);
- }
- };
+#undef WITH_DEPRECATED_ERROR_VERSION
- using Data = RequestData;
+ template <typename... StrTs>
+ static QByteArray makePath(StrTs&&... parts)
+ {
+ return (QByteArray() % ... % encodeIfParam(parts));
+ }
+
+ using Data
+#ifndef Q_CC_MSVC
+ Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead")
+#endif
+ = RequestData;
/*!
* This structure stores the status of a server call job. The status
@@ -136,10 +137,11 @@ public:
};
public:
- BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
+ BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ bool needsToken = true);
+ BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ const QUrlQuery& query, RequestData&& data = {},
bool needsToken = true);
- BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
- const Query& query, Data&& data = {}, bool needsToken = true);
QUrl requestUrl() const;
bool isBackground() const;
@@ -194,7 +196,7 @@ public:
* If there's no top-level JSON object in the response or if there's
* no node with the key \p keyName, \p defaultValue is returned.
*/
- template <typename T, typename StrT> // Waiting for QStringViews...
+ template <typename T, typename StrT>
T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const
{
const auto& jv = jsonData().value(keyName);
@@ -336,16 +338,18 @@ Q_SIGNALS:
protected:
using headers_t = QHash<QByteArray, QByteArray>;
+ Q_DECL_DEPRECATED_X("Deprecated due to being unused")
const QString& apiEndpoint() const;
+ Q_DECL_DEPRECATED_X("Deprecated due to being unused")
void setApiEndpoint(const QString& apiEndpoint);
const headers_t& requestHeaders() const;
void setRequestHeader(const headers_t::key_type& headerName,
const headers_t::mapped_type& headerValue);
void setRequestHeaders(const headers_t& headers);
- const QUrlQuery& query() const;
+ QUrlQuery query() const;
void setRequestQuery(const QUrlQuery& query);
- const Data& requestData() const;
- void setRequestData(Data&& data);
+ const RequestData& requestData() const;
+ void setRequestData(RequestData&& data);
const QByteArrayList& expectedContentTypes() const;
void addExpectedContentType(const QByteArray& contentType);
void setExpectedContentTypes(const QByteArrayList& contentTypes);
@@ -361,7 +365,7 @@ protected:
* The function ensures exactly one '/' between the path component of
* \p baseUrl and \p path. The query component of \p baseUrl is ignored.
*/
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& path,
+ static QUrl makeRequestUrl(QUrl baseUrl, const QByteArray &encodedPath,
const QUrlQuery& query = {});
/*! Prepares the job for execution
@@ -395,10 +399,12 @@ protected:
* was not good (usually because of an unsuccessful HTTP code).
* The base implementation assumes Matrix JSON error object in the body;
* overrides are strongly recommended to call it for all stock Matrix
- * responses as early as possible but in addition can process custom errors,
+ * responses as early as possible and only then process custom errors,
* with JSON or non-JSON payload.
+ *
+ * \return updated (if necessary) job status
*/
- virtual Status prepareError();
+ virtual Status prepareError(Status currentStatus);
/*! \brief Get direct access to the JSON response object in the job
*
diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp
index 7dbf4ab3..6fe8ef26 100644
--- a/lib/jobs/mediathumbnailjob.cpp
+++ b/lib/jobs/mediathumbnailjob.cpp
@@ -17,13 +17,17 @@ MediaThumbnailJob::MediaThumbnailJob(const QString& serverName,
const QString& mediaId, QSize requestedSize)
: GetContentThumbnailJob(serverName, mediaId, requestedSize.width(),
requestedSize.height(), "scale")
-{}
+{
+ setLoggingCategory(THUMBNAILJOB);
+}
MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize)
: MediaThumbnailJob(mxcUri.authority(),
mxcUri.path().mid(1), // sans leading '/'
requestedSize)
-{}
+{
+ setLoggingCategory(THUMBNAILJOB);
+}
QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; }
diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp
index 047e2920..2c001ccc 100644
--- a/lib/jobs/requestdata.cpp
+++ b/lib/jobs/requestdata.cpp
@@ -3,6 +3,7 @@
#include "requestdata.h"
+#include <QtCore/QIODevice>
#include <QtCore/QBuffer>
#include <QtCore/QByteArray>
#include <QtCore/QJsonArray>
@@ -31,4 +32,8 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {}
RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {}
+RequestData::RequestData(QIODevice* source)
+ : _source(std::unique_ptr<QIODevice>(source))
+{}
+
RequestData::~RequestData() = default;
diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h
index 4958e0f9..4f05e5ff 100644
--- a/lib/jobs/requestdata.h
+++ b/lib/jobs/requestdata.h
@@ -24,8 +24,7 @@ public:
RequestData(const QByteArray& a = {});
RequestData(const QJsonObject& jo);
RequestData(const QJsonArray& ja);
- RequestData(QIODevice* source) : _source(std::unique_ptr<QIODevice>(source))
- {}
+ RequestData(QIODevice* source);
RequestData(RequestData&&) = default;
RequestData& operator=(RequestData&&) = default;
~RequestData();
@@ -36,5 +35,3 @@ private:
std::unique_ptr<QIODevice> _source;
};
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp
index 59a34ef3..9b1b46f0 100644
--- a/lib/jobs/syncjob.cpp
+++ b/lib/jobs/syncjob.cpp
@@ -10,7 +10,7 @@ static size_t jobId = 0;
SyncJob::SyncJob(const QString& since, const QString& filter, int timeout,
const QString& presence)
: BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId),
- QStringLiteral("_matrix/client/r0/sync"))
+ "_matrix/client/r0/sync")
{
setLoggingCategory(SYNCJOB);
QUrlQuery query;
diff --git a/lib/joinstate.h b/lib/joinstate.h
deleted file mode 100644
index 805ce73a..00000000
--- a/lib/joinstate.h
+++ /dev/null
@@ -1,32 +0,0 @@
-// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#pragma once
-
-#include <QtCore/QFlags>
-
-#include <array>
-
-namespace Quotient {
-enum class JoinState : unsigned int {
- Join = 0x1,
- Invite = 0x2,
- Leave = 0x4,
-};
-
-Q_DECLARE_FLAGS(JoinStates, JoinState)
-
-// We cannot use Q_ENUM outside of a Q_OBJECT and besides, we want
-// to use strings that match respective JSON keys.
-static const std::array<const char*, 3> JoinStateStrings { { "join", "invite",
- "leave" } };
-
-inline const char* toCString(JoinState js)
-{
- size_t state = size_t(js), index = 0;
- while (state >>= 1u)
- ++index;
- return JoinStateStrings[index];
-}
-} // namespace Quotient
-Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates)
diff --git a/lib/logging.cpp b/lib/logging.cpp
index af229684..15eac69d 100644
--- a/lib/logging.cpp
+++ b/lib/logging.cpp
@@ -16,4 +16,6 @@ LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral")
LOGGING_CATEGORY(E2EE, "quotient.e2ee")
LOGGING_CATEGORY(JOBS, "quotient.jobs")
LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync")
+LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail")
+LOGGING_CATEGORY(NETWORK, "quotient.network")
LOGGING_CATEGORY(PROFILER, "quotient.profiler")
diff --git a/lib/logging.h b/lib/logging.h
index 432ed16f..5bf050a9 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -16,6 +16,8 @@ Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL)
Q_DECLARE_LOGGING_CATEGORY(E2EE)
Q_DECLARE_LOGGING_CATEGORY(JOBS)
Q_DECLARE_LOGGING_CATEGORY(SYNCJOB)
+Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB)
+Q_DECLARE_LOGGING_CATEGORY(NETWORK)
Q_DECLARE_LOGGING_CATEGORY(PROFILER)
namespace Quotient {
@@ -35,24 +37,18 @@ using QDebugManip = QDebug (*)(QDebug);
*/
inline QDebug formatJson(QDebug debug_object)
{
-#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0)
- return debug_object;
-#else
return debug_object.noquote();
-#endif
}
-/**
- * @brief A helper operator to facilitate usage of formatJson (and possibly
- * other manipulators)
- *
- * @param debug_object to output the json to
- * @param qdm a QDebug manipulator
- * @return a copy of debug_object that has its mode altered by qdm
- */
-inline QDebug operator<<(QDebug debug_object, QDebugManip qdm)
+//! Suppress full qualification of enums/QFlags when logging
+inline QDebug terse(QDebug dbg)
{
- return qdm(debug_object);
+ return
+#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
+ dbg.setVerbosity(0), dbg;
+#else
+ dbg.verbosity(QDebug::MinimumVerbosity);
+#endif
}
inline qint64 profilerMinNsecs()
@@ -66,8 +62,19 @@ inline qint64 profilerMinNsecs()
* 1000;
}
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
+
+/**
+ * @brief A helper operator to facilitate usage of formatJson (and possibly
+ * other manipulators)
+ *
+ * @param debug_object to output the json to
+ * @param qdm a QDebug manipulator
+ * @return a copy of debug_object that has its mode altered by qdm
+ */
+inline QDebug operator<<(QDebug debug_object, Quotient::QDebugManip qdm)
+{
+ return qdm(debug_object);
+}
inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et)
{
diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp
new file mode 100644
index 00000000..0b6643fc
--- /dev/null
+++ b/lib/mxcreply.cpp
@@ -0,0 +1,70 @@
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "mxcreply.h"
+
+#include "room.h"
+
+using namespace Quotient;
+
+class MxcReply::Private
+{
+public:
+ explicit Private(QNetworkReply* r = nullptr)
+ : m_reply(r)
+ {}
+ QNetworkReply* m_reply;
+};
+
+MxcReply::MxcReply(QNetworkReply* reply)
+ : d(std::make_unique<Private>(reply))
+{
+ reply->setParent(this);
+ connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
+ setError(d->m_reply->error(), d->m_reply->errorString());
+ setOpenMode(ReadOnly);
+ Q_EMIT finished();
+ });
+}
+
+MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
+ : d(std::make_unique<Private>(reply))
+{
+ reply->setParent(this);
+ connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() {
+ setError(d->m_reply->error(), d->m_reply->errorString());
+ setOpenMode(ReadOnly);
+ emit finished();
+ });
+}
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
+#define ERROR_SIGNAL errorOccurred
+#else
+#define ERROR_SIGNAL error
+#endif
+
+MxcReply::MxcReply()
+{
+ static const auto BadRequestPhrase = tr("Bad Request");
+ QMetaObject::invokeMethod(this, [this]() {
+ setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 400);
+ setAttribute(QNetworkRequest::HttpReasonPhraseAttribute,
+ BadRequestPhrase);
+ setError(QNetworkReply::ProtocolInvalidOperationError,
+ BadRequestPhrase);
+ setFinished(true);
+ emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError);
+ emit finished();
+ }, Qt::QueuedConnection);
+}
+
+qint64 MxcReply::readData(char *data, qint64 maxSize)
+{
+ return d->m_reply->read(data, maxSize);
+}
+
+void MxcReply::abort()
+{
+ d->m_reply->abort();
+}
diff --git a/lib/mxcreply.h b/lib/mxcreply.h
new file mode 100644
index 00000000..efaf01c6
--- /dev/null
+++ b/lib/mxcreply.h
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtNetwork/QNetworkReply>
+#include <memory>
+
+namespace Quotient {
+class Room;
+
+class MxcReply : public QNetworkReply
+{
+public:
+ explicit MxcReply();
+ explicit MxcReply(QNetworkReply *reply);
+ MxcReply(QNetworkReply* reply, Room* room, const QString& eventId);
+
+public Q_SLOTS:
+ void abort() override;
+
+protected:
+ qint64 readData(char *data, qint64 maxSize) override;
+
+private:
+ class Private;
+ std::unique_ptr<Private> d;
+};
+}
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index a94ead34..57618329 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -3,18 +3,43 @@
#include "networkaccessmanager.h"
+#include "connection.h"
+#include "room.h"
+#include "accountregistry.h"
+#include "mxcreply.h"
+
#include <QtCore/QCoreApplication>
+#include <QtCore/QThreadStorage>
+#include <QtCore/QSettings>
#include <QtNetwork/QNetworkReply>
using namespace Quotient;
class NetworkAccessManager::Private {
public:
+ explicit Private(NetworkAccessManager* q)
+ : q(q)
+ {}
+
+ QNetworkReply* createImplRequest(Operation op,
+ const QNetworkRequest& outerRequest,
+ Connection* connection)
+ {
+ Q_ASSERT(outerRequest.url().scheme() == "mxc");
+ QNetworkRequest r(outerRequest);
+ r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2")
+ .arg(connection->homeserver().toString(),
+ outerRequest.url().authority()
+ + outerRequest.url().path())));
+ return q->createRequest(op, r);
+ }
+
+ NetworkAccessManager* q;
QList<QSslError> ignoredSslErrors;
};
NetworkAccessManager::NetworkAccessManager(QObject* parent)
- : QNetworkAccessManager(parent), d(std::make_unique<Private>())
+ : QNetworkAccessManager(parent), d(std::make_unique<Private>(this))
{}
QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
@@ -34,7 +59,7 @@ void NetworkAccessManager::clearIgnoredSslErrors()
static NetworkAccessManager* createNam()
{
- auto nam = new NetworkAccessManager(QCoreApplication::instance());
+ auto nam = new NetworkAccessManager();
#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
// See #109; in newer Qt, bearer management is deprecated altogether
NetworkAccessManager::connect(nam,
@@ -47,8 +72,11 @@ static NetworkAccessManager* createNam()
NetworkAccessManager* NetworkAccessManager::instance()
{
- static auto* nam = createNam();
- return nam;
+ static QThreadStorage<NetworkAccessManager*> storage;
+ if(!storage.hasLocalData()) {
+ storage.setLocalData(createNam());
+ }
+ return storage.localData();
}
NetworkAccessManager::~NetworkAccessManager() = default;
@@ -56,7 +84,49 @@ NetworkAccessManager::~NetworkAccessManager() = default;
QNetworkReply* NetworkAccessManager::createRequest(
Operation op, const QNetworkRequest& request, QIODevice* outgoingData)
{
+ const auto& mxcUrl = request.url();
+ if (mxcUrl.scheme() == "mxc") {
+ const QUrlQuery query(mxcUrl.query());
+ const auto accountId = query.queryItemValue(QStringLiteral("user_id"));
+ if (accountId.isEmpty()) {
+ // Using QSettings here because Quotient::NetworkSettings
+ // doesn't provide multithreading guarantees
+ static thread_local QSettings s;
+ if (!s.value("Network/allow_direct_media_requests").toBool()) {
+ qCWarning(NETWORK) << "No connection specified";
+ return new MxcReply();
+ }
+ // TODO: Make the best effort with a direct unauthenticated request
+ // to the media server
+ } else {
+ auto* const connection = AccountRegistry::instance().get(accountId);
+ if (!connection) {
+ qCWarning(NETWORK) << "Connection" << accountId << "not found";
+ return new MxcReply();
+ }
+ const auto roomId = query.queryItemValue(QStringLiteral("room_id"));
+ if (!roomId.isEmpty()) {
+ auto room = connection->room(roomId);
+ if (!room) {
+ qCWarning(NETWORK) << "Room" << roomId << "not found";
+ return new MxcReply();
+ }
+ return new MxcReply(
+ d->createImplRequest(op, request, connection), room,
+ query.queryItemValue(QStringLiteral("event_id")));
+ }
+ return new MxcReply(
+ d->createImplRequest(op, request, connection));
+ }
+ }
auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData);
reply->ignoreSslErrors(d->ignoredSslErrors);
return reply;
}
+
+QStringList NetworkAccessManager::supportedSchemesImplementation() const
+{
+ auto schemes = QNetworkAccessManager::supportedSchemesImplementation();
+ schemes += QStringLiteral("mxc");
+ return schemes;
+}
diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h
index 47729a1b..87bc12a1 100644
--- a/lib/networkaccessmanager.h
+++ b/lib/networkaccessmanager.h
@@ -8,6 +8,8 @@
#include <memory>
namespace Quotient {
+class Room;
+class Connection;
class NetworkAccessManager : public QNetworkAccessManager {
Q_OBJECT
public:
@@ -21,6 +23,9 @@ public:
/** Get a pointer to the singleton */
static NetworkAccessManager* instance();
+public Q_SLOTS:
+ QStringList supportedSchemesImplementation() const;
+
private:
QNetworkReply* createRequest(Operation op, const QNetworkRequest& request,
QIODevice* outgoingData = Q_NULLPTR) override;
diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h
index c6fa037a..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>
@@ -19,12 +19,7 @@ namespace _impl {
decorated_slot_tt<ArgTs...> decoratedSlot,
Qt::ConnectionType connType)
{
- // See https://bugreports.qt.io/browse/QTBUG-60339
-#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
- auto pc = std::make_shared<QMetaObject::Connection>();
-#else
auto pc = std::make_unique<QMetaObject::Connection>();
-#endif
auto& c = *pc; // Resolve a reference before pc is moved to lambda
// Perfect forwarding doesn't work through signal-slot connections -
@@ -73,6 +68,17 @@ namespace _impl {
}),
connType);
}
+
+ // TODO: get rid of it as soon as Apple Clang gets proper deduction guides
+ // for std::function<>
+ // ...or consider using QtPrivate magic used by QObject::connect()
+ // ...for inspiration, also check a possible std::not_fn implementation
+ // at https://en.cppreference.com/w/cpp/utility/functional/not_fn
+ template <typename FnT>
+ inline auto wrap_in_function(FnT&& f)
+ {
+ return typename function_traits<FnT>::function_type(std::forward<FnT>(f));
+ }
} // namespace _impl
/*! \brief Create a connection that self-disconnects when its "slot" returns true
@@ -90,7 +96,7 @@ inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context,
const FunctorT& slot,
Qt::ConnectionType connType = Qt::AutoConnection)
{
- return _impl::connectUntil(sender, signal, context, wrap_in_function(slot),
+ return _impl::connectUntil(sender, signal, context, _impl::wrap_in_function(slot),
connType);
}
@@ -100,8 +106,13 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal,
ContextT* context, const FunctorT& slot,
Qt::ConnectionType connType = Qt::AutoConnection)
{
+#if QT_VERSION_MAJOR >= 6
+ return QObject::connect(sender, signal, context, slot,
+ Qt::ConnectionType(connType
+ | Qt::SingleShotConnection));
+#else
return _impl::connectSingleShot(
- sender, signal, context, wrap_in_function(slot), connType);
+ sender, signal, context, _impl::wrap_in_function(slot), connType);
}
// Specialisation for usual Qt slots passed as pointers-to-members.
@@ -114,11 +125,12 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal,
{
// TODO: when switching to C++20, use std::bind_front() instead
return _impl::connectSingleShot(sender, signal, receiver,
- wrap_in_function(
+ _impl::wrap_in_function(
[receiver, slot](const ArgTs&... args) {
(receiver->*slot)(args...);
}),
connType);
+#endif
}
/*! \brief A guard pointer that disconnects an interested object upon destruction
diff --git a/lib/quotient_common.h b/lib/quotient_common.h
index 22fdbe94..0e3e2a40 100644
--- a/lib/quotient_common.h
+++ b/lib/quotient_common.h
@@ -5,18 +5,95 @@
#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
+#define QUO_DECLARE_FLAGS(Flags, Enum) \
+ Q_DECLARE_FLAGS(Flags, Enum) \
+ Q_ENUM_IMPL(Enum) \
+ Q_FLAG(Flags)
+
+#define QUO_DECLARE_FLAGS_NS(Flags, Enum) \
+ Q_DECLARE_FLAGS(Flags, Enum) \
+ Q_ENUM_NS_IMPL(Enum) \
+ Q_FLAG_NS(Flags)
+
+#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \
+ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended
+
namespace Quotient {
Q_NAMESPACE
-/** Enumeration with flags defining the network job running policy
- * So far only background/foreground flags are available.
- *
- * \sa Connection::callApi, Connection::run
- */
-enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
+// std::array {} needs explicit template parameters on macOS because
+// Apple stdlib doesn't have deduction guides for std::array. C++20 has
+// to_array() but that can't be borrowed, this time because of MSVC:
+// https://developercommunity.visualstudio.com/t/vc-ice-p1-initc-line-3652-from-stdto-array/1464038
+// Therefore a simpler (but also slightly more wobbly - it resolves the element
+// type using std::common_type<>) make_array facility is implemented here.
+template <typename... Ts>
+constexpr auto make_array(Ts&&... items)
+{
+ return std::array<std::common_type_t<Ts...>, sizeof...(items)>(
+ { std::forward<Ts>(items)... });
+}
+
+// TODO: code like this should be generated from the CS API definition
+
+//! \brief Membership states
+//!
+//! These are used for member events. The names here are case-insensitively
+//! equal to state names used on the wire.
+//! \sa MemberEventContent, RoomMemberEvent
+enum class Membership : unsigned int {
+ // Specific power-of-2 values (1,2,4,...) are important here as syncdata.cpp
+ // depends on that, as well as Join being the first in line
+ Invalid = 0x0,
+ Join = 0x1,
+ Leave = 0x2,
+ Invite = 0x4,
+ Knock = 0x8,
+ Ban = 0x10,
+ Undefined = Invalid
+};
+QUO_DECLARE_FLAGS_NS(MembershipMask, Membership)
+constexpr inline auto MembershipStrings = make_array(
+ // The order MUST be the same as the order in the original enum
+ "join", "leave", "invite", "knock", "ban");
+
+//! \brief Local user join-state names
+//!
+//! This represents a subset of Membership values that may arrive as the local
+//! user's state grouping for the sync response.
+//! \sa SyncData
+enum class JoinState : std::underlying_type_t<Membership> {
+ Invalid = std::underlying_type_t<Membership>(Membership::Invalid),
+ Join = std::underlying_type_t<Membership>(Membership::Join),
+ Leave = std::underlying_type_t<Membership>(Membership::Leave),
+ Invite = std::underlying_type_t<Membership>(Membership::Invite),
+ Knock = std::underlying_type_t<Membership>(Membership::Knock),
+};
+QUO_DECLARE_FLAGS_NS(JoinStates, JoinState)
+
+constexpr inline auto JoinStateStrings = make_array(
+ MembershipStrings[0], MembershipStrings[1], MembershipStrings[2],
+ MembershipStrings[3] /* same as MembershipStrings, sans "ban" */
+);
+
+//! \brief Network job running policy flags
+//!
+//! So far only background/foreground flags are available.
+//! \sa Connection::callApi, Connection::run
+enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
Q_ENUM_NS(RunningPolicy)
+//! \brief The result of URI resolution using UriResolver
+//! \sa UriResolver
enum UriResolveResult : short {
StillResolving = -1,
UriResolved = 0,
@@ -27,6 +104,16 @@ enum UriResolveResult : short {
};
Q_ENUM_NS(UriResolveResult)
+enum RoomType {
+ Space,
+ Undefined,
+};
+Q_ENUM_NS(RoomType)
+
+constexpr inline auto RoomTypeStrings = make_array(
+ "m.space"
+);
+
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
+Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask)
+Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates)
diff --git a/lib/room.cpp b/lib/room.cpp
index 2a9cc0d8..6854879c 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -15,6 +15,10 @@
#include "e2ee.h"
#include "syncdata.h"
#include "user.h"
+#include "eventstats.h"
+
+// NB: since Qt 6, moc_room.cpp needs User fully defined
+#include "moc_room.cpp"
#include "csapi/account-data.h"
#include "csapi/banning.h"
@@ -51,7 +55,6 @@
#include <QtCore/QDir>
#include <QtCore/QHash>
-#include <QtCore/QMimeDatabase>
#include <QtCore/QPointer>
#include <QtCore/QRegularExpression>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
@@ -98,8 +101,8 @@ public:
UnorderedMap<StateEventKey, StateEventPtr> baseState;
/// State event stubs - events without content, just type and state key
static decltype(baseState) stubbedState;
- /// The state of the room at timeline position after-maxTimelineIndex()
- /// \sa Room::syncEdge
+ /// The state of the room at syncEdge()
+ /// \sa syncEdge
QHash<StateEventKey, const StateEventBase*> currentState;
/// Servers with aliases for this room except the one of the local user
/// \sa Room::remoteAliases
@@ -114,19 +117,21 @@ public:
QHash<QPair<QString, QString>, RelatedEvents> relations;
QString displayname;
Avatar avatar;
- int highlightCount = 0;
- int notificationCount = 0;
+ QHash<QString, Notification> notifications;
+ qsizetype serverHighlightCount = 0;
+ // Starting up with estimate event statistics as there's zero knowledge
+ // about the timeline.
+ EventStats partiallyReadStats {}, unreadStats {};
members_map_t membersMap;
QList<User*> usersTyping;
- QMultiHash<QString, User*> eventIdReadUsers;
+ QHash<QString, QSet<QString>> eventIdReadUsers;
QList<User*> usersInvited;
QList<User*> membersLeft;
- int unreadMessages = 0;
bool displayed = false;
QString firstDisplayedEventId;
QString lastDisplayedEventId;
- QHash<const User*, QString> lastReadEventIds;
- QString serverReadMarker;
+ QHash<QString, ReadReceipt> lastReadReceipts;
+ QString fullyReadUntilEventId;
TagsMap tags;
UnorderedMap<QString, EventPtr> accountData;
QString prevBatch;
@@ -195,6 +200,8 @@ public:
/// A point in the timeline corresponding to baseState
rev_iter_t timelineBase() const { return q->findInTimeline(-1); }
+ rev_iter_t historyEdge() const { return timeline.crend(); }
+ Timeline::const_iterator syncEdge() const { return timeline.cend(); }
void getPreviousContent(int limit = 10, const QString &filter = {});
@@ -210,6 +217,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);
@@ -219,6 +227,16 @@ 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
{
@@ -237,39 +255,34 @@ public:
// return EventT::content_type()
// }
- bool isEventNotable(const TimelineItem& ti) const
- {
- return !ti->isRedacted() && ti->senderId() != connection->userId()
- && is<RoomMessageEvent>(*ti)
- && ti.viewAs<RoomMessageEvent>()->replacedEvent().isEmpty();
- }
-
template <typename EventArrayT>
Changes updateStateFrom(EventArrayT&& events)
{
- Changes changes = NoChange;
+ Changes changes {};
if (!events.empty()) {
QElapsedTimer et;
et.start();
for (auto&& eptr : events) {
const auto& evt = *eptr;
Q_ASSERT(evt.isStateEvent());
- auto change = q->processStateEvent(evt);
- if (change != NoChange) {
+ if (auto change = q->processStateEvent(evt); change) {
changes |= change;
baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
}
}
if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER)
- << "*** Room::Private::updateStateFrom():" << events.size()
- << "event(s)," << et;
+ << "Updated" << q->objectName() << "room state from"
+ << events.size() << "event(s) in" << et;
}
return changes;
}
Changes addNewMessageEvents(RoomEvents&& events);
void addHistoricalMessageEvents(RoomEvents&& events);
+ Changes updateStatsFromSyncData(const SyncRoomData &data, bool fromCache);
+ void postprocessChanges(Changes changes, bool saveState = true);
+
/** Move events into the timeline
*
* Insert events into the timeline, either new or historical.
@@ -287,11 +300,12 @@ public:
*/
void dropDuplicateEvents(RoomEvents& events) const;
- Changes setLastReadEvent(User* u, QString eventId);
- void updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to);
- Changes promoteReadMarker(User* u, const rev_iter_t& newMarker, bool force = false);
-
- Changes markMessagesAsRead(rev_iter_t upToMarker);
+ Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker,
+ ReadReceipt newReceipt = {},
+ bool deferStatsUpdate = false);
+ Changes setFullyReadMarker(const QString &eventId);
+ Changes updateStats(const rev_iter_t& from, const rev_iter_t& to);
+ bool markMessagesAsRead(const rev_iter_t& upToMarker);
void getAllMembers();
@@ -303,6 +317,8 @@ public:
return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...));
}
+ QString doPostFile(RoomEventPtr &&msgEvent, const QUrl &localUrl);
+
RoomEvent* addAsPending(RoomEventPtr&& event);
QString doSendEvent(const RoomEvent* pEvent);
@@ -455,7 +471,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
emit baseStateLoaded();
return this == r; // loadedRoomState fires only once per room
});
- qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id;
+ qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id;
}
Room::~Room() { delete d; }
@@ -535,21 +551,6 @@ QStringList Room::altAliases() const
return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases();
}
-QStringList Room::localAliases() const
-{
- return d->getCurrentState<RoomAliasesEvent>(
- connection()->domain())
- ->aliases();
-}
-
-QStringList Room::remoteAliases() const
-{
- QStringList result;
- for (const auto& s : std::as_const(d->aliasServers))
- result += d->getCurrentState<RoomAliasesEvent>(s)->aliases();
- return result;
-}
-
QString Room::canonicalAlias() const
{
return d->getCurrentState<RoomCanonicalAliasEvent>()->alias();
@@ -574,6 +575,11 @@ QVector< const Quotient::RoomEvent* > Quotient::Room::pinnedEvents() const
return pinnedEvents;
}
+QString Room::displayNameForHtml() const
+{
+ return displayName().toHtmlEscaped();
+}
+
void Room::refreshDisplayName() { d->updateDisplayname(); }
QString Room::topic() const
@@ -593,13 +599,13 @@ QImage Room::avatar(int width, int height)
{
if (!d->avatar.url().isEmpty())
return d->avatar.get(connection(), width, height,
- [=] { emit avatarChanged(); });
+ [this] { emit avatarChanged(); });
// Use the first (excluding self) user's avatar for direct chats
const auto dcUsers = directChatUsers();
for (auto* u : dcUsers)
if (u != localUser())
- return u->avatar(width, height, this, [=] { emit avatarChanged(); });
+ return u->avatar(width, height, this, [this] { emit avatarChanged(); });
return {};
}
@@ -615,6 +621,16 @@ JoinState Room::memberJoinState(User* user) const
: JoinState::Leave;
}
+Membership Room::memberState(const QString& userId) const
+{
+ return d->getCurrentState<RoomMemberEvent>(userId)->membership();
+}
+
+bool Room::isMember(const QString& userId) const
+{
+ return memberState(userId) == Membership::Join;
+}
+
JoinState Room::joinState() const { return d->joinState; }
void Room::setJoinState(JoinState state)
@@ -623,156 +639,261 @@ void Room::setJoinState(JoinState state)
if (state == oldState)
return;
d->joinState = state;
- qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState)
- << "->" << int(state);
- emit changed(Change::JoinStateChange);
+ qCDebug(STATE) << "Room" << id() << "changed state: " << terse << oldState
+ << "->" << state;
emit joinStateChanged(oldState, state);
}
-Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId)
-{
- auto& storedId = lastReadEventIds[u];
- if (storedId == eventId)
- return Change::NoChange;
- eventIdReadUsers.remove(storedId, u);
- eventIdReadUsers.insert(eventId, u);
- swap(storedId, eventId);
- emit q->lastReadEventChanged(u);
- emit q->readMarkerForUserMoved(u, eventId, storedId);
- if (isLocalUser(u)) {
- if (storedId != serverReadMarker)
- connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
- storedId);
- emit q->readMarkerMoved(eventId, storedId);
- return Change::ReadMarkerChange;
+Room::Changes Room::Private::setLastReadReceipt(const QString& userId,
+ rev_iter_t newMarker,
+ ReadReceipt newReceipt,
+ bool deferStatsUpdate)
+{
+ if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty())
+ newMarker = q->findInTimeline(newReceipt.eventId);
+ if (newMarker != historyEdge()) {
+ // Try to auto-promote the read marker over the user's own messages
+ // (switch to direct iterators for that).
+ const auto eagerMarker = find_if(newMarker.base(), syncEdge(),
+ [=](const TimelineItem& ti) {
+ return ti->senderId() != userId;
+ });
+ // eagerMarker is now just after the desired event for newMarker
+ if (eagerMarker != newMarker.base()) {
+ newMarker = rev_iter_t(eagerMarker);
+ qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId
+ << "to" << *newMarker;
+ }
+ // Fill newReceipt with the event (and, if needed, timestamp) from
+ // eagerMarker
+ newReceipt.eventId = (eagerMarker - 1)->event()->id();
+ if (newReceipt.timestamp.isNull())
+ newReceipt.timestamp = QDateTime::currentDateTime();
+ }
+ auto& storedReceipt =
+ lastReadReceipts[userId]; // clazy:exclude=detaching-member
+ const auto prevEventId = storedReceipt.eventId;
+ // Check that either the new marker is actually "newer" than the current one
+ // or, if both markers are at historyEdge(), event ids are different.
+ // NB: with reverse iterators, timeline history edge >= sync edge
+ if (prevEventId == newReceipt.eventId
+ || newMarker > q->findInTimeline(prevEventId))
+ return Change::None;
+
+ // Finally make the change
+
+ Changes changes = Change::Other;
+ auto oldEventReadUsersIt =
+ eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member
+ if (oldEventReadUsersIt != eventIdReadUsers.end()) {
+ oldEventReadUsersIt->remove(userId);
+ if (oldEventReadUsersIt->isEmpty())
+ eventIdReadUsers.erase(oldEventReadUsersIt);
+ }
+ eventIdReadUsers[newReceipt.eventId].insert(userId);
+ storedReceipt = move(newReceipt);
+
+ {
+ auto dbg = qDebug(EPHEMERAL); // This trick needs qDebug, not qCDebug
+ dbg << "The new read receipt for" << userId << "is now at";
+ if (newMarker == historyEdge())
+ dbg << storedReceipt.eventId;
+ else
+ dbg << *newMarker;
+ }
+
+ // TODO: use Room::member() when it becomes a thing and only emit signals
+ // for actual members, not just any user
+ const auto member = q->user(userId);
+ Q_ASSERT(member != nullptr);
+ if (isLocalUser(member) && !deferStatsUpdate) {
+ if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(prevEventId),
+ newMarker)) {
+ qCDebug(MESSAGES)
+ << "Updated unread event statistics in" << q->objectName()
+ << "after moving the local read receipt:" << unreadStats;
+ changes |= Change::UnreadStats;
+ }
+ Q_ASSERT(unreadStats.isValidFor(q, newMarker)); // post-check
}
- return Change::NoChange;
+ emit q->lastReadEventChanged(member);
+ // TODO: remove in 0.8
+ if (!isLocalUser(member))
+ emit q->readMarkerForUserMoved(member, prevEventId,
+ storedReceipt.eventId);
+ return changes;
}
-void Room::Private::updateUnreadCount(const rev_iter_t& from,
- const rev_iter_t& to)
+Room::Changes Room::Private::updateStats(const rev_iter_t& from,
+ const rev_iter_t& to)
{
Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend());
Q_ASSERT(to >= from && to <= timeline.crend());
- // Catch a special case when the last read event id refers to an event
- // that has just arrived. In this case we should recalculate
- // unreadMessages and might need to promote the read marker further
- // over local-origin messages.
- auto readMarker = q->readMarker();
- if (readMarker == timeline.crend() && q->allHistoryLoaded())
- --readMarker; // Read marker not found in the timeline, initialise it
- if (readMarker >= from && readMarker < to) {
- promoteReadMarker(q->localUser(), readMarker, true);
- return;
+ const auto fullyReadMarker = q->fullyReadMarker();
+ auto readReceiptMarker = q->localReadReceiptMarker();
+ Changes changes = Change::None;
+ // Correct the read receipt to never be behind the fully read marker
+ if (readReceiptMarker > fullyReadMarker
+ && setLastReadReceipt(connection->userId(), fullyReadMarker, {}, true)) {
+ changes |= Change::Other;
+ readReceiptMarker = q->localReadReceiptMarker();
+ qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read "
+ "marker - it's now corrected to be at index"
+ << readReceiptMarker->index();
+ }
+
+ if (fullyReadMarker < from)
+ return Change::None; // What's arrived is already fully read
+
+ // If there's no read marker in the whole room, initialise it
+ if (fullyReadMarker == historyEdge() && q->allHistoryLoaded())
+ return setFullyReadMarker(timeline.front()->id());
+
+ // Catch a case when the id in the last fully read marker or the local read
+ // receipt refers to an event that has just arrived. In this case either
+ // one (unreadStats) or both statistics should be recalculated to get
+ // an exact number instead of an estimation (see documentation on
+ // EventStats::isEstimate). For the same reason (switching from the
+ // estimate to the exact number) this branch forces returning
+ // Change::UnreadStats and also possibly Change::PartiallyReadStats, even if
+ // the estimation luckily matched the exact result.
+ if (readReceiptMarker < to || changes /*i.e. read receipt was corrected*/) {
+ unreadStats = EventStats::fromMarker(q, readReceiptMarker);
+ Q_ASSERT(!unreadStats.isEstimate);
+ qCDebug(MESSAGES).nospace() << "Recalculated unread event statistics in"
+ << q->objectName() << ": " << unreadStats;
+ changes |= Change::UnreadStats;
+ if (fullyReadMarker < to) {
+ // Add up to unreadStats instead of counting same events again
+ partiallyReadStats = EventStats::fromRange(q, readReceiptMarker,
+ q->fullyReadMarker(),
+ unreadStats);
+ Q_ASSERT(!partiallyReadStats.isEstimate);
+
+ qCDebug(MESSAGES).nospace()
+ << "Recalculated partially read event statistics in "
+ << q->objectName() << ": " << partiallyReadStats;
+ return changes | Change::PartiallyReadStats;
+ }
}
- Q_ASSERT(to <= readMarker);
-
- QElapsedTimer et;
- et.start();
- const auto newUnreadMessages =
- count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Counting gained unread messages took" << et;
-
- if (newUnreadMessages > 0) {
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages < 0)
- unreadMessages = 0;
-
- unreadMessages += newUnreadMessages;
- qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained"
- << newUnreadMessages << "unread message(s),"
- << (q->readMarker() == timeline.crend()
- ? "in total at least"
- : "in total")
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- }
-}
-
-Room::Changes Room::Private::promoteReadMarker(User* u,
- const rev_iter_t& newMarker,
- bool force)
-{
- Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr");
- Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend());
-
- const auto prevMarker = q->readMarker(u);
- if (!force && prevMarker <= newMarker) // Remember, we deal with reverse
- // iterators
- return Change::NoChange;
-
- Q_ASSERT(newMarker < timeline.crend());
-
- // Try to auto-promote the read marker over the user's own messages
- // (switch to direct iterators for that).
- auto eagerMarker =
- find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) {
- return ti->senderId() != u->id();
- });
+ // As of here, at least the fully read marker (but maybe also read receipt)
+ // points to somewhere beyond the "oldest" message from the arrived batch -
+ // add up newly arrived messages to the current stats, instead of a complete
+ // recalculation.
+ Q_ASSERT(fullyReadMarker >= to);
- auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id());
- if (isLocalUser(u)) {
- const auto oldUnreadCount = unreadMessages;
- QElapsedTimer et;
- et.start();
- unreadMessages =
- int(count_if(eagerMarker, timeline.cend(),
- [this](const auto& ti) { return isEventNotable(ti); }));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Recounting unread messages took" << et;
+ const auto newStats = EventStats::fromRange(q, from, to);
+ Q_ASSERT(!newStats.isEstimate);
+ if (newStats.empty())
+ return changes;
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages == 0)
- unreadMessages = -1;
+ const auto doAddStats = [this, &changes, newStats](EventStats& s,
+ const rev_iter_t& marker,
+ Change c) {
+ s.notableCount += newStats.notableCount;
+ s.highlightCount += newStats.highlightCount;
+ if (!s.isEstimate)
+ s.isEstimate = marker == historyEdge();
+ changes |= c;
+ };
- if (force || unreadMessages != oldUnreadCount) {
- if (unreadMessages == -1) {
- qCDebug(MESSAGES)
- << "Room" << displayname << "has no more unread messages";
- } else
- qCDebug(MESSAGES) << "Room" << displayname << "still has"
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- changes |= Change::UnreadNotifsChange;
- }
+ doAddStats(partiallyReadStats, fullyReadMarker, Change::PartiallyReadStats);
+ if (readReceiptMarker >= to) {
+ // readReceiptMarker < to branch shouldn't have been entered
+ Q_ASSERT(!changes.testFlag(Change::UnreadStats));
+ doAddStats(unreadStats, readReceiptMarker, Change::UnreadStats);
}
+ qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newStats
+ << "notable/highlighted event(s); total statistics:"
+ << partiallyReadStats << "since the fully read marker,"
+ << unreadStats << "since read receipt";
+
+ // Check invariants
+ Q_ASSERT(partiallyReadStats.isValidFor(q, fullyReadMarker));
+ Q_ASSERT(unreadStats.isValidFor(q, readReceiptMarker));
return changes;
}
-Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
+Room::Changes Room::Private::setFullyReadMarker(const QString& eventId)
{
- const auto prevMarker = q->readMarker();
- auto changes = promoteReadMarker(q->localUser(), upToMarker);
- if (prevMarker != upToMarker)
- qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker();
+ if (fullyReadUntilEventId == eventId)
+ return Change::None;
- // We shouldn't send read receipts for the local user's own messages - so
- // search earlier messages for the latest message not from the local user
- // until the previous last-read message, whichever comes first.
- for (; upToMarker < prevMarker; ++upToMarker) {
- if ((*upToMarker)->senderId() != q->localUser()->id()) {
- connection->callApi<PostReceiptJob>(BackgroundRequest,
- id, QStringLiteral("m.read"),
- QUrl::toPercentEncoding(
- (*upToMarker)->id()));
- break;
+ const auto prevReadMarker = q->fullyReadMarker();
+ const auto newReadMarker = q->findInTimeline(eventId);
+ if (newReadMarker > prevReadMarker)
+ return Change::None;
+
+ const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId);
+ qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() //
+ << "set to" << fullyReadUntilEventId;
+
+ QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;)
+ if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) {
+ // Pull read receipt if it's behind, and update statistics
+ changes |= setLastReadReceipt(connection->userId(), rm);
+ if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) {
+ changes |= Change::PartiallyReadStats;
+ qCDebug(MESSAGES)
+ << "Updated partially read event statistics in"
+ << q->objectName()
+ << "after moving m.fully_read marker: " << partiallyReadStats;
}
+ Q_ASSERT(partiallyReadStats.isValidFor(q, rm)); // post-check
}
+ emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
+ // TODO: Remove in 0.8
+ emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
return changes;
}
-void Room::markMessagesAsRead(QString uptoEventId)
+void Room::setReadReceipt(const QString& atEventId)
+{
+ if (const auto changes = d->setLastReadReceipt(localUser()->id(),
+ historyEdge(),
+ { atEventId })) {
+ connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
+ QStringLiteral("m.read"),
+ QUrl::toPercentEncoding(atEventId));
+ d->postprocessChanges(changes);
+ } else
+ qCDebug(EPHEMERAL) << "The new read receipt for" << localUser()->id()
+ << "in" << objectName()
+ << "is at or behind the old one, skipping";
+}
+
+bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker)
+{
+ if (upToMarker == q->historyEdge())
+ qCWarning(MESSAGES) << "Cannot mark an unknown event in"
+ << q->objectName() << "as fully read";
+ else if (const auto changes = setFullyReadMarker(upToMarker->event()->id())) {
+ // The assumption below is that if a read receipt was sent on a newer
+ // event, the homeserver will keep it there instead of reverting to
+ // m.fully_read
+ connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
+ fullyReadUntilEventId,
+ fullyReadUntilEventId);
+ postprocessChanges(changes);
+ return true;
+ } else
+ qCDebug(MESSAGES) << "Event" << *upToMarker << "in" << q->objectName()
+ << "is behind the current fully read marker at"
+ << *q->fullyReadMarker()
+ << "- won't move fully read marker back in timeline";
+ return false;
+}
+
+void Room::markMessagesAsRead(const QString& uptoEventId)
{
d->markMessagesAsRead(findInTimeline(uptoEventId));
}
void Room::markAllMessagesAsRead()
{
- if (!d->timeline.empty())
- d->markMessagesAsRead(d->timeline.crbegin());
+ d->markMessagesAsRead(d->timeline.crbegin());
}
bool Room::canSwitchVersions() const
@@ -789,18 +910,44 @@ bool Room::canSwitchVersions() const
return true;
}
-bool Room::hasUnreadMessages() const { return unreadCount() >= 0; }
+bool Room::isEventNotable(const TimelineItem &ti) const
+{
+ const auto& evt = *ti;
+ const auto* rme = ti.viewAs<RoomMessageEvent>();
+ return !evt.isRedacted()
+ && (is<RoomTopicEvent>(evt) || is<RoomNameEvent>(evt)
+ || is<RoomAvatarEvent>(evt) || is<RoomTombstoneEvent>(evt)
+ || (rme && rme->msgtype() != MessageEventType::Notice
+ && rme->replacedEvent().isEmpty()))
+ && evt.senderId() != localUser()->id();
+}
+
+Notification Room::notificationFor(const TimelineItem &ti) const
+{
+ return d->notifications.value(ti->id());
+}
-int Room::unreadCount() const { return d->unreadMessages; }
+Notification Room::checkForNotifications(const TimelineItem &ti)
+{
+ return { Notification::None };
+}
-Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); }
+bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); }
-Room::Timeline::const_iterator Room::syncEdge() const
+int countFromStats(const EventStats& s)
{
- return d->timeline.cend();
+ return s.empty() ? -1 : int(s.notableCount);
}
-Room::rev_iter_t Room::timelineEdge() const { return historyEdge(); }
+int Room::unreadCount() const { return countFromStats(partiallyReadStats()); }
+
+EventStats Room::partiallyReadStats() const { return d->partiallyReadStats; }
+
+EventStats Room::unreadStats() const { return d->unreadStats; }
+
+Room::rev_iter_t Room::historyEdge() const { return d->historyEdge(); }
+
+Room::Timeline::const_iterator Room::syncEdge() const { return d->syncEdge(); }
TimelineItem::index_t Room::minTimelineIndex() const
{
@@ -820,7 +967,7 @@ bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const
Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const
{
- return timelineEdge()
+ return historyEdge()
- (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0);
}
@@ -872,18 +1019,17 @@ void Room::Private::getAllMembers()
allMembersJob = connection->callApi<GetMembersByRoomJob>(
id, connection->nextBatchToken(), "join");
auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1;
- connect(allMembersJob, &BaseJob::success, q, [=] {
+ connect(allMembersJob, &BaseJob::success, q, [this, nextIndex] {
Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1);
auto roomChanges = updateStateFrom(allMembersJob->chunk());
// Replay member events that arrived after the point for which
// the full members list was requested.
if (!timeline.empty())
for (auto it = q->findInTimeline(nextIndex).base();
- it != timeline.cend(); ++it)
+ it != syncEdge(); ++it)
if (is<RoomMemberEvent>(**it))
roomChanges |= q->processStateEvent(**it);
- if (roomChanges & MembersChange)
- emit q->memberListChanged();
+ postprocessChanges(roomChanges);
emit q->allMembersLoaded();
});
}
@@ -897,11 +1043,8 @@ void Room::setDisplayed(bool displayed)
d->displayed = displayed;
emit displayedChanged(displayed);
- if (displayed) {
- resetHighlightCount();
- resetNotificationCount();
+ if (displayed)
d->getAllMembers();
- }
}
QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; }
@@ -916,6 +1059,11 @@ void Room::setFirstDisplayedEventId(const QString& eventId)
if (d->firstDisplayedEventId == eventId)
return;
+ if (!eventId.isEmpty() && findInTimeline(eventId) == historyEdge())
+ qCWarning(MESSAGES)
+ << eventId
+ << "is marked as first displayed but doesn't seem to be loaded";
+
d->firstDisplayedEventId = eventId;
emit firstDisplayedEventChanged();
}
@@ -938,6 +1086,12 @@ void Room::setLastDisplayedEventId(const QString& eventId)
if (d->lastDisplayedEventId == eventId)
return;
+ const auto marker = findInTimeline(eventId);
+ if (!eventId.isEmpty() && marker == historyEdge())
+ qCWarning(MESSAGES)
+ << eventId
+ << "is marked as last displayed but doesn't seem to be loaded";
+
d->lastDisplayedEventId = eventId;
emit lastDisplayedEventChanged();
}
@@ -951,38 +1105,70 @@ void Room::setLastDisplayedEvent(TimelineItem::index_t index)
Room::rev_iter_t Room::readMarker(const User* user) const
{
Q_ASSERT(user);
- return findInTimeline(d->lastReadEventIds.value(user));
+ return findInTimeline(lastReadReceipt(user->id()).eventId);
}
-Room::rev_iter_t Room::readMarker() const { return readMarker(localUser()); }
+Room::rev_iter_t Room::readMarker() const { return fullyReadMarker(); }
+
+QString Room::readMarkerEventId() const { return lastFullyReadEventId(); }
+
+ReadReceipt Room::lastReadReceipt(const QString& userId) const
+{
+ return d->lastReadReceipts.value(userId);
+}
-QString Room::readMarkerEventId() const
+ReadReceipt Room::lastLocalReadReceipt() const
{
- return d->lastReadEventIds.value(localUser());
+ return d->lastReadReceipts.value(localUser()->id());
}
-QList<User*> Room::usersAtEventId(const QString& eventId)
+Room::rev_iter_t Room::localReadReceiptMarker() const
{
- return d->eventIdReadUsers.values(eventId);
+ return findInTimeline(lastLocalReadReceipt().eventId);
}
-int Room::notificationCount() const { return d->notificationCount; }
+QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; }
+
+Room::rev_iter_t Room::fullyReadMarker() const
+{
+ return findInTimeline(d->fullyReadUntilEventId);
+}
+
+QSet<QString> Room::userIdsAtEvent(const QString& eventId)
+{
+ return d->eventIdReadUsers.value(eventId);
+}
+
+QSet<User*> Room::usersAtEventId(const QString& eventId)
+{
+ const auto& userIds = d->eventIdReadUsers.value(eventId);
+ QSet<User*> users;
+ users.reserve(userIds.size());
+ for (const auto& uId : userIds)
+ users.insert(user(uId));
+ return users;
+}
+
+qsizetype Room::notificationCount() const
+{
+ return d->unreadStats.notableCount;
+}
void Room::resetNotificationCount()
{
- if (d->notificationCount == 0)
+ if (d->unreadStats.notableCount == 0)
return;
- d->notificationCount = 0;
+ d->unreadStats.notableCount = 0;
emit notificationCountChanged();
}
-int Room::highlightCount() const { return d->highlightCount; }
+qsizetype Room::highlightCount() const { return d->serverHighlightCount; }
void Room::resetHighlightCount()
{
- if (d->highlightCount == 0)
+ if (d->serverHighlightCount == 0)
return;
- d->highlightCount = 0;
+ d->serverHighlightCount = 0;
emit highlightCountChanged();
}
@@ -1120,6 +1306,17 @@ QList<User*> Room::directChatUsers() const
return connection()->directChatUsers(this);
}
+QUrl Room::makeMediaUrl(const QString& eventId, const QUrl& mxcUrl) const
+{
+ auto url = connection()->makeMediaUrl(mxcUrl);
+ QUrlQuery q(url.query());
+ Q_ASSERT(q.hasQueryItem("user_id"));
+ q.addQueryItem("room_id", id());
+ q.addQueryItem("event_id", eventId);
+ url.setQuery(q);
+ return url;
+}
+
QString safeFileName(QString rawName)
{
return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_");
@@ -1173,9 +1370,8 @@ QUrl Room::urlToThumbnail(const QString& eventId) const
if (event->hasThumbnail()) {
auto* thumbnail = event->content()->thumbnailInfo();
Q_ASSERT(thumbnail != nullptr);
- return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(),
- thumbnail->url,
- thumbnail->imageSize);
+ return connection()->getUrlForApi<MediaThumbnailJob>(
+ thumbnail->url, thumbnail->imageSize);
}
qCDebug(MAIN) << "Event" << eventId << "has no thumbnail";
return {};
@@ -1186,8 +1382,7 @@ QUrl Room::urlToDownload(const QString& eventId) const
if (auto* event = d->getEventWithFile(eventId)) {
auto* fileInfo = event->content()->fileInfo();
Q_ASSERT(fileInfo != nullptr);
- return DownloadFileJob::makeRequestUrl(connection()->homeserver(),
- fileInfo->url);
+ return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url);
}
return {};
}
@@ -1250,7 +1445,7 @@ QList<User*> Room::membersLeft() const { return d->membersLeft; }
QList<User*> Room::users() const { return d->membersMap.values(); }
-[[deprecated]] QStringList Room::memberNames() const
+QStringList Room::memberNames() const
{
return safeMemberNames();
}
@@ -1275,8 +1470,6 @@ QStringList Room::htmlSafeMemberNames() const
return res;
}
-int Room::memberCount() const { return d->membersMap.size(); }
-
int Room::timelineSize() const { return int(d->timeline.size()); }
bool Room::usesEncryption() const
@@ -1290,6 +1483,17 @@ 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
+{
+ return d->currentState;
+}
+
RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
{
#ifndef Quotient_E2EE_ENABLED
@@ -1353,11 +1557,10 @@ GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; }
Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
{
if (!summary.merge(newSummary))
- return Change::NoChange;
+ return Change::None;
qCDebug(STATE).nospace().noquote()
<< "Updated room summary for " << q->objectName() << ": " << summary;
- emit q->memberListChanged();
- return Change::SummaryChange;
+ return Change::Summary;
}
void Room::Private::insertMemberIntoMap(User* u)
@@ -1435,7 +1638,8 @@ void Room::Private::removeMemberFromMap(User* u)
inline auto makeErrorStr(const Event& e, QByteArray msg)
{
- return msg.append("; event dump follows:\n").append(e.originalJson());
+ return msg.append("; event dump follows:\n")
+ .append(QJsonDocument(e.fullJson()).toJson());
}
Room::Timeline::size_type
@@ -1461,11 +1665,12 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events,
!eventsIndex.contains(eId), __FUNCTION__,
makeErrorStr(*e, "Event is already in the timeline; "
"incoming events were not properly deduplicated"));
- if (placement == Older)
- timeline.emplace_front(move(e), --index);
- else
- timeline.emplace_back(move(e), ++index);
+ const auto& ti = placement == Older
+ ? timeline.emplace_front(move(e), --index)
+ : timeline.emplace_back(move(e), ++index);
eventsIndex.insert(eId, index);
+ if (auto n = q->checkForNotifications(ti); n.type != Notification::None)
+ notifications.insert(e->id(), n);
Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
}
const auto insertedSize = (index - baseIndex) * placement;
@@ -1523,7 +1728,7 @@ QString Room::disambiguatedMemberName(const QString& mxId) const
QString Room::safeMemberName(const QString& userId) const
{
- return sanitized(roomMembername(userId));
+ return sanitized(disambiguatedMemberName(userId));
}
QString Room::htmlSafeMemberName(const QString& userId) const
@@ -1540,63 +1745,132 @@ QUrl Room::memberAvatarUrl(const QString &mxId) const
: QUrl();
}
+Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data,
+ bool fromCache)
+{
+ Changes changes {};
+ if (fromCache) {
+ // Initial load of cached statistics
+ partiallyReadStats =
+ EventStats::fromCachedCounters(data.partiallyReadCount);
+ unreadStats = EventStats::fromCachedCounters(data.unreadCount,
+ data.highlightCount);
+ // Migrate from lib 0.6: -1 in the old unread counter overrides 0
+ // (which loads to an estimate) in notification_count. Next caching will
+ // save -1 in both places, completing the migration.
+ if (data.unreadCount == 0 && data.partiallyReadCount == -1)
+ unreadStats.isEstimate = false;
+ changes |= Change::PartiallyReadStats | Change::UnreadStats;
+ qCDebug(MESSAGES) << "Loaded" << q->objectName()
+ << "event statistics from cache:" << partiallyReadStats
+ << "since m.fully_read," << unreadStats
+ << "since m.read";
+ } else if (timeline.empty()) {
+ // In absence of actual events use statistics from the homeserver
+ if (merge(unreadStats.notableCount, data.unreadCount))
+ changes |= Change::PartiallyReadStats;
+ if (merge(unreadStats.highlightCount, data.highlightCount))
+ changes |= Change::UnreadStats;
+ unreadStats.isEstimate = !data.unreadCount.has_value()
+ || *data.unreadCount > 0;
+ qCDebug(MESSAGES)
+ << "Using server-side unread event statistics while the"
+ << q->objectName() << "timeline is empty:" << unreadStats;
+ }
+ bool correctedStats = false;
+ if (unreadStats.highlightCount > partiallyReadStats.highlightCount) {
+ correctedStats = true;
+ partiallyReadStats.highlightCount = unreadStats.highlightCount;
+ partiallyReadStats.isEstimate |= unreadStats.isEstimate;
+ }
+ if (unreadStats.notableCount > partiallyReadStats.notableCount) {
+ correctedStats = true;
+ partiallyReadStats.notableCount = unreadStats.notableCount;
+ partiallyReadStats.isEstimate |= unreadStats.isEstimate;
+ }
+ if (!unreadStats.isEstimate && partiallyReadStats.isEstimate) {
+ correctedStats = true;
+ partiallyReadStats.isEstimate = true;
+ }
+ if (correctedStats)
+ qCDebug(MESSAGES) << "Partially read event statistics in"
+ << q->objectName() << "were adjusted to"
+ << partiallyReadStats
+ << "to be consistent with the m.read receipt";
+ Q_ASSERT(partiallyReadStats.isValidFor(q, q->fullyReadMarker()));
+ Q_ASSERT(unreadStats.isValidFor(q, q->localReadReceiptMarker()));
+
+ // TODO: Once the library learns to count highlights, drop
+ // serverHighlightCount and only use the server-side counter when
+ // the timeline is empty (see the code above).
+ if (merge(serverHighlightCount, data.highlightCount)) {
+ qCDebug(MESSAGES) << "Updated highlights number in" << q->objectName()
+ << "to" << serverHighlightCount;
+ changes |= Change::Highlights;
+ }
+ return changes;
+}
+
void Room::updateData(SyncRoomData&& data, bool fromCache)
{
if (d->prevBatch.isEmpty())
d->prevBatch = data.timelinePrevBatch;
setJoinState(data.joinState);
- Changes roomChanges = Change::NoChange;
- QElapsedTimer et;
- et.start();
+ Changes roomChanges {};
+ // The order of calculation is important - don't merge the lines!
+ roomChanges |= d->updateStateFrom(data.state);
+ roomChanges |= d->setSummary(move(data.summary));
+ roomChanges |= d->addNewMessageEvents(move(data.timeline));
+
+ for (auto&& ephemeralEvent : data.ephemeral)
+ roomChanges |= processEphemeralEvent(move(ephemeralEvent));
+
for (auto&& event : data.accountData)
roomChanges |= processAccountDataEvent(move(event));
- roomChanges |= d->updateStateFrom(data.state);
+ roomChanges |= d->updateStatsFromSyncData(data, fromCache);
- if (!data.timeline.empty()) {
- et.restart();
- roomChanges |= d->addNewMessageEvents(move(data.timeline));
- if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "*** Room::addNewMessageEvents():" << data.timeline.size()
- << "event(s)," << et;
- }
- if (roomChanges & TopicChange)
+ if (roomChanges & Change::Topic)
emit topicChanged();
- if (roomChanges & (NameChange | AliasesChange))
+ if (roomChanges & (Change::Name | Change::Aliases))
emit namesChanged(this);
- if (roomChanges & MembersChange)
- emit memberListChanged();
+ d->postprocessChanges(roomChanges, !fromCache);
+}
- roomChanges |= d->setSummary(move(data.summary));
+void Room::Private::postprocessChanges(Changes changes, bool saveState)
+{
+ if (!changes)
+ return;
- for (auto&& ephemeralEvent : data.ephemeral)
- roomChanges |= processEphemeralEvent(move(ephemeralEvent));
+ if (changes & Change::Members)
+ emit q->memberListChanged();
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) {
- qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount;
- d->unreadMessages = data.unreadCount;
- emit unreadMessagesChanged(this);
- }
+ if (changes
+ & (Change::Name | Change::Aliases | Change::Members | Change::Summary))
+ updateDisplayname();
- if (data.highlightCount != d->highlightCount) {
- d->highlightCount = data.highlightCount;
- emit highlightCountChanged();
- }
- if (data.notificationCount != d->notificationCount) {
- d->notificationCount = data.notificationCount;
- emit notificationCountChanged();
- }
- if (roomChanges != Change::NoChange) {
- d->updateDisplayname();
- emit changed(roomChanges);
- if (!fromCache)
- connection()->saveRoomState(this);
+ if (changes & Change::PartiallyReadStats) {
+ emit q->unreadMessagesChanged(q); // TODO: remove in 0.8
+ emit q->partiallyReadStatsChanged();
}
+
+ if (changes & Change::UnreadStats)
+ emit q->unreadStatsChanged();
+
+ if (changes & Change::Highlights)
+ emit q->highlightCountChanged();
+
+ qCDebug(MAIN) << terse << changes << "= hex" <<
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ Qt::
+#endif
+ hex << uint(changes) << "in" << q->objectName();
+ emit q->changed(changes);
+ if (saveState)
+ connection->saveRoomState(q);
}
RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
@@ -1716,6 +1990,12 @@ QString Room::retryMessage(const QString& txnId)
return d->doSendEvent(it->event());
}
+// Lambda defers actual tr() invocation to the moment when translations are
+// initialised
+const auto FileTransferCancelledMsg = [] {
+ return Room::tr("File transfer cancelled");
+};
+
void Room::discardMessage(const QString& txnId)
{
auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
@@ -1730,7 +2010,7 @@ void Room::discardMessage(const QString& txnId)
if (isJobPending(transferIt->job)) {
transferIt->status = FileTransferInfo::Cancelled;
transferIt->job->abandon();
- emit fileTransferFailed(txnId, tr("File upload cancelled"));
+ emit fileTransferFailed(txnId, FileTransferCancelledMsg());
} else if (transferIt->status == FileTransferInfo::Completed) {
qCWarning(MAIN)
<< "File for transaction" << txnId
@@ -1770,57 +2050,80 @@ QString Room::postReaction(const QString& eventId, const QString& key)
return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key));
}
-QString Room::postFile(const QString& plainText, const QUrl& localPath,
- bool asGenericFile)
+QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl)
{
- QFileInfo localFile { localPath.toLocalFile() };
- Q_ASSERT(localFile.isFile());
-
- const auto txnId =
- d->addAsPending(
- makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile))
- ->transactionId();
+ const auto txnId = addAsPending(move(msgEvent))->transactionId();
// Remote URL will only be known after upload; fill in the local path
// to enable the preview while the event is pending.
- uploadFile(txnId, localPath);
+ q->uploadFile(txnId, localUrl);
// Below, the upload job is used as a context object to clean up connections
- const auto& transferJob = d->fileTransfers.value(txnId).job;
- connect(this, &Room::fileTransferCompleted, transferJob,
- [this, txnId](const QString& id, const QUrl&, const QUrl& mxcUri) {
- if (id == txnId) {
- auto it = findPendingEvent(txnId);
- if (it != d->unsyncedEvents.end()) {
- it->setFileUploaded(mxcUri);
- emit pendingEventChanged(
- int(it - d->unsyncedEvents.begin()));
- d->doSendEvent(it->get());
- } else {
- // Normally in this situation we should instruct
- // the media server to delete the file; alas, there's no
- // API specced for that.
- qCWarning(MAIN) << "File uploaded to" << mxcUri
- << "but the event referring to it was "
- "cancelled";
- }
+ const auto& transferJob = fileTransfers.value(txnId).job;
+ connect(q, &Room::fileTransferCompleted, transferJob,
+ [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri) {
+ if (tId != txnId)
+ return;
+
+ const auto it = q->findPendingEvent(txnId);
+ if (it != unsyncedEvents.end()) {
+ it->setFileUploaded(mxcUri);
+ emit q->pendingEventChanged(
+ int(it - unsyncedEvents.begin()));
+ doSendEvent(it->get());
+ } else {
+ // Normally in this situation we should instruct
+ // the media server to delete the file; alas, there's no
+ // API specced for that.
+ qCWarning(MAIN) << "File uploaded to" << mxcUri
+ << "but the event referring to it was "
+ "cancelled";
}
});
- connect(this, &Room::fileTransferCancelled, transferJob,
- [this, txnId](const QString& id) {
- if (id == txnId) {
- auto it = findPendingEvent(txnId);
- if (it != d->unsyncedEvents.end()) {
- const auto idx = int(it - d->unsyncedEvents.begin());
- emit pendingEventAboutToDiscard(idx);
- // See #286 on why iterator may not be valid here.
- d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx);
- emit pendingEventDiscarded();
- }
- }
+ connect(q, &Room::fileTransferFailed, transferJob,
+ [this, txnId](const QString& tId) {
+ if (tId != txnId)
+ return;
+
+ const auto it = q->findPendingEvent(txnId);
+ if (it == unsyncedEvents.end())
+ return;
+
+ const auto idx = int(it - unsyncedEvents.begin());
+ emit q->pendingEventAboutToDiscard(idx);
+ // See #286 on why `it` may not be valid here.
+ unsyncedEvents.erase(unsyncedEvents.begin() + idx);
+ emit q->pendingEventDiscarded();
});
return txnId;
}
+QString Room::postFile(const QString& plainText,
+ EventContent::TypedBase* content)
+{
+ Q_ASSERT(content != nullptr && content->fileInfo() != nullptr);
+ const auto* const fileInfo = content->fileInfo();
+ Q_ASSERT(fileInfo != nullptr);
+ QFileInfo localFile { fileInfo->url.toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+
+ return d->doPostFile(
+ makeEvent<RoomMessageEvent>(
+ plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content),
+ fileInfo->url);
+}
+
+#if QT_VERSION_MAJOR < 6
+QString Room::postFile(const QString& plainText, const QUrl& localPath,
+ bool asGenericFile)
+{
+ QFileInfo localFile { localPath.toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+ return d->doPostFile(makeEvent<RoomMessageEvent>(plainText, localFile,
+ asGenericFile),
+ localPath);
+}
+#endif
+
QString Room::postEvent(RoomEvent* event)
{
return d->sendEvent(RoomEventPtr(event));
@@ -1943,7 +2246,7 @@ void Room::Private::getPreviousContent(int limit, const QString &filter)
eventsHistoryJob =
connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit, filter);
emit q->eventsHistoryJobChanged();
- connect(eventsHistoryJob, &BaseJob::success, q, [=] {
+ connect(eventsHistoryJob, &BaseJob::success, q, [this] {
prevBatch = eventsHistoryJob->end();
addHistoricalMessageEvents(eventsHistoryJob->chunk());
});
@@ -2068,16 +2371,16 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
void Room::cancelFileTransfer(const QString& id)
{
- const auto it = d->fileTransfers.constFind(id);
- if (it == d->fileTransfers.cend()) {
+ const auto it = d->fileTransfers.find(id);
+ if (it == d->fileTransfers.end()) {
qCWarning(MAIN) << "No information on file transfer" << id << "in room"
<< d->id;
return;
}
if (isJobPending(it->job))
it->job->abandon();
- d->fileTransfers.remove(id);
- emit fileTransferCancelled(id);
+ it->status = FileTransferInfo::Cancelled;
+ emit fileTransferFailed(id, FileTransferCancelledMsg());
}
void Room::Private::dropDuplicateEvents(RoomEvents& events) const
@@ -2114,7 +2417,7 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const
RoomEventPtr makeRedacted(const RoomEvent& target,
const RedactionEvent& redaction)
{
- auto originalJson = target.originalJsonObject();
+ auto originalJson = target.fullJson();
// clang-format off
static const QStringList keepKeys {
EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey,
@@ -2125,15 +2428,14 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
QStringLiteral("membership") };
// clang-format on
- std::vector<std::pair<event_type_t, QStringList>> keepContentKeysMap {
+ static const std::pair<event_type_t, QStringList> keepContentKeysMap[] {
{ RoomMemberEvent::typeId(), { QStringLiteral("membership") } },
{ RoomCreateEvent::typeId(), { QStringLiteral("creator") } },
{ RoomPowerLevelsEvent::typeId(),
{ QStringLiteral("ban"), QStringLiteral("events"),
QStringLiteral("events_default"), QStringLiteral("kick"),
QStringLiteral("redact"), QStringLiteral("state_default"),
- QStringLiteral("users"), QStringLiteral("users_default") } },
- { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } }
+ QStringLiteral("users"), QStringLiteral("users_default") } }
// , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } }
// , { RoomHistoryVisibility::typeId(),
// { QStringLiteral("history_visibility") } }
@@ -2145,9 +2447,9 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
++it;
}
auto keepContentKeys =
- find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(),
+ find_if(begin(keepContentKeysMap), end(keepContentKeysMap),
[&target](const auto& t) { return target.type() == t.first; });
- if (keepContentKeys == keepContentKeysMap.end()) {
+ if (keepContentKeys == end(keepContentKeysMap)) {
originalJson.remove(ContentKeyL);
originalJson.remove(PrevContentKeyL);
} else {
@@ -2161,7 +2463,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
originalJson.insert(ContentKey, content);
}
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
- unsignedData[RedactedCauseKeyL] = redaction.originalJsonObject();
+ unsignedData[RedactedCauseKeyL] = redaction.fullJson();
originalJson.insert(QStringLiteral("unsigned"), unsignedData);
return loadEvent<RoomEvent>(originalJson);
@@ -2226,8 +2528,13 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
RoomEventPtr makeReplaced(const RoomEvent& target,
const RoomMessageEvent& replacement)
{
- auto originalJson = target.originalJsonObject();
- originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls);
+ const auto& targetReply = target.contentPart<QJsonObject>("m.relates_to");
+ auto newContent = replacement.contentPart<QJsonObject>("m.new_content"_ls);
+ if (!targetReply.empty()) {
+ newContent["m.relates_to"] = targetReply;
+ }
+ auto originalJson = target.fullJson();
+ originalJson[ContentKeyL] = newContent;
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
auto relations = unsignedData.take("m.relations"_ls).toObject();
@@ -2287,8 +2594,10 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
{
dropDuplicateEvents(events);
if (events.empty())
- return Change::NoChange;
+ return Change::None;
+ QElapsedTimer et;
+ et.start();
{
// Pre-process redactions and edits so that events that get
// redacted/replaced in the same batch landed in the timeline already
@@ -2338,7 +2647,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
// clients historically expect. This may eventually change though if we
// postulate that the current state is only current between syncs but not
// within a sync.
- Changes roomChanges = Change::NoChange;
+ Changes roomChanges {};
for (const auto& eptr : events)
roomChanges |= q->processStateEvent(*eptr);
@@ -2356,7 +2665,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
emit q->aboutToAddNewMessages(eventsSpan);
auto insertedSize = moveEventsToTimeline(eventsSpan, Newer);
totalInserted += insertedSize;
- auto firstInserted = timeline.cend() - insertedSize;
+ auto firstInserted = syncEdge() - insertedSize;
q->onAddNewTimelineEvents(firstInserted);
emit q->addedMessages(firstInserted->index(),
timeline.back().index());
@@ -2386,20 +2695,20 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx);
if (auto insertedSize = moveEventsToTimeline({ remoteEcho, it }, Newer)) {
totalInserted += insertedSize;
- q->onAddNewTimelineEvents(timeline.cend() - insertedSize);
+ q->onAddNewTimelineEvents(syncEdge() - insertedSize);
}
emit q->pendingEventMerged();
}
// Events merged and transferred from `events` to `timeline` now.
- const auto from = timeline.cend() - totalInserted;
+ const auto from = syncEdge() - totalInserted;
if (q->supportsCalls())
- for (auto it = from; it != timeline.cend(); ++it)
+ for (auto it = from; it != syncEdge(); ++it)
if (const auto* evt = it->viewAs<CallEventBase>())
emit q->callEvent(q, evt);
if (totalInserted > 0) {
- for (auto it = from; it != timeline.cend(); ++it) {
+ for (auto it = from; it != syncEdge(); ++it) {
if (const auto* reaction = it->viewAs<ReactionEvent>()) {
const auto& relation = reaction->relation();
relations[{ relation.eventId, relation.type }] << reaction;
@@ -2411,28 +2720,22 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
<< totalInserted << "new events; the last event is now"
<< timeline.back();
- // The first event in the just-added batch (referred to by `from`)
- // defines whose read marker can possibly be promoted any further over
- // the same author's events newly arrived. Others will need explicit
- // read receipts from the server (or, for the local user,
- // markMessagesAsRead() invocation) to promote their read markers over
- // the new message events.
- if (const auto senderId = (*from)->senderId(); !senderId.isEmpty()) {
- auto* const firstWriter = q->user(senderId);
- if (q->readMarker(firstWriter) != timeline.crend()) {
- roomChanges |=
- promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
- qCDebug(MESSAGES)
- << "Auto-promoted read marker for" << senderId
- << "to" << *q->readMarker(firstWriter);
- }
- }
+ roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from));
- updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
- roomChanges |= Change::UnreadNotifsChange;
+ // If the local user's message(s) is/are first in the batch
+ // and the fully read marker was right before it, promote
+ // the fully read marker to the same event as the read receipt.
+ const auto& firstWriterId = (*from)->senderId();
+ if (firstWriterId == connection->userId()
+ && q->fullyReadMarker().base() == from)
+ roomChanges |=
+ setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId);
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
+ if (totalInserted > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "Added" << totalInserted << "new event(s) to"
+ << q->objectName() << "in" << et;
return roomChanges;
}
@@ -2446,6 +2749,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
if (events.empty())
return;
+ Changes changes {};
// In case of lazy-loading new members may be loaded with historical
// messages. Also, the cache doesn't store events with empty content;
// so when such events show up in the timeline they should be properly
@@ -2454,39 +2758,40 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
const auto& e = *eptr;
if (e.isStateEvent()
&& !currentState.contains({ e.matrixType(), e.stateKey() })) {
- q->processStateEvent(e);
+ changes |= q->processStateEvent(e);
}
}
emit q->aboutToAddHistoricalMessages(events);
const auto insertedSize = moveEventsToTimeline(events, Older);
- const auto from = timeline.crend() - insertedSize;
+ const auto from = historyEdge() - insertedSize;
qCDebug(STATE) << "Room" << displayname << "received" << insertedSize
<< "past events; the oldest event is now" << timeline.front();
q->onAddHistoricalTimelineEvents(from);
emit q->addedMessages(timeline.front().index(), from->index());
- for (auto it = from; it != timeline.crend(); ++it) {
+ for (auto it = from; it != historyEdge(); ++it) {
if (const auto* reaction = it->viewAs<ReactionEvent>()) {
const auto& relation = reaction->relation();
relations[{ relation.eventId, relation.type }] << reaction;
emit q->updatedEvent(relation.eventId);
}
}
- if (from <= q->readMarker())
- updateUnreadCount(from, timeline.crend());
-
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():"
- << insertedSize << "event(s)," << et;
+ qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to"
+ << q->objectName() << "in" << et;
+
+ changes |= updateStats(from, historyEdge());
+ if (changes)
+ postprocessChanges(changes);
}
Room::Changes Room::processStateEvent(const RoomEvent& e)
{
if (!e.isStateEvent())
- return NoChange;
+ 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
@@ -2495,7 +2800,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
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);
@@ -2506,16 +2811,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return false; // Stay low and hope for the best...
}
const auto prevMembership = oldRme ? oldRme->membership()
- : MembershipType::Leave;
+ : Membership::Leave;
switch (prevMembership) {
- case MembershipType::Invite:
+ case Membership::Invite:
if (rme.membership() != prevMembership) {
d->usersInvited.removeOne(u);
Q_ASSERT(!d->usersInvited.contains(u));
}
break;
- case MembershipType::Join:
- if (rme.membership() == MembershipType::Join) {
+ case Membership::Join:
+ if (rme.membership() == Membership::Join) {
// rename/avatar change or no-op
if (rme.newDisplayName()) {
emit memberAboutToRename(u, *rme.newDisplayName());
@@ -2529,7 +2834,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return false;
}
} else {
- if (rme.membership() == MembershipType::Invite)
+ if (rme.membership() == Membership::Invite)
qCWarning(MAIN)
<< "Membership change from Join to Invite:" << rme;
// whatever the new membership, it's no more Join
@@ -2537,16 +2842,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
emit userRemoved(u);
}
break;
- case MembershipType::Ban:
- case MembershipType::Knock:
- case MembershipType::Leave:
- if (rme.membership() == MembershipType::Invite
- || rme.membership() == MembershipType::Join) {
+ case Membership::Ban:
+ case Membership::Knock:
+ case Membership::Leave:
+ if (rme.membership() == Membership::Invite
+ || rme.membership() == Membership::Join) {
d->membersLeft.removeOne(u);
Q_ASSERT(!d->membersLeft.contains(u));
}
break;
- case MembershipType::Undefined:
+ case Membership::Undefined:
; // A warning will be dropped in the post-processing block below
}
return true;
@@ -2573,8 +2878,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
}
, true); // By default, go forward with the state change
// clang-format on
- if (!proceed)
- return NoChange;
+ if (!proceed) {
+ if (!curStateEvent) // Remove the empty placeholder if one was created
+ d->currentState.remove({ e.matrixType(), e.stateKey() });
+ return Change::None;
+ }
// Change the state
const auto* const oldStateEvent =
@@ -2590,15 +2898,15 @@ 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 NameChange;
+ return Change::Name;
}
, [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) {
// 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();
@@ -2610,8 +2918,9 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
if (!cae.alias().isEmpty())
newAliases.push_front(cae.alias());
- connection()->updateRoomAliases(id(), previousAltAliases, newAliases);
- return AliasesChange;
+ connection()->updateRoomAliases(id(), previousAltAliases,
+ newAliases);
+ return Change::Aliases;
// clang-format off
}
, [this] (const RoomPinnedEvent&) {
@@ -2619,12 +2928,12 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return OtherChange;
}
, [] (const RoomTopicEvent&) {
- return TopicChange;
+ return Change::Topic;
}
, [this] (const RoomAvatarEvent& evt) {
if (d->avatar.updateUrl(evt.url()))
emit avatarChanged();
- return AvatarChange;
+ return Change::Avatar;
}
, [this,oldStateEvent] (const RoomMemberEvent& evt) {
// clang-format on
@@ -2633,10 +2942,10 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
static_cast<const RoomMemberEvent*>(oldStateEvent);
const auto prevMembership = oldMemberEvent
? oldMemberEvent->membership()
- : MembershipType::Leave;
+ : Membership::Leave;
switch (evt.membership()) {
- case MembershipType::Join:
- if (prevMembership != MembershipType::Join) {
+ case Membership::Join:
+ if (prevMembership != Membership::Join) {
d->insertMemberIntoMap(u);
emit userAdded(u);
} else {
@@ -2648,29 +2957,29 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
emit memberAvatarChanged(u);
}
break;
- case MembershipType::Invite:
+ case Membership::Invite:
if (!d->usersInvited.contains(u))
d->usersInvited.push_back(u);
if (u == localUser() && evt.isDirect())
connection()->addToDirectChats(this, user(evt.senderId()));
break;
- case MembershipType::Knock:
- case MembershipType::Ban:
- case MembershipType::Leave:
+ case Membership::Knock:
+ case Membership::Ban:
+ case Membership::Leave:
if (!d->membersLeft.contains(u))
d->membersLeft.append(u);
break;
- case MembershipType::Undefined:
+ case Membership::Undefined:
qCWarning(MEMBERS) << "Ignored undefined membership type";
}
- return MembersChange;
+ return Change::Members;
// clang-format off
}
, [this] (const EncryptionEvent&) {
// As encryption can only be switched on once, emit the signal here
// instead of aggregating and emitting in updateData()
emit encryption();
- return OtherChange;
+ return Change::Other;
}
, [this] (const RoomTombstoneEvent& evt) {
const auto successorId = evt.successorRoomId();
@@ -2686,30 +2995,31 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return true;
});
- return OtherChange;
+ return Change::Other;
// clang-format off
}
- , OtherChange);
+ , Change::Other);
// clang-format on
- Q_ASSERT(result != NoChange);
+ Q_ASSERT(result != Change::None);
return result;
}
Room::Changes Room::processEphemeralEvent(EventPtr&& event)
{
- Changes changes = NoChange;
+ Changes changes {};
QElapsedTimer et;
et.start();
if (auto* evt = eventCast<TypingEvent>(event)) {
d->usersTyping.clear();
- for (const auto& userId : evt->users()) {
- auto u = user(userId);
- if (memberJoinState(u) == JoinState::Join)
- d->usersTyping.append(u);
- }
+ d->usersTyping.reserve(evt->users().size()); // Assume all are members
+ for (const auto& userId : evt->users())
+ if (isMember(userId))
+ d->usersTyping.append(user(userId));
+
if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):"
- << evt->users().size() << "users," << et;
+ qCDebug(PROFILER)
+ << "Processing typing events from" << evt->users().size()
+ << "user(s) in" << objectName() << "took" << et;
emit typingChanged();
}
if (auto* evt = eventCast<ReceiptEvent>(event)) {
@@ -2717,67 +3027,54 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event)
const auto& eventsWithReceipts = evt->eventsWithReceipts();
for (const auto& p : eventsWithReceipts) {
totalReceipts += p.receipts.size();
- {
- if (p.receipts.size() == 1)
- qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for"
- << p.receipts[0].userId;
- else
- qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for"
- << p.receipts.size() << "users";
- }
const auto newMarker = findInTimeline(p.evtId);
- if (newMarker != timelineEdge()) {
- for (const Receipt& r : p.receipts) {
- if (r.userId == connection()->userId())
- continue; // FIXME, #185
- auto u = user(r.userId);
- if (memberJoinState(u) == JoinState::Join)
- changes |= d->promoteReadMarker(u, newMarker);
- }
- } else {
- qCDebug(EPHEMERAL) << "Event" << p.evtId
- << "not found; saving read receipts anyway";
- // If the event is not found (most likely, because it's too old
- // and hasn't been fetched from the server yet), but there is
- // a previous marker for a user, keep the previous marker.
- // Otherwise, blindly store the event id for this user.
- for (const Receipt& r : p.receipts) {
- if (r.userId == connection()->userId())
- continue; // FIXME, #185
- auto u = user(r.userId);
- if (memberJoinState(u) == JoinState::Join
- && readMarker(u) == timelineEdge())
- changes |= d->setLastReadEvent(u, p.evtId);
- }
- }
+ if (newMarker == historyEdge())
+ qCDebug(EPHEMERAL)
+ << "Event" << p.evtId
+ << "is not found; saving read receipt(s) anyway";
+ // If the event is not found (most likely, because it's too old and
+ // hasn't been fetched from the server yet) but there is a previous
+ // marker for a user, keep the previous marker because read receipts
+ // are not supposed to move backwards. Otherwise, blindly store
+ // the event id for this user and update the read marker when/if
+ // the event is fetched later on.
+ const auto updatedCount = std::count_if(
+ p.receipts.cbegin(), p.receipts.cend(),
+ [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) {
+ const auto change =
+ d->setLastReadReceipt(r.userId, newMarker,
+ { evtId, r.timestamp });
+ changes |= change;
+ return change & Change::Any;
+ });
+
+ if (p.receipts.size() > 1)
+ qCDebug(EPHEMERAL) << p.evtId << "marked as read for"
+ << updatedCount << "user(s)";
+ if (updatedCount < p.receipts.size())
+ qCDebug(EPHEMERAL) << p.receipts.size() - updatedCount
+ << "receipts were skipped";
}
if (eventsWithReceipts.size() > 3 || totalReceipts > 10
|| et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "*** Room::processEphemeralEvent(receipts):"
- << eventsWithReceipts.size() << "event(s) with"
- << totalReceipts << "receipt(s)," << et;
+ qCDebug(PROFILER) << "Processing" << totalReceipts
+ << "receipt(s) on" << eventsWithReceipts.size()
+ << "event(s) in" << objectName() << "took" << et;
}
return changes;
}
Room::Changes Room::processAccountDataEvent(EventPtr&& event)
{
- Changes changes = NoChange;
+ Changes changes {};
if (auto* evt = eventCast<TagEvent>(event)) {
d->setTags(evt->tags());
- changes |= Change::TagsChange;
+ changes |= Change::Tags;
}
- if (auto* evt = eventCast<ReadMarkerEvent>(event)) {
- auto readEventId = evt->event_id();
- qCDebug(STATE) << "Server-side read marker at" << readEventId;
- d->serverReadMarker = readEventId;
- const auto newMarker = findInTimeline(readEventId);
- changes |= newMarker != timelineEdge()
- ? d->markMessagesAsRead(newMarker)
- : d->setLastReadEvent(localUser(), readEventId);
- }
+ if (auto* evt = eventCast<const ReadMarkerEvent>(event))
+ changes |= d->setFullyReadMarker(evt->event_id());
+
// For all account data events
auto& currentData = d->accountData[event->matrixType()];
// A polymorphic event-specific comparison might be a bit more
@@ -2788,9 +3085,12 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event)
qCDebug(STATE) << "Updated account data of type"
<< currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
- return Change::AccountDataChange;
+ // TODO: Drop AccountDataChange in 0.8
+ // NB: GCC (at least 10) only accepts QT_IGNORE_DEPRECATIONS around
+ // a statement, not within a statement
+ QT_IGNORE_DEPRECATIONS(changes |= Change::AccountData | Change::Other;)
}
- return Change::NoChange;
+ return changes;
}
template <typename ContT>
@@ -2958,19 +3258,28 @@ QJsonObject Room::Private::toJson() const
{ QStringLiteral("events"), accountDataEvents } });
}
- QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey,
- unreadMessages } };
-
- if (highlightCount > 0)
- unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount);
- if (notificationCount > 0)
- unreadNotifObj.insert(QStringLiteral("notification_count"),
- notificationCount);
-
- result.insert(QStringLiteral("unread_notifications"), unreadNotifObj);
+ if (const auto& readReceipt = q->lastReadReceipt(connection->userId());
+ !readReceipt.eventId.isEmpty()) //
+ {
+ result.insert(
+ QStringLiteral("ephemeral"),
+ QJsonObject {
+ { QStringLiteral("events"),
+ QJsonArray { ReceiptEvent({ { readReceipt.eventId,
+ { { connection->userId(),
+ readReceipt.timestamp } } } })
+ .fullJson() } } });
+ }
+
+ result.insert(UnreadNotificationsKey,
+ QJsonObject { { PartiallyReadCountKey,
+ countFromStats(partiallyReadStats) },
+ { HighlightCountKey, serverHighlightCount } });
+ result.insert(NewUnreadCountKey, countFromStats(unreadStats));
if (et.elapsed() > 30)
- qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et;
+ qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took"
+ << et;
return result;
}
@@ -2984,12 +3293,25 @@ bool MemberSorter::operator()(User* u1, User* u2) const
return operator()(u1, room->disambiguatedMemberName(u2->id()));
}
-bool MemberSorter::operator()(User* u1, const QString& u2name) const
+bool MemberSorter::operator()(User* u1, QStringView u2name) const
{
auto n1 = room->disambiguatedMemberName(u1->id());
if (n1.startsWith('@'))
n1.remove(0, 1);
- auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0);
+ const auto n2 = u2name.mid(u2name.startsWith('@') ? 1 : 0)
+#if QT_VERSION_MAJOR < 6
+ .toString() // Qt 5 doesn't have QStringView::localeAwareCompare
+#endif
+ ;
return n1.localeAwareCompare(n2) < 0;
}
+
+void Room::activateEncryption()
+{
+ if(usesEncryption()) {
+ qCWarning(E2EE) << "Room" << objectName() << "is already encrypted";
+ return;
+ }
+ setState<EncryptionEvent>(EncryptionEventContent::MegolmV1AesSha2);
+}
diff --git a/lib/room.h b/lib/room.h
index 23c0c846..cbe3d1ad 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -11,7 +11,7 @@
#include "connection.h"
#include "eventitem.h"
-#include "joinstate.h"
+#include "quotient_common.h"
#include "csapi/message_pagination.h"
@@ -71,6 +71,45 @@ public:
bool failed() const { return status == Failed; }
};
+//! \brief Data structure for a room member's read receipt
+//! \sa Room::lastReadReceipt
+class ReadReceipt {
+ Q_GADGET
+ Q_PROPERTY(QString eventId MEMBER eventId CONSTANT)
+ Q_PROPERTY(QDateTime timestamp MEMBER timestamp CONSTANT)
+public:
+ QString eventId;
+ QDateTime timestamp = {};
+
+ bool operator==(const ReadReceipt& other) const
+ {
+ return eventId == other.eventId && timestamp == other.timestamp;
+ }
+ bool operator!=(const ReadReceipt& other) const
+ {
+ return !operator==(other);
+ }
+};
+inline void swap(ReadReceipt& lhs, ReadReceipt& rhs)
+{
+ swap(lhs.eventId, rhs.eventId);
+ swap(lhs.timestamp, rhs.timestamp);
+}
+
+struct EventStats;
+
+struct Notification
+{
+ enum Type { None = 0, Basic, Highlight };
+ Q_ENUM(Notification)
+
+ Type type = None;
+
+private:
+ Q_GADGET
+ Q_PROPERTY(Type type MEMBER type CONSTANT)
+};
+
class Room : public QObject {
Q_OBJECT
Q_PROPERTY(Connection* connection READ connection CONSTANT)
@@ -87,6 +126,7 @@ class Room : public QObject {
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
STORED false)
@@ -95,7 +135,6 @@ class Room : public QObject {
Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages)
Q_PROPERTY(QStringList memberNames READ safeMemberNames NOTIFY memberListChanged)
- Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged)
Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged)
Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged)
Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged)
@@ -106,21 +145,28 @@ class Room : public QObject {
setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged)
Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE
setLastDisplayedEventId NOTIFY lastDisplayedEventChanged)
-
+ //! \deprecated since 0.7
Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE
markMessagesAsRead NOTIFY readMarkerMoved)
+ Q_PROPERTY(QString lastFullyReadEventId READ lastFullyReadEventId WRITE
+ markMessagesAsRead NOTIFY fullyReadMarkerMoved)
+ //! \deprecated since 0.7
Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY
- unreadMessagesChanged)
- Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged)
- Q_PROPERTY(int highlightCount READ highlightCount NOTIFY
- highlightCountChanged RESET resetHighlightCount)
- Q_PROPERTY(int notificationCount READ notificationCount NOTIFY
- notificationCountChanged RESET resetNotificationCount)
+ partiallyReadStatsChanged STORED false)
+ //! \deprecated since 0.7
+ Q_PROPERTY(int unreadCount READ unreadCount NOTIFY partiallyReadStatsChanged
+ STORED false)
+ Q_PROPERTY(qsizetype highlightCount READ highlightCount
+ NOTIFY highlightCountChanged)
+ Q_PROPERTY(qsizetype notificationCount READ notificationCount
+ NOTIFY notificationCountChanged)
+ Q_PROPERTY(EventStats partiallyReadStats READ partiallyReadStats NOTIFY partiallyReadStatsChanged)
+ Q_PROPERTY(EventStats unreadStats READ unreadStats NOTIFY unreadStatsChanged)
Q_PROPERTY(bool allHistoryLoaded READ allHistoryLoaded NOTIFY addedMessages
STORED false)
Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged)
- Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged)
- Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged)
+ Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged STORED false)
+ Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged STORED false)
Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY
eventsHistoryJobChanged)
@@ -132,26 +178,49 @@ public:
using rev_iter_t = Timeline::const_reverse_iterator;
using timeline_iter_t = Timeline::const_iterator;
- enum Change : uint {
- NoChange = 0x0,
- NameChange = 0x1,
- AliasesChange = 0x2,
- CanonicalAliasChange = AliasesChange,
- TopicChange = 0x4,
- UnreadNotifsChange = 0x8,
- AvatarChange = 0x10,
- JoinStateChange = 0x20,
- TagsChange = 0x40,
- MembersChange = 0x80,
- /* = 0x100, */
- AccountDataChange = 0x200,
- SummaryChange = 0x400,
- ReadMarkerChange = 0x800,
- OtherChange = 0x8000,
- AnyChange = 0xFFFF
+ //! \brief Room changes that can be tracked using Room::changed() signal
+ //!
+ //! This enumeration lists kinds of changes that can be tracked with
+ //! a "cumulative" changed() signal instead of using individual signals for
+ //! each change. Specific enumerators mention these individual signals.
+ //! \sa changed
+ enum class Change : uint {
+ None = 0x0, //< No changes occurred in the room
+ Name = 0x1, //< \sa namesChanged, displaynameChanged
+ Aliases = 0x2, //< \sa namesChanged, displaynameChanged
+ CanonicalAlias = Aliases,
+ Topic = 0x4, //< \sa topicChanged
+ PartiallyReadStats = 0x8, //< \sa partiallyReadStatsChanged
+ DECL_DEPRECATED_ENUMERATOR(UnreadNotifs, PartiallyReadStats),
+ Avatar = 0x10, //< \sa avatarChanged
+ JoinState = 0x20, //< \sa joinStateChanged
+ Tags = 0x40, //< \sa tagsChanged
+ //! \sa userAdded, userRemoved, memberRenamed, memberListChanged,
+ //! displaynameChanged
+ Members = 0x80,
+ UnreadStats = 0x100, //< \sa unreadStatsChanged
+ AccountData Q_DECL_ENUMERATOR_DEPRECATED_X(
+ "Change::AccountData will be merged into Change::Other in 0.8") =
+ 0x200,
+ Summary = 0x400, //< \sa summaryChanged, displaynameChanged
+ ReadMarker Q_DECL_ENUMERATOR_DEPRECATED_X(
+ "Change::ReadMarker will be merged into Change::Other in 0.8") =
+ 0x800,
+ Highlights = 0x1000, //< \sa highlightCountChanged
+ //! A catch-all value that covers changes not listed above (such as
+ //! encryption turned on or the room having been upgraded), as well as
+ //! changes in the room state that the library is not aware of (e.g.,
+ //! custom state events) and m.read/m.fully_read position changes.
+ //! \sa encryptionChanged, upgraded, accountDataChanged
+ Other = 0x8000,
+ //! This is intended to test a Change/Changes value for non-emptiness;
+ //! adding <tt>& Change::Any</tt> has the same meaning as
+ //! !testFlag(Change::None) or adding <tt>!= Change::None</tt>
+ //! \note testFlag(Change::Any) tests that _all_ bits are on and
+ //! will always return false.
+ Any = 0xFFFF
};
- Q_DECLARE_FLAGS(Changes, Change)
- Q_FLAG(Changes)
+ QUO_DECLARE_FLAGS(Changes, Change)
Room(Connection* connection, QString id, JoinState initialJoinState);
~Room() override;
@@ -181,21 +250,15 @@ public:
Room* successor(JoinStates statesFilter = JoinState::Invite
| JoinState::Join) const;
QString name() const;
- /// Room aliases defined on the current user's server
- /// \sa remoteAliases, setLocalAliases
- [[deprecated("Use aliases()")]]
- QStringList localAliases() const;
- /// Room aliases defined on other servers
- /// \sa localAliases
- [[deprecated("Use aliases()")]]
- QStringList remoteAliases() const;
QString canonicalAlias() const;
QStringList altAliases() const;
+ //! 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;
QUrl avatarUrl() const;
@@ -205,12 +268,10 @@ public:
QList<User*> membersLeft() const;
Q_INVOKABLE QList<Quotient::User*> users() const;
- [[deprecated("Use safeMemberNames() or htmlSafeMemberNames() instead")]]
+ Q_DECL_DEPRECATED_X("Use safeMemberNames() or htmlSafeMemberNames() instead") //
QStringList memberNames() const;
QStringList safeMemberNames() const;
QStringList htmlSafeMemberNames() const;
- [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]]
- int memberCount() const;
int timelineSize() const;
bool usesEncryption() const;
RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent);
@@ -255,24 +316,27 @@ public:
*
* \return Join if the user is a room member; Leave otherwise
*/
+ Q_DECL_DEPRECATED_X("Use isMember() instead")
Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const;
+ //! \brief Check the join state of a given user in this room
+ //!
+ //! \return the given user's state with respect to the room
+ Q_INVOKABLE Quotient::Membership memberState(const QString& userId) const;
+
+ //! Check whether a user with the given id is a member of the room
+ Q_INVOKABLE bool isMember(const QString& userId) const;
+
//! \brief Get a display name (without disambiguation) for the given member
//!
//! \sa safeMemberName, htmlSafeMemberName
Q_INVOKABLE QString memberName(const QString& mxId) const;
- /*!
- * \brief Get a disambiguated name for the given user in the room context
- *
- * \deprecated use safeMemberName() instead
- */
+ //! \brief Get a disambiguated name for the given user in the room context
+ Q_DECL_DEPRECATED_X("Use safeMemberName() instead")
Q_INVOKABLE QString roomMembername(const Quotient::User* u) const;
- /*!
- * \brief Get a disambiguated name for a user with this id in the room context
- *
- * \deprecated use safeMemberName() instead
- */
+ //! \brief Get a disambiguated name for a user with this id in the room
+ Q_DECL_DEPRECATED_X("Use safeMemberName() instead")
Q_INVOKABLE QString roomMembername(const QString& userId) const;
/*!
@@ -319,8 +383,6 @@ public:
* arrived event; same as messageEvents().cend()
*/
Timeline::const_iterator syncEdge() const;
- /// \deprecated Use historyEdge instead
- rev_iter_t timelineEdge() const;
Q_INVOKABLE Quotient::TimelineItem::index_t minTimelineIndex() const;
Q_INVOKABLE Quotient::TimelineItem::index_t maxTimelineIndex() const;
Q_INVOKABLE bool
@@ -337,9 +399,13 @@ public:
const char* relType) const;
const RoomCreateEvent* creation() const
- { return getCurrentState<RoomCreateEvent>(); }
+ {
+ return getCurrentState<RoomCreateEvent>();
+ }
const RoomTombstoneEvent* tombstone() const
- { return getCurrentState<RoomTombstoneEvent>(); }
+ {
+ return getCurrentState<RoomTombstoneEvent>();
+ }
bool displayed() const;
/// Mark the room as currently displayed to the user
@@ -359,44 +425,223 @@ public:
void setLastDisplayedEventId(const QString& eventId);
void setLastDisplayedEvent(TimelineItem::index_t index);
+ //! \brief Obtain a read receipt of any user
+ //! \deprecated Use lastReadReceipt or fullyReadMarker instead.
+ //!
+ //! Historically, readMarker was returning a "converged" read marker
+ //! representing both the read receipt and the fully read marker, as
+ //! Quotient managed them together. Since 0.6.8, a single-argument call of
+ //! readMarker returns the last read receipt position (for any room member)
+ //! and a call without arguments returns the last _fully read_ position,
+ //! to provide access to both positions separately while maintaining API
+ //! stability guarantees. 0.7 has separate methods to return read receipts
+ //! and the fully read marker - use them instead.
+ //! \sa lastReadReceipt
+ [[deprecated("Use lastReadReceipt() to get m.read receipt or"
+ " fullyReadMarker() to get m.fully_read marker")]] //
rev_iter_t readMarker(const User* user) const;
+ //! \brief Obtain the local user's fully-read marker
+ //! \deprecated Use fullyReadMarker instead
+ //!
+ //! See the documentation for the single-argument overload.
+ //! \sa fullyReadMarker
+ [[deprecated("Use localReadReceiptMarker() or fullyReadMarker()")]] //
rev_iter_t readMarker() const;
+ //! \brief Get the event id for the local user's fully-read marker
+ //! \deprecated Use lastFullyReadEventId instead
+ //!
+ //! See the readMarker documentation
+ [[deprecated("Use lastReadReceipt() to get m.read receipt or"
+ " lastFullyReadEventId() to get an event id that"
+ " m.fully_read marker points to")]] //
QString readMarkerEventId() const;
- QList<User*> usersAtEventId(const QString& eventId);
- /**
- * \brief Mark the event with uptoEventId as read
- *
- * Finds in the timeline and marks as read the event with
- * the specified id; also posts a read receipt to the server either
- * for this message or, if it's from the local user, for
- * the nearest non-local message before. uptoEventId must be non-empty.
- */
- void markMessagesAsRead(QString uptoEventId);
- /// Check whether there are unread messages in the room
+ //! \brief Get the latest read receipt from a user
+ //!
+ //! The user id must be valid. A read receipt with an empty event id
+ //! is returned if the user id is valid but there was no read receipt
+ //! from them.
+ //! \sa usersAtEventId
+ ReadReceipt lastReadReceipt(const QString& userId) const;
+
+ //! \brief Get the latest read receipt from the local user
+ //!
+ //! This is a shortcut for <tt>lastReadReceipt(localUserId)</tt>.
+ //! \sa lastReadReceipt
+ ReadReceipt lastLocalReadReceipt() const;
+
+ //! \brief Find the timeline item the local read receipt is at
+ //!
+ //! This is a shortcut for \code
+ //! room->findInTimeline(room->lastLocalReadReceipt().eventId);
+ //! \endcode
+ rev_iter_t localReadReceiptMarker() const;
+
+ //! \brief Get the latest event id marked as fully read
+ //!
+ //! This can be either the event id pointed to by the actual latest
+ //! m.fully_read event, or the latest event id marked locally as fully read
+ //! if markMessagesAsRead or markAllMessagesAsRead has been called and
+ //! the homeserver didn't return an updated m.fully_read event yet.
+ //! \sa markMessagesAsRead, markAllMessagesAsRead, fullyReadMarker
+ QString lastFullyReadEventId() const;
+
+ //! \brief Get the iterator to the latest timeline item marked as fully read
+ //!
+ //! This method calls findInTimeline on the result of lastFullyReadEventId.
+ //! If the fully read marker turns out to be outside the timeline (because
+ //! the event marked as fully read is too far back in the history) the
+ //! returned value will be equal to historyEdge.
+ //!
+ //! Be sure to read the caveats on iterators returned by findInTimeline.
+ //! \sa lastFullyReadEventId, findInTimeline
+ rev_iter_t fullyReadMarker() const;
+
+ //! \brief Get users whose latest read receipts point to the event
+ //!
+ //! This method is for cases when you need to show users who have read
+ //! an event. Calling it on inexistent or empty event id will return
+ //! an empty set.
+ //! \note The returned list may contain ids resolving to users that are
+ //! not loaded as room members yet (in particular, if members are not
+ //! yet lazy-loaded). For now this merely means that the user's
+ //! room-specific name and avatar will not be there; but generally
+ //! it's recommended to ensure that all room members are loaded
+ //! before operating on the result of this function.
+ //! \sa lastReadReceipt, allMembersLoaded
+ QSet<QString> userIdsAtEvent(const QString& eventId);
+
+ [[deprecated("Use userIdsAtEvent instead")]]
+ QSet<User*> usersAtEventId(const QString& eventId);
+
+ //! \brief Mark the event with uptoEventId as fully read
+ //!
+ //! Marks the event with the specified id as fully read locally and also
+ //! sends an update to m.fully_read account data to the server either
+ //! for this message or, if it's from the local user, for
+ //! the nearest non-local message before. uptoEventId must point to a known
+ //! event in the timeline; the method will do nothing if the event is behind
+ //! the current m.fully_read marker or is not loaded, to prevent
+ //! accidentally trying to move the marker back in the timeline.
+ //! \sa markAllMessagesAsRead, fullyReadMarker
+ Q_INVOKABLE void markMessagesAsRead(const QString& uptoEventId);
+
+ //! \brief Determine whether an event should be counted as unread
+ //!
+ //! The criteria of including an event in unread counters are described in
+ //! [MSC2654](https://github.com/matrix-org/matrix-doc/pull/2654); according
+ //! to these, the event should be counted as unread (or, in libQuotient
+ //! parlance, is "notable") if it is:
+ //! - either
+ //! - a message event that is not m.notice, or
+ //! - a state event with type being one of:
+ //! `m.room.topic`, `m.room.name`, `m.room.avatar`, `m.room.tombstone`;
+ //! - neither redacted, nor an edit (redactions cause the redacted event
+ //! to stop being notable, while edits are not notable themselves while
+ //! the original event usually is);
+ //! - from a non-local user (events from other devices of the local
+ //! user are not notable).
+ //! \sa partiallyReadStats, unreadStats
+ virtual bool isEventNotable(const TimelineItem& ti) const;
+
+ //! \brief Get notification details for an event
+ //!
+ //! This allows to get details on the kind of notification that should
+ //! generated for \p evt.
+ Notification notificationFor(const TimelineItem& ti) const;
+
+ //! \brief Get event statistics since the fully read marker
+ //!
+ //! This call returns a structure containing:
+ //! - the number of notable unread events since the fully read marker;
+ //! depending on the fully read marker state with respect to the local
+ //! timeline, this number may be either exact or estimated
+ //! (see EventStats::isEstimate);
+ //! - the number of highlights (TODO).
+ //!
+ //! Note that this is different from the unread count defined by MSC2654
+ //! and from the notification/highlight numbers defined by the spec in that
+ //! it counts events since the fully read marker, not since the last
+ //! read receipt position.
+ //!
+ //! As E2EE is not supported in the library, the returned result will always
+ //! be an estimate (<tt>isEstimate == true</tt>) for encrypted rooms;
+ //! moreover, since the library doesn't know how to tackle push rules yet
+ //! the number of highlights returned here will always be zero (there's no
+ //! good substitute for that now).
+ //!
+ //! \sa isEventNotable, fullyReadMarker, unreadStats, EventStats
+ EventStats partiallyReadStats() const;
+
+ //! \brief Get event statistics since the last read receipt
+ //!
+ //! This call returns a structure that contains the following three numbers,
+ //! all counted on the timeline segment between the event pointed to by
+ //! the m.fully_read marker and the sync edge:
+ //! - the number of unread events - depending on the read receipt state
+ //! with respect to the local timeline, this number may be either precise
+ //! or estimated (see EventStats::isEstimate);
+ //! - the number of highlights (TODO).
+ //!
+ //! As E2EE is not supported in the library, the returned result will always
+ //! be an estimate (<tt>isEstimate == true</tt>) for encrypted rooms;
+ //! moreover, since the library doesn't know how to tackle push rules yet
+ //! the number of highlights returned here will always be zero - use
+ //! highlightCount() for now.
+ //!
+ //! \sa isEventNotable, lastLocalReadReceipt, partiallyReadStats,
+ //! highlightCount
+ EventStats unreadStats() const;
+
+ [[deprecated(
+ "Use partiallyReadStats/unreadStats() and EventStats::empty()")]]
bool hasUnreadMessages() const;
- /** Get the number of unread messages in the room
- * Depending on the read marker state, this call may return either
- * a precise or an estimate number of unread events. Only "notable"
- * events (non-redacted message events from users other than local)
- * are counted.
- *
- * In a case when readMarker() == timelineEdge() (the local read
- * marker is beyond the local timeline) only the bottom limit of
- * the unread messages number can be estimated (and even that may
- * be slightly off due to, e.g., redactions of events not loaded
- * to the local timeline).
- *
- * If all messages are read, this function will return -1 (_not_ 0,
- * as zero may mean "zero or more unread messages" in a situation
- * when the read marker is outside the local timeline.
- */
+ //! \brief Get the number of notable events since the fully read marker
+ //!
+ //! \deprecated Since 0.7 there are two ways to count unread events: since
+ //! the fully read marker (used by libQuotient pre-0.7) and since the last
+ //! read receipt (as used by most of Matrix ecosystem, including the spec
+ //! and MSCs). This function currently returns a value derived from
+ //! partiallyReadStats() for compatibility with libQuotient 0.6; it will be
+ //! removed due to ambiguity. Use unreadStats() to obtain the spec-compliant
+ //! count of unread events and the highlight count; partiallyReadStats() to
+ //! obtain the unread events count since the fully read marker.
+ //!
+ //! \return -1 (_not 0_) when all messages are known to have been fully read,
+ //! i.e. the fully read marker points to _the latest notable_ event
+ //! loaded in the local timeline (which may be different from
+ //! the latest event in the local timeline as that might not be
+ //! notable);
+ //! 0 when there may be unread messages but the current local
+ //! timeline doesn't have any notable ones (often but not always
+ //! because it's entirely empty yet);
+ //! a positive integer when there is (or estimated to be) a number
+ //! of unread notable events as described above.
+ //!
+ //! \sa partiallyReadStats, unreadStats
+ [[deprecated("Use partiallyReadStats() or unreadStats() instead")]] //
int unreadCount() const;
- Q_INVOKABLE int notificationCount() const;
+ //! \brief Get the number of notifications since the last read receipt
+ //!
+ //! This is the same as <tt>unreadStats().notableCount</tt>.
+ //!
+ //! \sa unreadStats, lastLocalReadReceipt
+ qsizetype notificationCount() const;
+
+ //! \deprecated Use setReadReceipt() to drive changes in notification count
Q_INVOKABLE void resetNotificationCount();
- Q_INVOKABLE int highlightCount() const;
+
+ //! \brief Get the number of highlights since the last read receipt
+ //!
+ //! As of 0.7, this is defined by the homeserver as Quotient doesn't process
+ //! push rules.
+ //!
+ //! \sa unreadStats, lastLocalReadReceipt
+ qsizetype highlightCount() const;
+
+ //! \deprecated Use setReadReceipt() to drive changes in highlightCount
Q_INVOKABLE void resetHighlightCount();
/** Check whether the room has account data of the given type
@@ -466,6 +711,9 @@ public:
/// Get the list of users this room is a direct chat with
QList<User*> directChatUsers() const;
+ Q_INVOKABLE QUrl makeMediaUrl(const QString& eventId,
+ const QUrl &mxcUrl) const;
+
Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const;
Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const;
@@ -514,6 +762,19 @@ public:
Q_INVOKABLE 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.
@@ -554,8 +815,13 @@ public Q_SLOTS:
QString postHtmlText(const QString& plainText, const QString& html);
/// Send a reaction on a given event with a given key
QString postReaction(const QString& eventId, const QString& key);
+
+ QString postFile(const QString& plainText, EventContent::TypedBase* content);
+#if QT_VERSION_MAJOR < 6
+ Q_DECL_DEPRECATED_X("Use postFile(QString, MessageEventType, EventContent)") //
QString postFile(const QString& plainText, const QUrl& localPath,
bool asGenericFile = false);
+#endif
/** Post a pre-created room message event
*
* Takes ownership of the event, deleting it once the matching one
@@ -594,7 +860,12 @@ public Q_SLOTS:
void downloadFile(const QString& eventId, const QUrl& localFilename = {});
void cancelFileTransfer(const QString& id);
- /// Mark all messages in the room as read
+ //! \brief Set a given event as last read and post a read receipt on it
+ //!
+ //! Does nothing if the event is behind the current read receipt.
+ //! \sa lastReadReceipt, markMessagesAsRead, markAllMessagesAsRead
+ void setReadReceipt(const QString& atEventId);
+ //! Put the fully-read marker at the latest message in the room
void markAllMessagesAsRead();
/// Switch the room's version (aka upgrade)
@@ -608,6 +879,12 @@ public Q_SLOTS:
void answerCall(const QString& callId, const QString& sdp);
void hangupCall(const QString& callId);
+ /**
+ * Activates encryption for this room.
+ * Warning: Cannot be undone
+ */
+ void activateEncryption();
+
Q_SIGNALS:
/// Initial set of state events has been loaded
/**
@@ -619,11 +896,11 @@ Q_SIGNALS:
*/
void baseStateLoaded();
void eventsHistoryJobChanged();
- void aboutToAddHistoricalMessages(RoomEventsRange events);
- void aboutToAddNewMessages(RoomEventsRange events);
+ void aboutToAddHistoricalMessages(Quotient::RoomEventsRange events);
+ void aboutToAddNewMessages(Quotient::RoomEventsRange events);
void addedMessages(int fromIndex, int toIndex);
/// The event is about to be appended to the list of pending events
- void pendingEventAboutToAdd(RoomEvent* event);
+ void pendingEventAboutToAdd(Quotient::RoomEvent* event);
/// An event has been appended to the list of pending events
void pendingEventAdded();
/// The remote echo has arrived with the sync and will be merged
@@ -692,17 +969,26 @@ Q_SIGNALS:
Quotient::JoinState newState);
void typingChanged();
- void highlightCountChanged();
- void notificationCountChanged();
+ void highlightCountChanged(); //< \sa highlightCount
+ void notificationCountChanged(); //< \sa notificationCount
void displayedChanged(bool displayed);
void firstDisplayedEventChanged();
void lastDisplayedEventChanged();
+ //! The event that m.read receipt points to has changed
+ //! \sa lastReadReceipt
void lastReadEventChanged(Quotient::User* user);
+ void fullyReadMarkerMoved(QString fromEventId, QString toEventId);
+ //! \deprecated since 0.7 - use fullyReadMarkerMoved
void readMarkerMoved(QString fromEventId, QString toEventId);
+ //! \deprecated since 0.7 - use lastReadEventChanged
void readMarkerForUserMoved(Quotient::User* user, QString fromEventId,
QString toEventId);
+ //! \deprecated since 0.7 - use either partiallyReadStatsChanged
+ //! or unreadStatsChanged
void unreadMessagesChanged(Quotient::Room* room);
+ void partiallyReadStatsChanged();
+ void unreadStatsChanged();
void accountDataAboutToChange(QString type);
void accountDataChanged(QString type);
@@ -717,7 +1003,8 @@ Q_SIGNALS:
void fileTransferProgress(QString id, qint64 progress, qint64 total);
void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl);
void fileTransferFailed(QString id, QString errorMessage = {});
- void fileTransferCancelled(QString id);
+ // fileTransferCancelled() is no more here; use fileTransferFailed() and
+ // check the transfer status instead
void callEvent(Quotient::Room* room, const Quotient::RoomEvent* event);
@@ -743,6 +1030,7 @@ protected:
{}
virtual QJsonObject toJson() const;
virtual void updateData(SyncRoomData&& data, bool fromCache = false);
+ virtual Notification checkForNotifications(const TimelineItem& ti);
private:
friend class Connection;
@@ -761,7 +1049,7 @@ public:
explicit MemberSorter(const Room* r) : room(r) {}
bool operator()(User* u1, User* u2) const;
- bool operator()(User* u1, const QString& u2name) const;
+ bool operator()(User* u1, QStringView u2name) const;
template <typename ContT, typename ValT>
typename ContT::size_type lowerBoundIndex(const ContT& c, const ValT& v) const
@@ -774,4 +1062,5 @@ private:
};
} // namespace Quotient
Q_DECLARE_METATYPE(Quotient::FileTransferInfo)
+Q_DECLARE_METATYPE(Quotient::ReadReceipt)
Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::Room::Changes)
diff --git a/lib/settings.cpp b/lib/settings.cpp
index 703f4320..ed9082b0 100644
--- a/lib/settings.cpp
+++ b/lib/settings.cpp
@@ -21,7 +21,9 @@ void Settings::setLegacyNames(const QString& organizationName,
Settings::Settings(QObject* parent) : QSettings(parent)
{
+#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
setIniCodec("UTF-8");
+#endif
}
void Settings::setValue(const QString& key, const QVariant& value)
@@ -124,19 +126,6 @@ void AccountSettings::setHomeserver(const QUrl& url)
QString AccountSettings::userId() const { return group().section('/', -1); }
-QString AccountSettings::accessToken() const
-{
- return value(AccessTokenKey).toString();
-}
-
-void AccountSettings::setAccessToken(const QString& accessToken)
-{
- qCWarning(MAIN) << "Saving access_token to QSettings is insecure."
- " Developers, do it manually or contribute to share "
- "QtKeychain logic to libQuotient.";
- setValue(AccessTokenKey, accessToken);
-}
-
void AccountSettings::clearAccessToken()
{
legacySettings.remove(AccessTokenKey);
diff --git a/lib/settings.h b/lib/settings.h
index 84c54802..efd0d714 100644
--- a/lib/settings.h
+++ b/lib/settings.h
@@ -78,9 +78,8 @@ protected:
class SettingsGroup : public Settings {
public:
- template <typename... ArgTs>
- explicit SettingsGroup(QString path, ArgTs&&... qsettingsArgs)
- : Settings(std::forward<ArgTs>(qsettingsArgs)...)
+ explicit SettingsGroup(QString path, QObject* parent = nullptr)
+ : Settings(parent)
, groupPath(std::move(path))
{}
@@ -131,15 +130,11 @@ class AccountSettings : public SettingsGroup {
QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId)
QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName)
QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn)
- /** \deprecated \sa setAccessToken */
- Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken)
Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle
WRITE setEncryptionAccountPickle)
public:
- template <typename... ArgTs>
- explicit AccountSettings(const QString& accountId, ArgTs&&... qsettingsArgs)
- : SettingsGroup("Accounts/" + accountId,
- std::forward<ArgTs>(qsettingsArgs)...)
+ explicit AccountSettings(const QString& accountId, QObject* parent = nullptr)
+ : SettingsGroup("Accounts/" + accountId, parent)
{}
QString userId() const;
@@ -147,11 +142,7 @@ public:
QUrl homeserver() const;
void setHomeserver(const QUrl& url);
- /** \deprecated \sa setToken */
- QString accessToken() const;
- /** \deprecated Storing accessToken in QSettings is unsafe,
- * see quotient-im/Quaternion#181 */
- void setAccessToken(const QString& accessToken);
+ Q_DECL_DEPRECATED_X("Access tokens are not stored in QSettings any more")
Q_INVOKABLE void clearAccessToken();
QByteArray encryptionAccountPickle();
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index adcba5cd..396e77eb 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -10,9 +10,6 @@
using namespace Quotient;
-const QString SyncRoomData::UnreadCountKey =
- QStringLiteral("x-quotient.unread_count");
-
bool RoomSummary::isEmpty() const
{
return !joinedMemberCount && !invitedMemberCount && !heroes;
@@ -64,23 +61,23 @@ inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls));
}
-SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
- const QJsonObject& room_)
- : roomId(roomId_)
- , joinState(joinState_)
- , summary(fromJson<RoomSummary>(room_["summary"_ls]))
- , state(load<StateEvents>(room_, joinState == JoinState::Invite
+SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState,
+ const QJsonObject& roomJson)
+ : roomId(std::move(roomId_))
+ , joinState(joinState)
+ , summary(fromJson<RoomSummary>(roomJson["summary"_ls]))
+ , state(load<StateEvents>(roomJson, joinState == JoinState::Invite
? "invite_state"_ls
: "state"_ls))
{
switch (joinState) {
case JoinState::Join:
- ephemeral = load<Events>(room_, "ephemeral"_ls);
+ ephemeral = load<Events>(roomJson, "ephemeral"_ls);
[[fallthrough]];
case JoinState::Leave: {
- accountData = load<Events>(room_, "account_data"_ls);
- timeline = load<RoomEvents>(room_, "timeline"_ls);
- const auto timelineJson = room_.value("timeline"_ls).toObject();
+ accountData = load<Events>(roomJson, "account_data"_ls);
+ timeline = load<RoomEvents>(roomJson, "timeline"_ls);
+ const auto timelineJson = roomJson.value("timeline"_ls).toObject();
timelineLimited = timelineJson.value("limited"_ls).toBool();
timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString();
@@ -89,21 +86,24 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
default: /* nothing on top of state */;
}
- const auto unreadJson = room_.value("unread_notifications"_ls).toObject();
- unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
- highlightCount = unreadJson.value("highlight_count"_ls).toInt();
- notificationCount = unreadJson.value("notification_count"_ls).toInt();
- if (highlightCount > 0 || notificationCount > 0)
- qCDebug(SYNCJOB) << "Room" << roomId_
- << "has highlights:" << highlightCount
- << "and notifications:" << notificationCount;
+ const auto unreadJson = roomJson.value(UnreadNotificationsKey).toObject();
+
+ fromJson(unreadJson.value(PartiallyReadCountKey), partiallyReadCount);
+ if (!partiallyReadCount.has_value())
+ fromJson(unreadJson.value("x-quotient.unread_count"_ls),
+ partiallyReadCount);
+
+ fromJson(roomJson.value(NewUnreadCountKey), unreadCount);
+ if (!unreadCount.has_value())
+ fromJson(unreadJson.value("notification_count"_ls), unreadCount);
+ fromJson(unreadJson.value(HighlightCountKey), highlightCount);
}
SyncData::SyncData(const QString& cacheFileName)
{
QFileInfo cacheFileInfo { cacheFileName };
auto json = loadJson(cacheFileName);
- auto requiredVersion = std::get<0>(cacheVersion());
+ auto requiredVersion = MajorCacheVersion;
auto actualVersion =
json.value("cache_version"_ls).toObject().value("major"_ls).toInt();
if (actualVersion == requiredVersion)
@@ -128,6 +128,11 @@ Events&& SyncData::takeAccountData() { return std::move(accountData); }
Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); }
+std::pair<int, int> SyncData::cacheVersion()
+{
+ return { MajorCacheVersion, 2 };
+}
+
QJsonObject SyncData::loadJson(const QString& fileName)
{
QFile roomFile { fileName };
@@ -171,13 +176,14 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
deviceOneTimeKeysCount_);
auto rooms = json.value("rooms"_ls).toObject();
- JoinStates::Int ii = 1; // ii is used to make a JoinState value
auto totalRooms = 0;
auto totalEvents = 0;
- for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) {
+ for (size_t i = 0; i < JoinStateStrings.size(); ++i) {
+ // This assumes that MemberState values go over powers of 2: 1,2,4,...
+ const auto joinState = JoinState(1U << i);
const auto rs = rooms.value(JoinStateStrings[i]).toObject();
// We have a Qt container on the right and an STL one on the left
- roomData.reserve(static_cast<size_t>(rs.size()));
+ roomData.reserve(roomData.size() + static_cast<size_t>(rs.size()));
for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) {
auto roomJson =
roomIt->isObject()
@@ -187,7 +193,7 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
unresolvedRoomIds.push_back(roomIt.key());
continue;
}
- roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson);
+ roomData.emplace_back(roomIt.key(), joinState, roomJson);
const auto& r = roomData.back();
totalEvents += r.state.size() + r.ephemeral.size()
+ r.accountData.size() + r.timeline.size();
diff --git a/lib/syncdata.h b/lib/syncdata.h
index e69bac17..36d2e0bf 100644
--- a/lib/syncdata.h
+++ b/lib/syncdata.h
@@ -3,11 +3,17 @@
#pragma once
-#include "joinstate.h"
+#include "quotient_common.h"
#include "events/stateevent.h"
namespace Quotient {
+
+constexpr auto UnreadNotificationsKey = "unread_notifications"_ls;
+constexpr auto PartiallyReadCountKey = "x-quotient.since_fully_read_count"_ls;
+constexpr auto NewUnreadCountKey = "org.matrix.msc2654.unread_count"_ls;
+constexpr auto HighlightCountKey = "highlight_count"_ls;
+
/// Room summary, as defined in MSC688
/**
* Every member of this structure is an Omittable; as per the MSC, only
@@ -29,7 +35,6 @@ struct RoomSummary {
};
QDebug operator<<(QDebug dbg, const RoomSummary& rs);
-
template <>
struct JsonObjectConverter<RoomSummary> {
static void dumpTo(QJsonObject& jo, const RoomSummary& rs);
@@ -48,16 +53,14 @@ public:
bool timelineLimited;
QString timelinePrevBatch;
- int unreadCount;
- int highlightCount;
- int notificationCount;
+ Omittable<int> partiallyReadCount;
+ Omittable<int> unreadCount;
+ Omittable<int> highlightCount;
- SyncRoomData(const QString& roomId, JoinState joinState_,
- const QJsonObject& room_);
+ SyncRoomData(QString roomId, JoinState joinState,
+ const QJsonObject& roomJson);
SyncRoomData(SyncRoomData&&) = default;
SyncRoomData& operator=(SyncRoomData&&) = default;
-
- static const QString UnreadCountKey;
};
// QVector cannot work with non-copyable objects, std::vector can.
@@ -87,7 +90,8 @@ public:
QStringList unresolvedRooms() const { return unresolvedRoomIds; }
- static std::pair<int, int> cacheVersion() { return { 11, 0 }; }
+ static constexpr int MajorCacheVersion = 11;
+ static std::pair<int, int> cacheVersion();
static QString fileNameForRoom(QString roomId);
private:
diff --git a/lib/uri.cpp b/lib/uri.cpp
index 291bfcae..c8843dda 100644
--- a/lib/uri.cpp
+++ b/lib/uri.cpp
@@ -70,7 +70,7 @@ static QString pathSegment(const QUrl& url, int which)
encodedPath(url).section('/', which, which).toUtf8());
}
-static auto decodeFragmentPart(const QStringRef& part)
+static auto decodeFragmentPart(QStringView part)
{
return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8();
}
@@ -98,7 +98,7 @@ Uri::Uri(QUrl url) : QUrl(std::move(url))
if (scheme() == "matrix") {
// Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312
const auto& urlPath = encodedPath(*this);
- const auto& splitPath = urlPath.splitRef('/');
+ const auto& splitPath = urlPath.split('/');
switch (splitPath.size()) {
case 2:
break;
@@ -128,9 +128,9 @@ Uri::Uri(QUrl url) : QUrl(std::move(url))
// so force QUrl to decode everything.
auto f = fragment(QUrl::EncodeUnicode);
if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch())
- *this = Uri { decodeFragmentPart(m.capturedRef("main")),
- decodeFragmentPart(m.capturedRef("sec")),
- decodeFragmentPart(m.capturedRef("query")) };
+ *this = Uri { decodeFragmentPart(m.capturedView(u"main")),
+ decodeFragmentPart(m.capturedView(u"sec")),
+ decodeFragmentPart(m.capturedView(u"query")) };
}
}
@@ -186,14 +186,18 @@ QString Uri::primaryId() const
if (primaryType_ == Empty || primaryType_ == Invalid)
return {};
- const auto& idStem = pathSegment(*this, 1);
- return idStem.isEmpty() ? idStem : primaryType_ + idStem;
+ auto idStem = pathSegment(*this, 1);
+ if (!idStem.isEmpty())
+ idStem.push_front(char(primaryType_));
+ return idStem;
}
QString Uri::secondaryId() const
{
- const auto& idStem = pathSegment(*this, 3);
- return idStem.isEmpty() ? idStem : secondaryType() + idStem;
+ auto idStem = pathSegment(*this, 3);
+ if (!idStem.isEmpty())
+ idStem.push_front(char(secondaryType()));
+ return idStem;
}
static const auto ActionKey = QStringLiteral("action");
diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp
index 287e0552..681e3842 100644
--- a/lib/uriresolver.cpp
+++ b/lib/uriresolver.cpp
@@ -8,6 +8,8 @@
using namespace Quotient;
+UriResolverBase::~UriResolverBase() = default;
+
UriResolveResult UriResolverBase::visitResource(Connection* account,
const Uri& uri)
{
diff --git a/lib/uriresolver.h b/lib/uriresolver.h
index f290e58b..ff97324a 100644
--- a/lib/uriresolver.h
+++ b/lib/uriresolver.h
@@ -42,23 +42,29 @@ public:
UriResolveResult visitResource(Connection* account, const Uri& uri);
protected:
+ virtual ~UriResolverBase() = 0;
+
/// Called by visitResource() when the passed URI identifies a Matrix user
/*!
* \return IncorrectAction if the action is not correct or not supported;
* UriResolved if it is accepted; other values are disallowed
*/
- virtual UriResolveResult visitUser(User* user, const QString& action)
+ virtual UriResolveResult visitUser(User* user [[maybe_unused]],
+ const QString& action [[maybe_unused]])
{
return IncorrectAction;
}
/// Called by visitResource() when the passed URI identifies a room or
/// an event in a room
- virtual void visitRoom(Room* room, const QString& eventId) {}
+ virtual void visitRoom(Room* room [[maybe_unused]],
+ const QString& eventId [[maybe_unused]])
+ {}
/// Called by visitResource() when the passed URI has `action() == "join"`
/// and identifies a room that the user defined by the Connection argument
/// is not a member of
- virtual void joinRoom(Connection* account, const QString& roomAliasOrId,
- const QStringList& viaServers = {})
+ virtual void joinRoom(Connection* account [[maybe_unused]],
+ const QString& roomAliasOrId [[maybe_unused]],
+ const QStringList& viaServers [[maybe_unused]] = {})
{}
/// Called by visitResource() when the passed URI has `type() == NonMatrix`
/*!
@@ -67,7 +73,10 @@ protected:
* `return QDesktopServices::openUrl(url);` but it's strongly advised to
* ask for a user confirmation beforehand.
*/
- virtual bool visitNonMatrix(const QUrl& url) { return false; }
+ virtual bool visitNonMatrix(const QUrl& url [[maybe_unused]])
+ {
+ return false;
+ }
};
/*! \brief Resolve the resource and invoke an action on it, via function objects
diff --git a/lib/user.cpp b/lib/user.cpp
index 7933c5d9..7da71dba 100644
--- a/lib/user.cpp
+++ b/lib/user.cpp
@@ -65,7 +65,8 @@ User::~User() = default;
void User::load()
{
- auto *profileJob = connection()->callApi<GetUserProfileJob>(id());
+ auto* profileJob =
+ connection()->callApi<GetUserProfileJob>(id());
connect(profileJob, &BaseJob::result, this, [this, profileJob] {
d->defaultName = profileJob->displayname();
d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl()));
@@ -81,7 +82,7 @@ bool User::isGuest() const
Q_ASSERT(!d->id.isEmpty() && d->id.startsWith('@'));
auto it = std::find_if_not(d->id.cbegin() + 1, d->id.cend(),
[](QChar c) { return c.isDigit(); });
- Q_ASSERT(it != d->id.end());
+ Q_ASSERT(it != d->id.cend());
return *it == ':';
}
@@ -92,8 +93,6 @@ QString User::name(const Room* room) const
return room ? room->memberName(id()) : d->defaultName;
}
-QString User::rawName(const Room* room) const { return name(room); }
-
void User::rename(const QString& newName)
{
const auto actualNewName = sanitized(newName);
@@ -121,11 +120,11 @@ void User::rename(const QString& newName, const Room* r)
rename(newName);
return;
}
- Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__,
+ // #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");
- const auto actualNewName = sanitized(newName);
- MemberEventContent evtC;
- evtC.displayName = actualNewName;
+ evtC.displayName = sanitized(newName);
r->setState<RoomMemberEvent>(id(), move(evtC));
// The state will be updated locally after it arrives with sync
}
@@ -134,17 +133,17 @@ template <typename SourceT>
inline bool User::doSetAvatar(SourceT&& source)
{
return d->defaultAvatar.upload(
- connection(), source, [this](const QString& contentUri) {
+ connection(), source, [this](const QUrl& contentUri) {
auto* j = connection()->callApi<SetAvatarUrlJob>(id(), contentUri);
connect(j, &BaseJob::success, this,
- [this, newUrl = QUrl(contentUri)] {
- if (newUrl == d->defaultAvatar.url()) {
- d->defaultAvatar.updateUrl(newUrl);
+ [this, contentUri] {
+ if (contentUri == d->defaultAvatar.url()) {
+ d->defaultAvatar.updateUrl(contentUri);
emit defaultAvatarChanged();
} else
qCWarning(MAIN) << "User" << id()
<< "already has avatar URL set to"
- << newUrl.toDisplayString();
+ << contentUri.toDisplayString();
});
});
}
@@ -161,7 +160,7 @@ bool User::setAvatar(QIODevice* source)
void User::removeAvatar()
{
- connection()->callApi<SetAvatarUrlJob>(id(), "");
+ connection()->callApi<SetAvatarUrlJob>(id(), QUrl());
}
void User::requestDirectChat() { connection()->requestDirectChat(this); }
@@ -184,8 +183,6 @@ QString User::fullName(const Room* room) const
return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')');
}
-QString User::bridged() const { return {}; }
-
const Avatar& User::avatarObject(const Room* room) const
{
if (!room)
@@ -196,18 +193,18 @@ const Avatar& User::avatarObject(const Room* room) const
return d->otherAvatars.try_emplace(mediaId, url).first->second;
}
-QImage User::avatar(int dimension, const Room* room)
+QImage User::avatar(int dimension, const Room* room) const
{
return avatar(dimension, dimension, room);
}
-QImage User::avatar(int width, int height, const Room* room)
+QImage User::avatar(int width, int height, const Room* room) const
{
return avatar(width, height, room, [] {});
}
QImage User::avatar(int width, int height, const Room* room,
- const Avatar::get_callback_t& callback)
+ const Avatar::get_callback_t& callback) const
{
return avatarObject(room).get(connection(), width, height, callback);
}
diff --git a/lib/user.h b/lib/user.h
index e4560843..78b72bf2 100644
--- a/lib/user.h
+++ b/lib/user.h
@@ -22,7 +22,6 @@ class User : public QObject {
Q_PROPERTY(QString name READ name NOTIFY defaultNameChanged)
Q_PROPERTY(QString displayName READ displayname NOTIFY defaultNameChanged STORED false)
Q_PROPERTY(QString fullName READ fullName NOTIFY defaultNameChanged STORED false)
- Q_PROPERTY(QString bridgeName READ bridged NOTIFY defaultNameChanged STORED false)
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY defaultAvatarChanged STORED false)
Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged)
public:
@@ -40,18 +39,10 @@ public:
* This may be empty if the user didn't choose the name or cleared
* it. If the user is bridged, the bridge postfix (such as '(IRC)')
* is stripped out. No disambiguation for the room is done.
- * \sa displayName, rawName
+ * \sa displayName
*/
QString name(const Room* room = nullptr) const;
- /** Get the user name along with the bridge postfix
- * This function is similar to name() but appends the bridge postfix
- * (such as '(IRC)') to the user name. No disambiguation is done.
- * \sa name, displayName
- */
- [[deprecated("Bridge postfixes exist no more, use name() instead")]]
- QString rawName(const Room* room = nullptr) const;
-
/** Get the displayed user name
* When \p room is null, this method returns result of name() if
* the name is non-empty; otherwise it returns user id.
@@ -70,13 +61,6 @@ public:
*/
QString fullName(const Room* room = nullptr) const;
- /**
- * Returns the name of bridge the user is connected from or empty.
- */
- [[deprecated("Bridged status is no more supported; this always returns"
- " an empty string")]]
- QString bridged() const;
-
/** Whether the user is a guest
* As of now, the function relies on the convention used in Synapse
* that guests and only guests have all-numeric IDs. This may or
@@ -99,11 +83,11 @@ public:
*/
const Avatar& avatarObject(const Room* room = nullptr) const;
Q_INVOKABLE QImage avatar(int dimension,
- const Quotient::Room* room = nullptr);
+ const Quotient::Room* room = nullptr) const;
Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight,
- const Quotient::Room* room = nullptr);
+ const Quotient::Room* room = nullptr) const;
QImage avatar(int width, int height, const Room* room,
- const Avatar::get_callback_t& callback);
+ const Avatar::get_callback_t& callback) const;
QString avatarMediaId(const Room* room = nullptr) const;
QUrl avatarUrl(const Room* room = nullptr) const;
diff --git a/lib/util.cpp b/lib/util.cpp
index 904bfd5a..03ebf325 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -14,9 +14,6 @@
static const auto RegExpOptions =
QRegularExpression::CaseInsensitiveOption
-#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0)
- | QRegularExpression::OptimizeOnFirstUsageOption // Default since 5.12
-#endif
| QRegularExpression::UseUnicodePropertiesOption;
// Converts all that looks like a URL into HTML links
@@ -33,7 +30,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText)
// comma or dot
static const QRegularExpression FullUrlRegExp(
QStringLiteral(
- R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
+ R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
RegExpOptions);
// email address:
// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
@@ -44,7 +41,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText)
// https://matrix.org/docs/spec/appendices.html#identifier-grammar
static const QRegularExpression MxIdRegExp(
QStringLiteral(
- R"((^|[^<>/])([!#@][-a-z0-9_=#/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"),
+ R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
RegExpOptions);
Q_ASSERT(FullUrlRegExp.isValid() && EmailAddressRegExp.isValid()
&& MxIdRegExp.isValid());
@@ -119,34 +116,22 @@ QString Quotient::serverPart(const QString& mxId)
return parser.match(mxId).captured(1);
}
-// 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");
+QString Quotient::versionString()
+{
+ return QStringLiteral(Quotient_VERSION_STRING);
+}
+
+int Quotient::majorVersion()
+{
+ return Quotient_VERSION_MAJOR;
+}
+
+int Quotient::minorVersion()
+{
+ return Quotient_VERSION_MINOR;
+}
+
+int Quotient::patchVersion()
+{
+ return Quotient_VERSION_PATCH;
+}
diff --git a/lib/util.h b/lib/util.h
index cb0ff44a..97f0ecbc 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -12,10 +12,30 @@
#include <unordered_map>
#include <optional>
-// Along the lines of Q_DISABLE_COPY - the upstream version comes in Qt 5.13
-#define DISABLE_MOVE(_ClassName) \
- _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \
- _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE;
+#ifndef Q_DISABLE_MOVE
+// Q_DISABLE_MOVE was introduced in Q_VERSION_CHECK(5,13,0)
+# define Q_DISABLE_MOVE(_ClassName) \
+ _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \
+ _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE;
+#endif
+
+#ifndef Q_DISABLE_COPY_MOVE
+#define Q_DISABLE_COPY_MOVE(Class) \
+ Q_DISABLE_COPY(Class) \
+ Q_DISABLE_MOVE(Class)
+#endif
+
+#define DISABLE_MOVE(_ClassName) \
+static_assert(false, "Use Q_DISABLE_MOVE instead; Quotient enables it across all used versions of Qt");
+
+#ifndef QT_IGNORE_DEPRECATIONS
+// QT_IGNORE_DEPRECATIONS was introduced in Q_VERSION_CHECK(5,15,0)
+# define QT_IGNORE_DEPRECATIONS(statement) \
+ QT_WARNING_PUSH \
+ QT_WARNING_DISABLE_DEPRECATED \
+ statement \
+ QT_WARNING_POP
+#endif
namespace Quotient {
/// An equivalent of std::hash for QTypes to enable std::unordered_map<QType, ...>
@@ -30,6 +50,13 @@ 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
@@ -107,18 +134,18 @@ public:
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
+ //! Merge the value from another Omittable
+ //! \return true if \p other is not omitted and the value of
+ //! the current Omittable was different (or omitted),
+ //! in other words, if the current Omittable has changed;
+ //! false otherwise
template <typename T1>
auto merge(const Omittable<T1>& other)
- -> std::enable_if_t<std::is_convertible<T1, T>::value, bool>
+ -> std::enable_if_t<std::is_convertible_v<T1, T>, bool>
{
if (!other || (this->has_value() && **this == *other))
return false;
- *this = other;
+ emplace(*other);
return true;
}
@@ -131,72 +158,38 @@ public:
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 AlwaysVoid, typename>
- struct fn_traits {};
+ template <typename T>
+ constexpr inline auto IsOmittableValue<Omittable<T>> = true;
}
-/// 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...)>;
-};
+template <typename T1, typename T2>
+inline auto merge(Omittable<T1>& lhs, T2&& rhs)
+{
+ return lhs.merge(std::forward<T2>(rhs));
+}
-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>;
-
-// TODO: get rid of it as soon as Apple Clang gets proper deduction guides
-// for std::function<>
-// ...or consider using QtPrivate magic used by QObject::connect()
-// since wrap_in_function() is actually made for qt_connection_util.h
-// ...for inspiration, also check a possible std::not_fn implementation at
-// https://en.cppreference.com/w/cpp/utility/functional/not_fn
-template <typename FnT>
-inline auto wrap_in_function(FnT&& f)
+//! \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>
{
- return typename function_traits<FnT>::function_type(std::forward<FnT>(f));
+ if (!rhs || lhs == *rhs)
+ return false;
+ lhs = *rhs;
+ return true;
}
-inline auto operator"" _ls(const char* s, std::size_t size)
+inline constexpr auto operator"" _ls(const char* s, std::size_t size)
{
return QLatin1String(s, int(size));
}
@@ -283,4 +276,9 @@ qreal stringToHueF(const QString& s);
/** Extract the serverpart from MXID */
QString serverPart(const QString& mxId);
+
+QString versionString();
+int majorVersion();
+int minorVersion();
+int patchVersion();
} // namespace Quotient
diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt
index 29c53fae..cb41141d 100644
--- a/quotest/CMakeLists.txt
+++ b/quotest/CMakeLists.txt
@@ -4,10 +4,26 @@
set(quotest_SRCS quotest.cpp)
+find_package(${Qt} COMPONENTS Concurrent)
add_executable(quotest ${quotest_SRCS})
-target_link_libraries(quotest PRIVATE Qt5::Core Qt5::Test ${PROJECT_NAME})
+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)
+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 a0914c72..7bd9c5c3 100644
--- a/quotest/quotest.cpp
+++ b/quotest/quotest.cpp
@@ -5,6 +5,7 @@
#include "room.h"
#include "user.h"
#include "uriresolver.h"
+#include "networkaccessmanager.h"
#include "csapi/joining.h"
#include "csapi/leaving.h"
@@ -20,6 +21,8 @@
#include <QtCore/QStringBuilder>
#include <QtCore/QTemporaryFile>
#include <QtCore/QTimer>
+#include <QtConcurrent/QtConcurrent>
+#include <QtNetwork/QNetworkReply>
#include <functional>
#include <iostream>
@@ -48,7 +51,7 @@ private:
QByteArrayList running {}, succeeded {}, failed {};
};
-using TestToken = QByteArray; // return value of QMetaMethod::name
+using TestToken = decltype(std::declval<QMetaMethod>().name());
Q_DECLARE_METATYPE(TestToken)
// For now, the token itself is the test name but that may change.
@@ -105,6 +108,7 @@ private slots:
TEST_DECL(addAndRemoveTag)
TEST_DECL(markDirectChat)
TEST_DECL(visitResources)
+ TEST_DECL(prettyPrintTests)
// Add more tests above here
public:
@@ -137,7 +141,7 @@ private:
// connectUntil() to break the QMetaObject::Connection upon finishing the test
// item.
#define FINISH_TEST(Condition) \
- return (finishTest(thisTest, Condition, __FILE__, __LINE__), true)
+ return (finishTest(thisTest, (Condition), __FILE__, __LINE__), true)
#define FAIL_TEST() FINISH_TEST(false)
@@ -210,6 +214,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
@@ -241,7 +246,7 @@ void TestManager::setupAndRun()
clog << "Sync " << ++i << " complete" << endl;
if (auto* r = testSuite->room()) {
clog << "Test room timeline size = " << r->timelineSize();
- if (r->pendingEvents().empty())
+ if (!r->pendingEvents().empty())
clog << ", pending size = " << r->pendingEvents().size();
clog << endl;
}
@@ -397,15 +402,16 @@ TEST_IMPL(sendFile)
}
tf->write("Test");
tf->close();
+ QFileInfo tfi { *tf };
// QFileInfo::fileName brings only the file name; QFile::fileName brings
// the full path
- const auto tfName = QFileInfo(*tf).fileName();
+ const auto tfName = tfi.fileName();
clog << "Sending file " << tfName.toStdString() << endl;
- const auto txnId =
- targetRoom->postFile("Test file", QUrl::fromLocalFile(tf->fileName()));
+ const auto txnId = targetRoom->postFile(
+ "Test file", new EventContent::FileContent(tfi));
if (!validatePendingEvent(txnId)) {
clog << "Invalid pending event right after submitting" << endl;
- delete tf;
+ tf->deleteLater();
FAIL_TEST();
}
@@ -434,6 +440,39 @@ TEST_IMPL(sendFile)
return false;
}
+// Can be replaced with a lambda once QtConcurrent is able to resolve return
+// types from lambda invocations (Qt 6 can, not sure about earlier)
+struct DownloadRunner {
+ QUrl url;
+
+ using result_type = QNetworkReply::NetworkError;
+
+ QNetworkReply::NetworkError operator()(int) const
+ {
+ QEventLoop el;
+ QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply {
+ NetworkAccessManager::instance()->get(QNetworkRequest(url))
+ };
+ QObject::connect(
+ reply.data(), &QNetworkReply::finished, &el, [&el] { el.exit(); },
+ Qt::QueuedConnection);
+ el.exec();
+ return reply->error();
+ }
+};
+
+bool testDownload(const QUrl& url)
+{
+ // Move out actual test from the multithreaded code
+ // to help debugging
+ auto results = QtConcurrent::blockingMapped(QVector<int> { 1, 2, 3 },
+ DownloadRunner { url });
+ return std::all_of(results.cbegin(), results.cend(),
+ [](QNetworkReply::NetworkError ne) {
+ return ne == QNetworkReply::NoError;
+ });
+}
+
bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest,
const QString& txnId,
const QString& fileName)
@@ -460,18 +499,19 @@ 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: actually try to download it to check, e.g., #366
- // (and #368 would help to test against bad file names).
+ // TODO: check #366 once #368 is implemented
FINISH_TEST(
!e.id().isEmpty()
- && pendingEvents[size_t(pendingIdx)]->transactionId()
- == txnId
- && e.hasFileContent()
- && e.content()->fileInfo()->originalName == fileName);
+ && pendingEvents[size_t(pendingIdx)]->transactionId()
+ == txnId
+ && e.hasFileContent()
+ && e.content()->fileInfo()->originalName == fileName
+ && testDownload(targetRoom->connection()->makeMediaUrl(
+ e.content()->fileInfo()->url)));
},
[this, thisTest](const RoomEvent&) { FAIL_TEST(); });
});
@@ -498,14 +538,32 @@ 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::memberRenamed, this,
+ [this, thisTest, localUser, newName](const User* u) {
+ if (localUser != u)
+ return false;
+ if (localUser->name(targetRoom) != 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;
}
@@ -556,7 +614,7 @@ bool TestSuite::checkRedactionOutcome(const QByteArray& thisTest,
// redacted at the next sync, or the nearest sync completes with
// the unredacted event but the next one brings redaction.
auto it = targetRoom->findInTimeline(evtIdToRedact);
- if (it == targetRoom->timelineEdge())
+ if (it == targetRoom->historyEdge())
return false; // Waiting for the next sync
if ((*it)->isRedacted()) {
@@ -786,6 +844,52 @@ TEST_IMPL(visitResources)
FINISH_TEST(true);
}
+bool checkPrettyPrint(
+ std::initializer_list<std::pair<const char*, const char*>> tests)
+{
+ bool result = true;
+ for (const auto& [test, etalon] : tests) {
+ const auto is = prettyPrint(test).toStdString();
+ const auto shouldBe = std::string("<span style='white-space:pre-wrap'>")
+ + etalon + "</span>";
+ if (is == shouldBe)
+ continue;
+ clog << is << " != " << shouldBe << endl;
+ result = false;
+ }
+ return result;
+}
+
+TEST_IMPL(prettyPrintTests)
+{
+ const bool prettyPrintTestResult = checkPrettyPrint(
+ { { "https://www.matrix.org",
+ R"(<a href="https://www.matrix.org">https://www.matrix.org</a>)" },
+// { "www.matrix.org", // Doesn't work yet
+// R"(<a href="https://www.matrix.org">www.matrix.org</a>)" },
+ { "smb://somewhere/file", "smb://somewhere/file" }, // Disallowed scheme
+ { "https:/something", "https:/something" }, // Malformed URL
+ { "https://matrix.to/#/!roomid:example.org",
+ R"(<a href="https://matrix.to/#/!roomid:example.org">https://matrix.to/#/!roomid:example.org</a>)" },
+ { "https://matrix.to/#/@user_id:example.org",
+ R"(<a href="https://matrix.to/#/@user_id:example.org">https://matrix.to/#/@user_id:example.org</a>)" },
+ { "https://matrix.to/#/#roomalias:example.org",
+ R"(<a href="https://matrix.to/#/#roomalias:example.org">https://matrix.to/#/#roomalias:example.org</a>)" },
+ { "https://matrix.to/#/##ircroomalias:example.org",
+ R"(<a href="https://matrix.to/#/##ircroomalias:example.org">https://matrix.to/#/##ircroomalias:example.org</a>)" },
+ { "me@example.org",
+ R"(<a href="mailto:me@example.org">me@example.org</a>)" },
+ { "mailto:me@example.org",
+ R"(<a href="mailto:me@example.org">mailto:me@example.org</a>)" },
+ { "!room_id:example.org",
+ R"(<a href="https://matrix.to/#/!room_id:example.org">!room_id:example.org</a>)" },
+ { "@user_id:example.org",
+ R"(<a href="https://matrix.to/#/@user_id:example.org">@user_id:example.org</a>)" },
+ { "#room_alias:example.org",
+ R"(<a href="https://matrix.to/#/#room_alias:example.org">#room_alias:example.org</a>)" } });
+ FINISH_TEST(prettyPrintTestResult);
+}
+
void TestManager::conclude()
{
// Clean up the room (best effort)
@@ -854,10 +958,22 @@ void TestManager::conclude()
void TestManager::finalize()
{
+ if (!c->isUsable() || !c->isLoggedIn()) {
+ clog << "No usable connection reached" << endl;
+ QCoreApplication::exit(-2);
+ return; // NB: QCoreApplication::exit() does return to the caller
+ }
clog << "Logging out" << endl;
c->logout();
- connect(c, &Connection::loggedOut, this,
- [this] { QCoreApplication::exit(failed.size() + running.size()); },
+ connect(
+ c, &Connection::loggedOut, this,
+ [this] {
+ QCoreApplication::exit(!testSuite ? -3
+ : succeeded.empty() && failed.empty()
+ && running.empty()
+ ? -4
+ : failed.size() + running.size());
+ },
Qt::QueuedConnection);
}
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 00000000..96e98985
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,11 @@
+sonar.projectKey=quotient-im_libQuotient
+sonar.organization=quotient-im
+
+# This is the name and version displayed in the SonarCloud UI.
+sonar.projectName=libQuotient
+sonar.projectVersion=0.7
+
+sonar.sources=lib
+
+# Encoding of the source code. Default is default system encoding
+#sonar.sourceEncoding=UTF-8